OCaml library for JSONfeed parsing and creation

add support for the _references extension

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

+266 -3
+1 -1
dune-project
··· 1 - (lang dune 3.20) 1 + (lang dune 3.18) 2 2 3 3 (name jsonfeed) 4 4
+151
lib/cito.mli
··· 1 + (** Citation Typing Ontology (CiTO) intent annotations. 2 + 3 + CiTO provides a structured vocabulary for describing the nature of citations. 4 + This module implements support for CiTO annotations as used in the references extension. 5 + 6 + @see <https://purl.archive.org/spar/cito> Citation Typing Ontology 7 + @see <https://sparontologies.github.io/cito/current/cito.html> CiTO Specification *) 8 + 9 + 10 + (** CiTO citation intent annotation. 11 + 12 + Represents the intent or nature of a citation using the Citation Typing Ontology. 13 + Each variant corresponds to a specific CiTO property. The [`Other] variant allows 14 + for custom or future CiTO terms not yet included in this library. 15 + 16 + {b Categories:} 17 + - Factual: Citing for data, methods, evidence, or information 18 + - Critical: Agreement, disagreement, correction, or qualification 19 + - Rhetorical: Style-based citations (parody, ridicule, etc.) 20 + - Relational: Document relationships and compilations 21 + - Support: Providing or obtaining backing and context 22 + - Exploratory: Speculation and recommendations 23 + - Quotation: Direct quotes and excerpts 24 + - Dialogue: Replies and responses 25 + - Sharing: Common attributes between works *) 26 + type t = [ 27 + | `Cites (** The base citation property *) 28 + 29 + (* Factual citation intents *) 30 + | `CitesAsAuthority (** Cites as authoritative source *) 31 + | `CitesAsDataSource (** Cites as origin of data *) 32 + | `CitesAsEvidence (** Cites for factual evidence *) 33 + | `CitesForInformation (** Cites as information source *) 34 + | `UsesDataFrom (** Uses data from cited work *) 35 + | `UsesMethodIn (** Uses methodology from cited work *) 36 + | `UsesConclusionsFrom (** Applies conclusions from cited work *) 37 + 38 + (* Agreement/disagreement *) 39 + | `AgreesWith (** Concurs with cited statements *) 40 + | `DisagreesWith (** Rejects cited statements *) 41 + | `Confirms (** Validates facts in cited work *) 42 + | `Refutes (** Disproves cited statements *) 43 + | `Disputes (** Contests without definitive refutation *) 44 + 45 + (* Critical engagement *) 46 + | `Critiques (** Analyzes and finds fault *) 47 + | `Qualifies (** Places conditions on statements *) 48 + | `Corrects (** Fixes errors in cited work *) 49 + | `Updates (** Advances understanding beyond cited work *) 50 + | `Extends (** Builds upon cited facts *) 51 + 52 + (* Rhetorical/stylistic *) 53 + | `Parodies (** Imitates for comic effect *) 54 + | `Plagiarizes (** Uses without acknowledgment *) 55 + | `Derides (** Expresses contempt *) 56 + | `Ridicules (** Mocks cited work *) 57 + 58 + (* Document relationships *) 59 + | `Describes (** Characterizes cited entity *) 60 + | `Documents (** Records information about source *) 61 + | `CitesAsSourceDocument (** Cites as foundational source *) 62 + | `CitesAsMetadataDocument (** Cites containing metadata *) 63 + | `Compiles (** Uses to create new work *) 64 + | `Reviews (** Examines cited statements *) 65 + | `Retracts (** Formally withdraws *) 66 + 67 + (* Support/context *) 68 + | `Supports (** Provides intellectual backing *) 69 + | `GivesSupportTo (** Provides support to citing entity *) 70 + | `ObtainsSupportFrom (** Obtains backing from cited work *) 71 + | `GivesBackgroundTo (** Provides context *) 72 + | `ObtainsBackgroundFrom (** Obtains context from cited work *) 73 + 74 + (* Exploratory *) 75 + | `SpeculatesOn (** Theorizes without firm evidence *) 76 + | `CitesAsPotentialSolution (** Offers possible resolution *) 77 + | `CitesAsRecommendedReading (** Suggests as further reading *) 78 + | `CitesAsRelated (** Identifies as thematically connected *) 79 + 80 + (* Quotation/excerpting *) 81 + | `IncludesQuotationFrom (** Incorporates direct quotes *) 82 + | `IncludesExcerptFrom (** Uses non-quoted passages *) 83 + 84 + (* Dialogue *) 85 + | `RepliesTo (** Responds to cited statements *) 86 + | `HasReplyFrom (** Evokes response *) 87 + 88 + (* Linking *) 89 + | `LinksTo (** Provides URL hyperlink *) 90 + 91 + (* Shared attribution *) 92 + | `SharesAuthorWith (** Common authorship *) 93 + | `SharesJournalWith (** Published in same journal *) 94 + | `SharesPublicationVenueWith (** Published in same venue *) 95 + | `SharesFundingAgencyWith (** Funded by same agency *) 96 + | `SharesAuthorInstitutionWith (** Authors share affiliation *) 97 + 98 + (* Extensibility *) 99 + | `Other of string (** Custom or future CiTO term *) 100 + ] 101 + 102 + 103 + (** {1 Conversion} *) 104 + 105 + (** [of_string s] converts a CiTO term string to its variant representation. 106 + 107 + Recognized CiTO terms are converted to their corresponding variants. 108 + Unrecognized terms are wrapped in [`Other]. 109 + 110 + The comparison is case-insensitive for standard CiTO terms but preserves 111 + the original case in [`Other] variants. 112 + 113 + {b Examples:} 114 + {[ 115 + of_string "cites" (* returns `Cites *) 116 + of_string "usesMethodIn" (* returns `UsesMethodIn *) 117 + of_string "citesAsRecommendedReading" (* returns `CitesAsRecommendedReading *) 118 + of_string "customTerm" (* returns `Other "customTerm" *) 119 + ]} *) 120 + val of_string : string -> t 121 + 122 + (** [to_string t] converts a CiTO variant to its canonical string representation. 123 + 124 + Standard CiTO terms use their official CiTO local names (camelCase). 125 + [`Other] variants return the wrapped string unchanged. 126 + 127 + {b Examples:} 128 + {[ 129 + to_string `Cites (* returns "cites" *) 130 + to_string `UsesMethodIn (* returns "usesMethodIn" *) 131 + to_string (`Other "customTerm") (* returns "customTerm" *) 132 + ]} *) 133 + val to_string : t -> string 134 + 135 + 136 + (** {1 Comparison} *) 137 + 138 + (** [equal a b] tests equality between two CiTO annotations. 139 + 140 + Two annotations are equal if they represent the same CiTO term. 141 + For [`Other] variants, string comparison is case-sensitive. *) 142 + val equal : t -> t -> bool 143 + 144 + 145 + (** {1 Pretty Printing} *) 146 + 147 + (** [pp ppf t] pretty prints a CiTO annotation to the formatter. 148 + 149 + {b Example output:} 150 + {v citesAsRecommendedReading v} *) 151 + val pp : Format.formatter -> t -> unit
+2 -1
lib/dune
··· 1 1 (library 2 2 (name jsonfeed) 3 3 (public_name jsonfeed) 4 - (libraries jsonm ptime fmt)) 4 + (libraries jsonm ptime fmt) 5 + (modules_without_implementation cito reference))
+4 -1
lib/item.ml
··· 21 21 tags : string list option; 22 22 language : string option; 23 23 attachments : Attachment.t list option; 24 + references : Reference.t list option; 24 25 } 25 26 26 27 let create ~id ~content ?url ?external_url ?title ?summary ?image ?banner_image 27 - ?date_published ?date_modified ?authors ?tags ?language ?attachments () = 28 + ?date_published ?date_modified ?authors ?tags ?language ?attachments ?references () = 28 29 { 29 30 id; 30 31 content; ··· 40 41 tags; 41 42 language; 42 43 attachments; 44 + references; 43 45 } 44 46 45 47 let id t = t.id ··· 56 58 let tags t = t.tags 57 59 let language t = t.language 58 60 let attachments t = t.attachments 61 + let references t = t.references 59 62 60 63 let content_html t = 61 64 match t.content with
+17
lib/item.mli
··· 44 44 @param tags Plain text tags/categories for the item 45 45 @param language Primary language of the item (RFC 5646 format, e.g. ["en-US"]) 46 46 @param attachments Related resources like audio files or downloads 47 + @param references References to cited sources (extension) 47 48 48 49 {b Examples:} 49 50 {[ ··· 77 78 ~content:(`Html "<p>Episode description</p>") 78 79 ~title:"Episode 1" 79 80 ~attachments:[attachment] () 81 + 82 + (* Article with references *) 83 + let reference = Reference.create 84 + ~url:"https://doi.org/10.5281/zenodo.16755947" 85 + ~doi:"10.5281/zenodo.16755947" 86 + ~cito:[`CitesAsRecommendedReading; `UsesMethodIn] () in 87 + let item = Item.create 88 + ~id:"https://doi.org/10.59350/krw9n-dv417" 89 + ~content:(`Html "<p>Research article content</p>") 90 + ~title:"One Million IUPAC names #4: a lot is happening" 91 + ~url:"https://chem-bla-ics.linkedchemistry.info/2025/08/09/one-million-iupac-names-4.html" 92 + ~references:[reference] () 80 93 ]} *) 81 94 val create : 82 95 id:string -> ··· 93 106 ?tags:string list -> 94 107 ?language:string -> 95 108 ?attachments:Attachment.t list -> 109 + ?references:Reference.t list -> 96 110 unit -> 97 111 t 98 112 ··· 140 154 141 155 (** [attachments t] returns the item's attachments, if set. *) 142 156 val attachments : t -> Attachment.t list option 157 + 158 + (** [references t] returns the item's references, if set. *) 159 + val references : t -> Reference.t list option 143 160 144 161 145 162 (** {1 Content Helpers} *)
+2
lib/jsonfeed.ml
··· 6 6 module Attachment = Attachment 7 7 module Hub = Hub 8 8 module Item = Item 9 + module Reference = Reference 10 + module Cito = Cito 9 11 10 12 type t = { 11 13 version : string;
+6
lib/jsonfeed.mli
··· 374 374 375 375 (** Feed items (posts, episodes, entries). *) 376 376 module Item = Item 377 + 378 + (** References to cited sources in items (extension). *) 379 + module Reference = Reference 380 + 381 + (** Citation Typing Ontology annotations for references (extension). *) 382 + module Cito = Cito
+83
lib/reference.mli
··· 1 + (** References extension for JSON Feed items. 2 + 3 + This implements the references extension that allows items to cite sources. 4 + Each reference represents a cited resource with optional DOI and CiTO annotations. 5 + 6 + @see <https://github.com/egonw/JSONFeed-extensions/blob/main/references.md> References Extension Specification 7 + @see <https://purl.archive.org/spar/cito> Citation Typing Ontology *) 8 + 9 + 10 + (** The type representing a reference to a cited source. *) 11 + type t 12 + 13 + 14 + (** {1 Construction} *) 15 + 16 + (** [create ~url ?doi ?cito ()] creates a reference. 17 + 18 + @param url Unique URL for the reference (required). 19 + A URL based on a persistent unique identifier (like DOI) is recommended. 20 + @param doi Digital Object Identifier for the reference 21 + @param cito Citation Typing Ontology intent annotations 22 + 23 + {b Examples:} 24 + {[ 25 + (* Simple reference with just a URL *) 26 + let ref1 = Reference.create 27 + ~url:"https://doi.org/10.5281/zenodo.16755947" 28 + () 29 + 30 + (* Reference with DOI *) 31 + let ref2 = Reference.create 32 + ~url:"https://doi.org/10.5281/zenodo.16755947" 33 + ~doi:"10.5281/zenodo.16755947" 34 + () 35 + 36 + (* Reference with CiTO annotations *) 37 + let ref3 = Reference.create 38 + ~url:"https://doi.org/10.5281/zenodo.16755947" 39 + ~doi:"10.5281/zenodo.16755947" 40 + ~cito:[`CitesAsRecommendedReading; `UsesMethodIn] 41 + () 42 + 43 + (* Reference with custom CiTO term *) 44 + let ref4 = Reference.create 45 + ~url:"https://example.com/paper" 46 + ~cito:[`Other "customIntent"] 47 + () 48 + ]} *) 49 + val create : 50 + url:string -> 51 + ?doi:string -> 52 + ?cito:Cito.t list -> 53 + unit -> 54 + t 55 + 56 + 57 + (** {1 Accessors} *) 58 + 59 + (** [url t] returns the reference's URL. *) 60 + val url : t -> string 61 + 62 + (** [doi t] returns the reference's DOI, if set. *) 63 + val doi : t -> string option 64 + 65 + (** [cito t] returns the reference's CiTO annotations, if set. *) 66 + val cito : t -> Cito.t list option 67 + 68 + 69 + (** {1 Comparison} *) 70 + 71 + (** [equal a b] tests equality between two references. 72 + 73 + References are considered equal if they have the same URL. *) 74 + val equal : t -> t -> bool 75 + 76 + 77 + (** {1 Pretty Printing} *) 78 + 79 + (** [pp ppf t] pretty prints a reference to the formatter. 80 + 81 + {b Example output:} 82 + {v https://doi.org/10.5281/zenodo.16755947 [DOI: 10.5281/zenodo.16755947] v} *) 83 + val pp : Format.formatter -> t -> unit