package rdfimport ()// ParseJSONLD parses a JSON-LD document into a Graph. It expands compact IRIs and// terms against an inline @context (prefix maps and term definitions, including// "@type":"@id" coercion), and handles @graph, @id, @type, @value/@language/@type// literals, @list, nested node objects, and arrays of any of these. It does not// fetch remote @context documents or run full JSON-LD 1.1 framing.func ( []byte) (*Graph, error) {varanyif := json.Unmarshal(, &); != nil {returnnil, } := &jsonldParser{g: &Graph{}, ctx: map[string]string{}, coerce: map[string]string{}, expanded: map[string]string{}}// A document is a node, an array of nodes, or an object wrapping @graph.switch d := .(type) {casemap[string]any: .loadContext(["@context"])if , := ["@graph"]; { .eachNode() } else { .node() }case []any:for , := range { .node() } }return .g, nil}type jsonldParser struct { g *Graph ctx map[string]string// prefix or term -> IRI coerce map[string]string// term -> "@id" (value is an IRI reference, not a literal) expanded map[string]string// compact IRI/term -> interned full IRI blanks int}func ( *jsonldParser) () Term { .blanks++returnNewBlank("j" + strconv.Itoa(.blanks))}// loadContext records prefix and term mappings from an inline @context. String// (remote) contexts are skipped.func ( *jsonldParser) ( any) {switch v := .(type) {case []any:for , := range { .() }casemap[string]any:for , := range {ifstrings.HasPrefix(, "@") {continue }switch t := .(type) {casestring: .ctx[] = casemap[string]any:if , := ["@id"].(string); { .ctx[] = }if , := ["@type"].(string); && == "@id" { .coerce[] = "@id" } } } }}// expand resolves a compact IRI or term to a full IRI.func ( *jsonldParser) ( string) string {if == "" || [0] == '@' {return }// Compact IRIs and terms recur heavily (every property, every type); intern the // expansion so it is built only once. The cache key is the existing JSON string, // so the lookup itself allocates nothing.if , := .expanded[]; {return } := .expandUncached() .expanded[] = return}func ( *jsonldParser) ( string) string {if , , := strings.Cut(, ":"); {if !strings.HasPrefix(, "//") {if , := .ctx[]; {return + } }return// already an absolute IRI }if , := .ctx[]; {return }return}func ( *jsonldParser) ( any) {if , := .([]any); {for , := range { .node() }return } .node()}// node emits triples for a node object and returns its subject term.func ( *jsonldParser) ( any) Term { , := .(map[string]any)if ! {returnTerm{} } := .subjectOf()if , := ["@type"]; {for , := rangeasSlice() {if , := .(string); { .g.Add(, NewIRI(TypeIRI), NewIRI(.expand())) } } }// Iterate the predicate keys in sorted order so the emitted triples (and thus // the blank-node numbering and serialized output) are deterministic across // runs, rather than following Go's randomized map order. := make([]string, 0, len())for := range {if !strings.HasPrefix(, "@") { // @id and @type handled; other keywords carry no statement here = append(, ) } }slices.Sort()for , := range { := NewIRI(.expand()) := .coerce[] == "@id"for , := range .values([], ) { .g.Add(, , ) } }return}// values turns a JSON-LD value (or array) into the object terms it denotes,// emitting triples for any nested nodes.func ( *jsonldParser) ( any, bool) []Term {switch t := .(type) {case []any:var []Termfor , := range { = append(, .(, )...) }returncasestring:if {return []Term{NewIRI(.expand())} }return []Term{NewLiteral(, "", "")}casebool:return []Term{NewLiteral(strconv.FormatBool(), "", "http://www.w3.org/2001/XMLSchema#boolean")}casefloat64:// A JSON number with no fractional part maps to xsd:integer; otherwise to // xsd:double (the JSON-LD-to-RDF number conversion).if == float64(int64()) {return []Term{NewLiteral(strconv.FormatInt(int64(), 10), "", xsdInteger)} }return []Term{NewLiteral(formatNumber(), "", xsdDouble)}casemap[string]any:return .objectValue() }returnnil}// objectValue interprets a JSON-LD object as a literal (@value), an RDF list// (@list), a bare reference (@id only), or a nested node.func ( *jsonldParser) ( map[string]any) []Term {if , := ["@value"]; { , := ["@language"].(string) , := ["@type"].(string)if != "" { = .expand() }return []Term{NewLiteral(scalarString(), , )} }if , := ["@list"]; {return []Term{.list()} }if , := ["@id"].(string); && onlyKeys(, "@id") {return []Term{NewIRI(.expand())} }return []Term{.node()}}// list materializes a JSON-LD @list as an RDF collection and returns its head// (rdf:nil for the empty list).func ( *jsonldParser) ( any) Term { := asSlice()iflen() == 0 {returnNewIRI(NilIRI) } := .fresh() := for , := range {for , := range .values(, false) { .g.Add(, NewIRI(FirstIRI), ) }if == len()-1 { .g.Add(, NewIRI(RestIRI), NewIRI(NilIRI)) } else { := .fresh() .g.Add(, NewIRI(RestIRI), ) = } }return}func ( *jsonldParser) ( map[string]any) Term {if , := ["@id"].(string); {ifstrings.HasPrefix(, "_:") {// Prefix the document label so it can never collide with a generated // ("j"+n) blank: an "@id":"_:j1" would otherwise merge with the first // anonymous node.returnNewBlank("u" + [2:]) }returnNewIRI(.expand()) }return .fresh()}func asSlice( any) []any {if , := .([]any); {return }return []any{}}func onlyKeys( map[string]any, ...string) bool {for := range {if !slices.Contains(, ) {returnfalse } }returntrue}func scalarString( any) string {switch t := .(type) {casestring:returncasebool:returnstrconv.FormatBool()casefloat64:returnformatNumber() }return""}// formatNumber renders a JSON number without a trailing ".0" for integers.func formatNumber( float64) string {if == float64(int64()) {returnstrconv.FormatInt(int64(), 10) }returnstrconv.FormatFloat(, 'g', -1, 64)}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.