package rdfimport ()// SyntaxError reports a line that is neither blank, nor a comment, nor a valid// N-Triples/N-Quads statement. Line is 1-based; Text is the offending line,// trimmed and truncated for the message.//// It exists so that a truncated document is an error rather than a smaller graph.// A parser that skips what it cannot read turns a half-written dump into a// well-formed, wrong answer, and no caller downstream can tell.//// Line counts from the start of the input actually handed to the parser, which is// not the document when a caller feeds it one chunk at a time: a bad line in the// third 1MB chunk of a large dump is reported at its offset within that chunk, and// the number looks entirely plausible. Parse a chunked document through// [NewDecoder] instead -- it reads across an [io.Reader] and numbers lines// continuously -- or add the chunk's line base to Line before showing it to// anyone.typeSyntaxErrorstruct { Line int// 1-based, relative to the input given to the parser; see the type doc Text string// the offending line, trimmed}func ( *SyntaxError) () string { := .Textiflen() > 64 { = [:61] + "..." }return"rdf: line " + strconv.Itoa(.Line) + ": malformed N-Triples/N-Quads statement: " + strconv.Quote()}// lineKind classifies one line of an N-Triples/N-Quads document. Blank and// comment lines carry no statement but are not errors; a malformed line is.// Collapsing the two into one "not a statement" bool is what let a truncated// document parse clean.type lineKind uint8const ( lineStatement lineKind = iota// a well-formed statement lineIgnorable // blank or comment: no statement, no error lineMalformed // not parseable as a statement)// ParseNTriples parses an N-Triples document (one "subject predicate object ."// statement per line) into a Graph. It also accepts N-Quads, ignoring any fourth// (graph) term. Blank and comment lines are skipped; a malformed line is a// *SyntaxError naming the line number, so a truncated document does not parse as// a smaller graph. One private copy of the input backs every term, so data is// free for reuse once it returns.//// Use [NewDecoder] with [Decoder.SkipMalformed] to tolerate the trailing noise// some real-world dumps carry.func ( []byte) (*Graph, error) {returnparseNTriples(string())}// ParseNTriplesShared is ParseNTriples without the private input copy: terms// are zero-copy views into data itself, saving one input-sized allocation. In// exchange the caller must not modify data while the Graph, or any Term drawn// from it, remains in use.func ( []byte) (*Graph, error) {returnparseNTriples(bytesView())}func parseNTriples( string) (*Graph, error) {// Zero-copy substrings of data back every term. Preallocate the triple slice // at full length from the line count and fill each statement in place through // a write index, so the 168-byte Triple is assembled directly in the slice // rather than returned and appended by value -- that struct copy (runtime. // duffcopy) otherwise dominates parse CPU. Ignorable/malformed lines leave the // index where it is; the tail of untouched slots is truncated at the end. := &Graph{Triples: make([]Triple, strings.Count(, "\n")+1)}vararena , := 0, 0for := rangestrings.SplitSeq(, "\n") { ++switchparseNTLineInto(, &, &.Triples[]) {caselineStatement: ++caselineMalformed: .Triples = .Triples[:]return , &SyntaxError{Line: , Text: strings.TrimSpace()} } } .Triples = .Triples[:]return , nil}// parseNTLineInto parses one N-Triples/N-Quads line, writing subject, predicate// and object directly into t (dropping any fourth, graph, term) so the Triple is// never copied by value. It mirrors parseNQuadLine's leniency: a trailing graph// term or terminator is not required, and content past the object is ignored.func parseNTLineInto( string, *arena, *Triple) lineKind { := strings.TrimSpace()if == "" || [0] == '#' {returnlineIgnorable }varboolif .S, , = readNTTerm(, ); ! || .S.IsLiteral() { // a literal subject is not valid RDFreturnlineMalformed }if .P, , = readNTTerm(strings.TrimLeft(, " \t"), ); ! || !.P.IsIRI() {returnlineMalformed }if .O, _, = readNTTerm(strings.TrimLeft(, " \t"), ); ! {returnlineMalformed }returnlineStatement}// parseNQuadLine parses one N-Triples/N-Quads line into a quad, keeping the// optional fourth (graph) term; a three-term line falls in the default graph// (zero-value G).func parseNQuadLine( string, *arena) (Quad, lineKind) { := strings.TrimSpace()if == "" || [0] == '#' {returnQuad{}, lineIgnorable } , , := readNTTerm(, )if ! || .IsLiteral() { // a literal subject is not valid RDFreturnQuad{}, lineMalformed } , , := readNTTerm(strings.TrimLeft(, " \t"), )if ! || !.IsIRI() {returnQuad{}, lineMalformed } , , := readNTTerm(strings.TrimLeft(, " \t"), )if ! {returnQuad{}, lineMalformed }// The optional graph label is an IRI or blank node before the terminating // '.'; a literal there is not a valid graph name and is ignored.varTermif := strings.TrimLeft(, " \t"); len() > 0 && [0] != '.' {if , , := readNTTerm(, ); && !.IsLiteral() { = } }returnQuad{, , , }, lineStatement}// readNTTerm reads one term from the front of s, returning the term and the// remaining string.func readNTTerm( string, *arena) (Term, string, bool) {switch {casestrings.HasPrefix(, "<"): := strings.IndexByte(, '>')if < 0 {returnTerm{}, , false }returnNewIRI(unescapeRDF([1:])), [+1:], truecasestrings.HasPrefix(, "_:"): := strings.IndexAny(, " \t")if < 0 { = len() }// A trailing "." is the statement terminator, not part of the label // (a label may contain "." but not end with one), so strip it. := for > 2 && [-1] == '.' { -- }returnNewBlank([2:]), [:], truecasestrings.HasPrefix(, `"`):returnreadNTLiteral(, ) }returnTerm{}, , false}// readNTLiteral reads a quoted literal and any ^^<datatype> or @lang suffix. The// lexical form is a zero-copy slice of the input when it has no escapes, and is// unescaped only when needed.func readNTLiteral( string, *arena) (Term, string, bool) { := false := 1for < len() {switch [] {case'\\': = true += 2// skip the escaped character so an escaped quote is not the closecase'"': := [1:]if {if != nil { = .unescape() // arena-backed (whole-document parse) } else { = unescapeRDF() // streaming: each triple owns its strings } }returnntLiteralSuffix(, [+1:])default: ++ } }returnTerm{}, , false// unterminated}// ntLiteralSuffix attaches a ^^<datatype> or @lang suffix to a literal value.func ntLiteralSuffix(, string) (Term, string, bool) {switch {casestrings.HasPrefix(, "^^<"): := strings.IndexByte(, '>')if < 0 {returnTerm{}, , false }returnNewLiteral(, "", unescapeRDF([3:])), [+1:], truecasestrings.HasPrefix(, "@"): := langTagLen([1:])returnNewLiteral(, [1:1+], ""), [1+:], true }returnNewLiteral(, "", ""), , true}// langTagLen returns the length of a well-formed language tag at the start of s// (the text after the '@'): [a-zA-Z]+ ('-' [a-zA-Z0-9]+)*. It returns 0 when s does// not begin with a letter, so an invalid tag like "0" yields no language rather// than a value the Turtle grammar would reject on a round trip.func langTagLen( string) int { := 0for < len() && isAlpha([]) { ++ }if == 0 {return0 }for < len() && [] == '-' { := + 1for < len() && (isAlpha([]) || ([] >= '0' && [] <= '9')) { ++ }if == +1 { // a '-' must be followed by at least one alphanumericbreak } = }return}// NTriples serializes the graph as N-Triples.func ( *Graph) () []byte {varEncoderreturn .AppendNTriples(nil, )}// appendNTTerm writes a term in N-Triples syntax.func appendNTTerm( []byte, Term, *blankNamer) []byte {switch .Kind {caseIRI: = append(, '<') = appendEscapedIRI(, .Value)returnappend(, '>')caseBlank: = append(, '_', ':')returnappend(, .name(.Value)...)default: = append(, '"') = appendEscapedLiteral(, .Value) = append(, '"')if .Lang != "" { = append(, '@')returnappend(, .Lang...) }if .Datatype != "" && .Datatype != XSDString { = append(, "^^<"...) = appendEscapedIRI(, .Datatype)returnappend(, '>') }return }}// ---- shared escaping ----// iriNeedsEscape[c] reports whether byte c must be \u-escaped in an IRI// reference — the ASCII controls and space plus the delimiters IRIs forbid bare.// A table lookup replaces a per-byte chain of comparisons on the write path.var iriNeedsEscape = func() ( [256]bool) {for := 0; <= 0x20; ++ { [] = true }for , := range []byte{'<', '>', '"', '{', '}', '|', '^', '`', '\\'} { [] = true }return}()// appendEscapedIRI escapes the characters not allowed bare in an IRI reference.// Clean runs — the overwhelming common case, since real IRIs need no escaping —// are bulk-copied in one append rather than byte by byte.func appendEscapedIRI( []byte, string) []byte { := 0for := 0; < len(); ++ {ifiriNeedsEscape[[]] { = append(, [:]...) = appendUnicodeEscape(, rune([])) = + 1 } }returnappend(, [:]...)}// literalEscape parameterizes appendLiteralEscaped for the two output profiles// that share its body. The N-Triples/Turtle serializer uses the zero value; the// canonical N-Quads form additionally emits \b and \f as named escapes and escapes// U+007F, as RDFC-1.0 requires.type literalEscape struct { namedBF bool// emit \b and \f as named escapes (else the \u00XX control rule) escapeDEL bool// escape U+007F (DEL) as \u007F rather than passing it through}// appendLiteralEscaped escapes a literal's lexical form per cfg: always the named// escapes \\ \" \n \r \t and \uXXXX for C0 controls, with \b \f and U+007F handled// per cfg; valid UTF-8 passes through and a lone invalid byte is dropped. It is the// single escaper both the N-Triples serializer and the canonicalizer call, so the// two cannot drift.func appendLiteralEscaped( []byte, string, literalEscape) []byte {for := 0; < len(); { := []if < 0x80 {switch {case == '\\': = append(, '\\', '\\')case == '"': = append(, '\\', '"')case == '\n': = append(, '\\', 'n')case == '\r': = append(, '\\', 'r')case == '\t': = append(, '\\', 't')case == '\b' && .namedBF: = append(, '\\', 'b')case == '\f' && .namedBF: = append(, '\\', 'f')case < 0x20 || ( == 0x7f && .escapeDEL): = appendUnicodeEscape(, rune())default: = append(, ) } ++continue } , := utf8.DecodeRuneInString([:])if == utf8.RuneError && == 1 { ++continue } = append(, [:+]...) += }return}// appendEscapedLiteral escapes a literal's lexical form for Turtle/N-Triples.func appendEscapedLiteral( []byte, string) []byte {returnappendLiteralEscaped(, , literalEscape{})}// blankNamer maps blank-node labels to fresh, always-valid identifiers (b1, b2, …)// during one serialization. The mapping is injective, so two distinct blank nodes// never collapse to the same output label — which a character-sanitizing scheme// could (e.g. "!x" and "?x" both becoming "_x"), silently merging nodes.type blankNamer struct { m map[string]string n int}func ( *blankNamer) ( string) string {if , := .m[]; {return }if .m == nil { .m = make(map[string]string) } .n++ := "b" + strconv.Itoa(.n) .m[] = return}// newScope drops the label→name mappings while keeping the running counter, so// the next labels begin a fresh blank-node scope yet still receive globally// unique output names. It lets independently-labeled graphs (e.g. one per// record) serialize into a single N-Quads document without their blank nodes// merging.func ( *blankNamer) () { .m = nil }func appendUnicodeEscape( []byte, rune) []byte {const = "0123456789ABCDEF"returnappend(, '\\', 'u', [(>>12)&0xf], [(>>8)&0xf], [(>>4)&0xf], [&0xf])}// unescapeRDF unescapes \uXXXX, \UXXXXXXXX and the string escapes in an IRI or// literal lexical form.func unescapeRDF( string) string {if !strings.ContainsRune(, '\\') {return }varstrings.Builder .Grow(len()) // the unescaped form is never longer, so one allocation sufficesfor := 0; < len(); {if [] == '\\' && +1 < len() { , := unescapeRune([:]) .WriteRune() += continue } .WriteByte([]) ++ }return .String()}// unescapeRune decodes one backslash escape at the front of s, returning the rune// and the number of bytes consumed.func unescapeRune( string) (rune, int) {iflen() < 2 {return'\\', 1 }switch [1] {case'n':return'\n', 2case'r':return'\r', 2case't':return'\t', 2case'b':return'\b', 2case'f':return'\f', 2case'"':return'"', 2case'\'':return'\'', 2case'\\':return'\\', 2case'u':iflen() >= 6 {if , := parseHex([2:6]); {return , 6 } }case'U':iflen() >= 10 {if , := parseHex([2:10]); {return , 10 } } }returnrune([1]), 2}func parseHex( string) (rune, bool) {varrunefor := 0; < len(); ++ { := []switch {case >= '0' && <= '9': = <<4 | rune(-'0')case >= 'a' && <= 'f': = <<4 | rune(-'a'+10)case >= 'A' && <= 'F': = <<4 | rune(-'A'+10)default:return0, false } }return , true}
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.