OCaml library for JSONfeed parsing and creation

add support for the _references extension

see https://github.com/egonw/JSONFeed-extensions/blob/main/references.md

+1 -1
dune-project
···
-
(lang dune 3.20)
(name jsonfeed)
···
+
(lang dune 3.18)
(name jsonfeed)
+151
lib/cito.mli
···
···
+
(** Citation Typing Ontology (CiTO) intent annotations.
+
+
CiTO provides a structured vocabulary for describing the nature of citations.
+
This module implements support for CiTO annotations as used in the references extension.
+
+
@see <https://purl.archive.org/spar/cito> Citation Typing Ontology
+
@see <https://sparontologies.github.io/cito/current/cito.html> CiTO Specification *)
+
+
+
(** CiTO citation intent annotation.
+
+
Represents the intent or nature of a citation using the Citation Typing Ontology.
+
Each variant corresponds to a specific CiTO property. The [`Other] variant allows
+
for custom or future CiTO terms not yet included in this library.
+
+
{b Categories:}
+
- Factual: Citing for data, methods, evidence, or information
+
- Critical: Agreement, disagreement, correction, or qualification
+
- Rhetorical: Style-based citations (parody, ridicule, etc.)
+
- Relational: Document relationships and compilations
+
- Support: Providing or obtaining backing and context
+
- Exploratory: Speculation and recommendations
+
- Quotation: Direct quotes and excerpts
+
- Dialogue: Replies and responses
+
- Sharing: Common attributes between works *)
+
type t = [
+
| `Cites (** The base citation property *)
+
+
(* Factual citation intents *)
+
| `CitesAsAuthority (** Cites as authoritative source *)
+
| `CitesAsDataSource (** Cites as origin of data *)
+
| `CitesAsEvidence (** Cites for factual evidence *)
+
| `CitesForInformation (** Cites as information source *)
+
| `UsesDataFrom (** Uses data from cited work *)
+
| `UsesMethodIn (** Uses methodology from cited work *)
+
| `UsesConclusionsFrom (** Applies conclusions from cited work *)
+
+
(* Agreement/disagreement *)
+
| `AgreesWith (** Concurs with cited statements *)
+
| `DisagreesWith (** Rejects cited statements *)
+
| `Confirms (** Validates facts in cited work *)
+
| `Refutes (** Disproves cited statements *)
+
| `Disputes (** Contests without definitive refutation *)
+
+
(* Critical engagement *)
+
| `Critiques (** Analyzes and finds fault *)
+
| `Qualifies (** Places conditions on statements *)
+
| `Corrects (** Fixes errors in cited work *)
+
| `Updates (** Advances understanding beyond cited work *)
+
| `Extends (** Builds upon cited facts *)
+
+
(* Rhetorical/stylistic *)
+
| `Parodies (** Imitates for comic effect *)
+
| `Plagiarizes (** Uses without acknowledgment *)
+
| `Derides (** Expresses contempt *)
+
| `Ridicules (** Mocks cited work *)
+
+
(* Document relationships *)
+
| `Describes (** Characterizes cited entity *)
+
| `Documents (** Records information about source *)
+
| `CitesAsSourceDocument (** Cites as foundational source *)
+
| `CitesAsMetadataDocument (** Cites containing metadata *)
+
| `Compiles (** Uses to create new work *)
+
| `Reviews (** Examines cited statements *)
+
| `Retracts (** Formally withdraws *)
+
+
(* Support/context *)
+
| `Supports (** Provides intellectual backing *)
+
| `GivesSupportTo (** Provides support to citing entity *)
+
| `ObtainsSupportFrom (** Obtains backing from cited work *)
+
| `GivesBackgroundTo (** Provides context *)
+
| `ObtainsBackgroundFrom (** Obtains context from cited work *)
+
+
(* Exploratory *)
+
| `SpeculatesOn (** Theorizes without firm evidence *)
+
| `CitesAsPotentialSolution (** Offers possible resolution *)
+
| `CitesAsRecommendedReading (** Suggests as further reading *)
+
| `CitesAsRelated (** Identifies as thematically connected *)
+
+
(* Quotation/excerpting *)
+
| `IncludesQuotationFrom (** Incorporates direct quotes *)
+
| `IncludesExcerptFrom (** Uses non-quoted passages *)
+
+
(* Dialogue *)
+
| `RepliesTo (** Responds to cited statements *)
+
| `HasReplyFrom (** Evokes response *)
+
+
(* Linking *)
+
| `LinksTo (** Provides URL hyperlink *)
+
+
(* Shared attribution *)
+
| `SharesAuthorWith (** Common authorship *)
+
| `SharesJournalWith (** Published in same journal *)
+
| `SharesPublicationVenueWith (** Published in same venue *)
+
| `SharesFundingAgencyWith (** Funded by same agency *)
+
| `SharesAuthorInstitutionWith (** Authors share affiliation *)
+
+
(* Extensibility *)
+
| `Other of string (** Custom or future CiTO term *)
+
]
+
+
+
(** {1 Conversion} *)
+
+
(** [of_string s] converts a CiTO term string to its variant representation.
+
+
Recognized CiTO terms are converted to their corresponding variants.
+
Unrecognized terms are wrapped in [`Other].
+
+
The comparison is case-insensitive for standard CiTO terms but preserves
+
the original case in [`Other] variants.
+
+
{b Examples:}
+
{[
+
of_string "cites" (* returns `Cites *)
+
of_string "usesMethodIn" (* returns `UsesMethodIn *)
+
of_string "citesAsRecommendedReading" (* returns `CitesAsRecommendedReading *)
+
of_string "customTerm" (* returns `Other "customTerm" *)
+
]} *)
+
val of_string : string -> t
+
+
(** [to_string t] converts a CiTO variant to its canonical string representation.
+
+
Standard CiTO terms use their official CiTO local names (camelCase).
+
[`Other] variants return the wrapped string unchanged.
+
+
{b Examples:}
+
{[
+
to_string `Cites (* returns "cites" *)
+
to_string `UsesMethodIn (* returns "usesMethodIn" *)
+
to_string (`Other "customTerm") (* returns "customTerm" *)
+
]} *)
+
val to_string : t -> string
+
+
+
(** {1 Comparison} *)
+
+
(** [equal a b] tests equality between two CiTO annotations.
+
+
Two annotations are equal if they represent the same CiTO term.
+
For [`Other] variants, string comparison is case-sensitive. *)
+
val equal : t -> t -> bool
+
+
+
(** {1 Pretty Printing} *)
+
+
(** [pp ppf t] pretty prints a CiTO annotation to the formatter.
+
+
{b Example output:}
+
{v citesAsRecommendedReading v} *)
+
val pp : Format.formatter -> t -> unit
+2 -1
lib/dune
···
(library
(name jsonfeed)
(public_name jsonfeed)
-
(libraries jsonm ptime fmt))
···
(library
(name jsonfeed)
(public_name jsonfeed)
+
(libraries jsonm ptime fmt)
+
(modules_without_implementation cito reference))
+4 -1
lib/item.ml
···
tags : string list option;
language : string option;
attachments : Attachment.t list option;
}
let create ~id ~content ?url ?external_url ?title ?summary ?image ?banner_image
-
?date_published ?date_modified ?authors ?tags ?language ?attachments () =
{
id;
content;
···
tags;
language;
attachments;
}
let id t = t.id
···
let tags t = t.tags
let language t = t.language
let attachments t = t.attachments
let content_html t =
match t.content with
···
tags : string list option;
language : string option;
attachments : Attachment.t list option;
+
references : Reference.t list option;
}
let create ~id ~content ?url ?external_url ?title ?summary ?image ?banner_image
+
?date_published ?date_modified ?authors ?tags ?language ?attachments ?references () =
{
id;
content;
···
tags;
language;
attachments;
+
references;
}
let id t = t.id
···
let tags t = t.tags
let language t = t.language
let attachments t = t.attachments
+
let references t = t.references
let content_html t =
match t.content with
+17
lib/item.mli
···
@param tags Plain text tags/categories for the item
@param language Primary language of the item (RFC 5646 format, e.g. ["en-US"])
@param attachments Related resources like audio files or downloads
{b Examples:}
{[
···
~content:(`Html "<p>Episode description</p>")
~title:"Episode 1"
~attachments:[attachment] ()
]} *)
val create :
id:string ->
···
?tags:string list ->
?language:string ->
?attachments:Attachment.t list ->
unit ->
t
···
(** [attachments t] returns the item's attachments, if set. *)
val attachments : t -> Attachment.t list option
(** {1 Content Helpers} *)
···
@param tags Plain text tags/categories for the item
@param language Primary language of the item (RFC 5646 format, e.g. ["en-US"])
@param attachments Related resources like audio files or downloads
+
@param references References to cited sources (extension)
{b Examples:}
{[
···
~content:(`Html "<p>Episode description</p>")
~title:"Episode 1"
~attachments:[attachment] ()
+
+
(* Article with references *)
+
let reference = Reference.create
+
~url:"https://doi.org/10.5281/zenodo.16755947"
+
~doi:"10.5281/zenodo.16755947"
+
~cito:[`CitesAsRecommendedReading; `UsesMethodIn] () in
+
let item = Item.create
+
~id:"https://doi.org/10.59350/krw9n-dv417"
+
~content:(`Html "<p>Research article content</p>")
+
~title:"One Million IUPAC names #4: a lot is happening"
+
~url:"https://chem-bla-ics.linkedchemistry.info/2025/08/09/one-million-iupac-names-4.html"
+
~references:[reference] ()
]} *)
val create :
id:string ->
···
?tags:string list ->
?language:string ->
?attachments:Attachment.t list ->
+
?references:Reference.t list ->
unit ->
t
···
(** [attachments t] returns the item's attachments, if set. *)
val attachments : t -> Attachment.t list option
+
+
(** [references t] returns the item's references, if set. *)
+
val references : t -> Reference.t list option
(** {1 Content Helpers} *)
+2
lib/jsonfeed.ml
···
module Attachment = Attachment
module Hub = Hub
module Item = Item
type t = {
version : string;
···
module Attachment = Attachment
module Hub = Hub
module Item = Item
+
module Reference = Reference
+
module Cito = Cito
type t = {
version : string;
+6
lib/jsonfeed.mli
···
(** Feed items (posts, episodes, entries). *)
module Item = Item
···
(** Feed items (posts, episodes, entries). *)
module Item = Item
+
+
(** References to cited sources in items (extension). *)
+
module Reference = Reference
+
+
(** Citation Typing Ontology annotations for references (extension). *)
+
module Cito = Cito
+83
lib/reference.mli
···
···
+
(** References extension for JSON Feed items.
+
+
This implements the references extension that allows items to cite sources.
+
Each reference represents a cited resource with optional DOI and CiTO annotations.
+
+
@see <https://github.com/egonw/JSONFeed-extensions/blob/main/references.md> References Extension Specification
+
@see <https://purl.archive.org/spar/cito> Citation Typing Ontology *)
+
+
+
(** The type representing a reference to a cited source. *)
+
type t
+
+
+
(** {1 Construction} *)
+
+
(** [create ~url ?doi ?cito ()] creates a reference.
+
+
@param url Unique URL for the reference (required).
+
A URL based on a persistent unique identifier (like DOI) is recommended.
+
@param doi Digital Object Identifier for the reference
+
@param cito Citation Typing Ontology intent annotations
+
+
{b Examples:}
+
{[
+
(* Simple reference with just a URL *)
+
let ref1 = Reference.create
+
~url:"https://doi.org/10.5281/zenodo.16755947"
+
()
+
+
(* Reference with DOI *)
+
let ref2 = Reference.create
+
~url:"https://doi.org/10.5281/zenodo.16755947"
+
~doi:"10.5281/zenodo.16755947"
+
()
+
+
(* Reference with CiTO annotations *)
+
let ref3 = Reference.create
+
~url:"https://doi.org/10.5281/zenodo.16755947"
+
~doi:"10.5281/zenodo.16755947"
+
~cito:[`CitesAsRecommendedReading; `UsesMethodIn]
+
()
+
+
(* Reference with custom CiTO term *)
+
let ref4 = Reference.create
+
~url:"https://example.com/paper"
+
~cito:[`Other "customIntent"]
+
()
+
]} *)
+
val create :
+
url:string ->
+
?doi:string ->
+
?cito:Cito.t list ->
+
unit ->
+
t
+
+
+
(** {1 Accessors} *)
+
+
(** [url t] returns the reference's URL. *)
+
val url : t -> string
+
+
(** [doi t] returns the reference's DOI, if set. *)
+
val doi : t -> string option
+
+
(** [cito t] returns the reference's CiTO annotations, if set. *)
+
val cito : t -> Cito.t list option
+
+
+
(** {1 Comparison} *)
+
+
(** [equal a b] tests equality between two references.
+
+
References are considered equal if they have the same URL. *)
+
val equal : t -> t -> bool
+
+
+
(** {1 Pretty Printing} *)
+
+
(** [pp ppf t] pretty prints a reference to the formatter.
+
+
{b Example output:}
+
{v https://doi.org/10.5281/zenodo.16755947 [DOI: 10.5281/zenodo.16755947] v} *)
+
val pp : Format.formatter -> t -> unit