Merge pull request #3107 from ktsiligkiris/documentation/fix_xml_docs

Fix comments for proper rendering in documentation in encoding/xml
This commit is contained in:
Jeroen van Rijn
2024-01-17 21:10:49 +01:00
committed by GitHub
5 changed files with 85 additions and 156 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
package xml
/* /*
An XML 1.0 / 1.1 parser An XML 1.0 / 1.1 parser
@@ -9,7 +11,7 @@
List of contributors: List of contributors:
Jeroen van Rijn: Initial implementation. Jeroen van Rijn: Initial implementation.
*/ */
package xml
import "core:io" import "core:io"
import "core:fmt" import "core:fmt"
+1 -1
View File
@@ -20,7 +20,7 @@ example :: proc() {
xml.destroy(docs[round]) xml.destroy(docs[round])
} }
DOC :: #load("../../../../tests/core/assets/XML/unicode.xml") DOC :: #load("../../../../tests/core/assets/XML/utf8.xml")
input := DOC input := DOC
for round in 0..<N { for round in 0..<N {
+3 -1
View File
@@ -1,3 +1,5 @@
package xml
/* /*
An XML 1.0 / 1.1 parser An XML 1.0 / 1.1 parser
@@ -6,7 +8,7 @@
This file contains helper functions. This file contains helper functions.
*/ */
package xml
// Find parent's nth child with a given ident. // Find parent's nth child with a given ident.
find_child_by_ident :: proc(doc: ^Document, parent_id: Element_ID, ident: string, nth := 0) -> (res: Element_ID, found: bool) { find_child_by_ident :: proc(doc: ^Document, parent_id: Element_ID, ident: string, nth := 0) -> (res: Element_ID, found: bool) {
+3 -1
View File
@@ -1,3 +1,5 @@
package xml
/* /*
An XML 1.0 / 1.1 parser An XML 1.0 / 1.1 parser
@@ -9,7 +11,7 @@
List of contributors: List of contributors:
Jeroen van Rijn: Initial implementation. Jeroen van Rijn: Initial implementation.
*/ */
package xml
import "core:fmt" import "core:fmt"
import "core:unicode" import "core:unicode"
+65 -142
View File
@@ -1,28 +1,28 @@
/* /*
An XML 1.0 / 1.1 parser XML 1.0 / 1.1 parser
Copyright 2021-2022 Jeroen van Rijn <nom@duclavier.com>. 2021-2022 Jeroen van Rijn <nom@duclavier.com>.
Made available under Odin's BSD-3 license. available under Odin's BSD-3 license.
A from-scratch XML implementation, loosely modelled on the [spec](https://www.w3.org/TR/2006/REC-xml11-20060816). from-scratch XML implementation, loosely modelled on the [spec](https://www.w3.org/TR/2006/REC-xml11-20060816).
Features: Features:
- Supports enough of the XML 1.0/1.1 spec to handle the 99.9% of XML documents in common current usage. - Supports enough of the XML 1.0/1.1 spec to handle the 99.9% of XML documents in common current usage.
- Simple to understand and use. Small. - Simple to understand and use. Small.
Caveats: Caveats:
- We do NOT support HTML in this package, as that may or may not be valid XML. - We do NOT support HTML in this package, as that may or may not be valid XML.
If it works, great. If it doesn't, that's not considered a bug. If it works, great. If it doesn't, that's not considered a bug.
- We do NOT support UTF-16. If you have a UTF-16 XML file, please convert it to UTF-8 first. Also, our condolences. - We do NOT support UTF-16. If you have a UTF-16 XML file, please convert it to UTF-8 first. Also, our condolences.
- <[!ELEMENT and <[!ATTLIST are not supported, and will be either ignored or return an error depending on the parser options. - <[!ELEMENT and <[!ATTLIST are not supported, and will be either ignored or return an error depending on the parser options.
MAYBE: MAYBE:
- XML writer? - XML writer?
- Serialize/deserialize Odin types? - Serialize/deserialize Odin types?
List of contributors: List of contributors:
Jeroen van Rijn: Initial implementation. - Jeroen van Rijn: Initial implementation.
*/ */
package xml package xml
// An XML 1.0 / 1.1 parser // An XML 1.0 / 1.1 parser
@@ -43,48 +43,32 @@ DEFAULT_OPTIONS :: Options{
} }
Option_Flag :: enum { Option_Flag :: enum {
/* // If the caller says that input may be modified, we can perform in-situ parsing.
If the caller says that input may be modified, we can perform in-situ parsing. // If this flag isn't provided, the XML parser first duplicates the input so that it can.
If this flag isn't provided, the XML parser first duplicates the input so that it can.
*/
Input_May_Be_Modified, Input_May_Be_Modified,
/* // Document MUST start with `<?xml` prologue.
Document MUST start with `<?xml` prologue.
*/
Must_Have_Prolog, Must_Have_Prolog,
/* // Document MUST have a `<!DOCTYPE`.
Document MUST have a `<!DOCTYPE`.
*/
Must_Have_DocType, Must_Have_DocType,
/* // By default we skip comments. Use this option to intern a comment on a parented Element.
By default we skip comments. Use this option to intern a comment on a parented Element.
*/
Intern_Comments, Intern_Comments,
/* // How to handle unsupported parts of the specification, like <! other than <!DOCTYPE and <![CDATA[
How to handle unsupported parts of the specification, like <! other than <!DOCTYPE and <![CDATA[
*/
Error_on_Unsupported, Error_on_Unsupported,
Ignore_Unsupported, Ignore_Unsupported,
/* // By default CDATA tags are passed-through as-is.
By default CDATA tags are passed-through as-is. // This option unwraps them when encountered.
This option unwraps them when encountered.
*/
Unbox_CDATA, Unbox_CDATA,
/* // By default SGML entities like `&gt;`, `&#32;` and `&#x20;` are passed-through as-is.
By default SGML entities like `&gt;`, `&#32;` and `&#x20;` are passed-through as-is. // This option decodes them when encountered.
This option decodes them when encountered.
*/
Decode_SGML_Entities, Decode_SGML_Entities,
/* // If a tag body has a comment, it will be stripped unless this option is given.
If a tag body has a comment, it will be stripped unless this option is given.
*/
Keep_Tag_Body_Comments, Keep_Tag_Body_Comments,
} }
Option_Flags :: bit_set[Option_Flag; u16] Option_Flags :: bit_set[Option_Flag; u16]
@@ -97,28 +81,20 @@ Document :: struct {
encoding: Encoding, encoding: Encoding,
doctype: struct { doctype: struct {
/* // We only scan the <!DOCTYPE IDENT part and skip the rest.
We only scan the <!DOCTYPE IDENT part and skip the rest.
*/
ident: string, ident: string,
rest: string, rest: string,
}, },
/* // If we encounter comments before the root node, and the option to intern comments is given, this is where they'll live.
If we encounter comments before the root node, and the option to intern comments is given, this is where they'll live. // Otherwise they'll be in the element tree.
Otherwise they'll be in the element tree.
*/
comments: [dynamic]string, comments: [dynamic]string,
/* // Internal
Internal
*/
tokenizer: ^Tokenizer, tokenizer: ^Tokenizer,
allocator: mem.Allocator, allocator: mem.Allocator,
/* // Input. Either the original buffer, or a copy if `.Input_May_Be_Modified` isn't specified.
Input. Either the original buffer, or a copy if `.Input_May_Be_Modified` isn't specified.
*/
input: []u8, input: []u8,
strings_to_free: [dynamic]string, strings_to_free: [dynamic]string,
} }
@@ -158,34 +134,24 @@ Encoding :: enum {
UTF_8, UTF_8,
ISO_8859_1, ISO_8859_1,
/* // Aliases
Aliases
*/
LATIN_1 = ISO_8859_1, LATIN_1 = ISO_8859_1,
} }
Error :: enum { Error :: enum {
/* // General return values.
General return values.
*/
None = 0, None = 0,
General_Error, General_Error,
Unexpected_Token, Unexpected_Token,
Invalid_Token, Invalid_Token,
/* // Couldn't find, open or read file.
Couldn't find, open or read file.
*/
File_Error, File_Error,
/* // File too short.
File too short.
*/
Premature_EOF, Premature_EOF,
/* // XML-specific errors.
XML-specific errors.
*/
No_Prolog, No_Prolog,
Invalid_Prolog, Invalid_Prolog,
Too_Many_Prologs, Too_Many_Prologs,
@@ -194,11 +160,9 @@ Error :: enum {
Too_Many_DocTypes, Too_Many_DocTypes,
DocType_Must_Preceed_Elements, DocType_Must_Preceed_Elements,
/* // If a DOCTYPE is present _or_ the caller
If a DOCTYPE is present _or_ the caller // asked for a specific DOCTYPE and the DOCTYPE
asked for a specific DOCTYPE and the DOCTYPE // and root tag don't match, we return `.Invalid_DocType`.
and root tag don't match, we return `.Invalid_DocType`.
*/
Invalid_DocType, Invalid_DocType,
Invalid_Tag_Value, Invalid_Tag_Value,
@@ -211,27 +175,20 @@ Error :: enum {
Unsupported_Version, Unsupported_Version,
Unsupported_Encoding, Unsupported_Encoding,
/* // <!FOO are usually skipped.
<!FOO are usually skipped.
*/
Unhandled_Bang, Unhandled_Bang,
Duplicate_Attribute, Duplicate_Attribute,
Conflicting_Options, Conflicting_Options,
} }
/*
Implementation starts here.
*/
parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_handler := default_error_handler, allocator := context.allocator) -> (doc: ^Document, err: Error) { parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_handler := default_error_handler, allocator := context.allocator) -> (doc: ^Document, err: Error) {
data := data data := data
context.allocator = allocator context.allocator = allocator
opts := validate_options(options) or_return opts := validate_options(options) or_return
/* // If `.Input_May_Be_Modified` is not specified, we duplicate the input so that we can modify it in-place.
If `.Input_May_Be_Modified` is not specified, we duplicate the input so that we can modify it in-place.
*/
if .Input_May_Be_Modified not_in opts.flags { if .Input_May_Be_Modified not_in opts.flags {
data = bytes.clone(data) data = bytes.clone(data)
} }
@@ -252,10 +209,8 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha
element, parent: Element_ID element, parent: Element_ID
open: Token open: Token
/* // If a DOCTYPE is present, the root tag has to match.
If a DOCTYPE is present, the root tag has to match. // If an expected DOCTYPE is given in options (i.e. it's non-empty), the DOCTYPE (if present) and root tag have to match.
If an expected DOCTYPE is given in options (i.e. it's non-empty), the DOCTYPE (if present) and root tag have to match.
*/
expected_doctype := options.expected_doctype expected_doctype := options.expected_doctype
loop: for { loop: for {
@@ -263,17 +218,13 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha
// NOTE(Jeroen): This is faster as a switch. // NOTE(Jeroen): This is faster as a switch.
switch t.ch { switch t.ch {
case '<': case '<':
/* // Consume peeked `<`
Consume peeked `<`
*/
advance_rune(t) advance_rune(t)
open = scan(t) open = scan(t)
// NOTE(Jeroen): We're not using a switch because this if-else chain ordered by likelihood is 2.5% faster at -o:size and -o:speed. // NOTE(Jeroen): We're not using a switch because this if-else chain ordered by likelihood is 2.5% faster at -o:size and -o:speed.
if likely(open.kind, Token_Kind.Ident) == .Ident { if likely(open.kind, Token_Kind.Ident) == .Ident {
/* // e.g. <odin - Start of new element.
e.g. <odin - Start of new element.
*/
element = new_element(doc) element = new_element(doc)
if element == 0 { // First Element if element == 0 { // First Element
parent = element parent = element
@@ -286,11 +237,9 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha
parse_attributes(doc, &doc.elements[element].attribs) or_return parse_attributes(doc, &doc.elements[element].attribs) or_return
/* // If a DOCTYPE is present _or_ the caller
If a DOCTYPE is present _or_ the caller // asked for a specific DOCTYPE and the DOCTYPE
asked for a specific DOCTYPE and the DOCTYPE // and root tag don't match, we return .Invalid_Root_Tag.
and root tag don't match, we return .Invalid_Root_Tag.
*/
if element == 0 { // Root tag? if element == 0 { // Root tag?
if len(expected_doctype) > 0 && expected_doctype != open.text { if len(expected_doctype) > 0 && expected_doctype != open.text {
error(t, t.offset, "Root Tag doesn't match DOCTYPE. Expected: %v, got: %v\n", expected_doctype, open.text) error(t, t.offset, "Root Tag doesn't match DOCTYPE. Expected: %v, got: %v\n", expected_doctype, open.text)
@@ -298,23 +247,17 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha
} }
} }
/* // One of these should follow:
One of these should follow: // - `>`, which means we've just opened this tag and expect a later element to close it.
- `>`, which means we've just opened this tag and expect a later element to close it. // - `/>`, which means this is an 'empty' or self-closing tag.
- `/>`, which means this is an 'empty' or self-closing tag.
*/
end_token := scan(t) end_token := scan(t)
#partial switch end_token.kind { #partial switch end_token.kind {
case .Gt: case .Gt:
/* // We're now the new parent.
We're now the new parent.
*/
parent = element parent = element
case .Slash: case .Slash:
/* // Empty tag. Close it.
Empty tag. Close it.
*/
expect(t, .Gt) or_return expect(t, .Gt) or_return
parent = doc.elements[element].parent parent = doc.elements[element].parent
element = parent element = parent
@@ -325,9 +268,7 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha
} }
} else if open.kind == .Slash { } else if open.kind == .Slash {
/* // Close tag.
Close tag.
*/
ident := expect(t, .Ident) or_return ident := expect(t, .Ident) or_return
_ = expect(t, .Gt) or_return _ = expect(t, .Gt) or_return
@@ -339,9 +280,7 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha
element = parent element = parent
} else if open.kind == .Exclaim { } else if open.kind == .Exclaim {
/* // <!
<!
*/
next := scan(t) next := scan(t)
#partial switch next.kind { #partial switch next.kind {
case .Ident: case .Ident:
@@ -370,10 +309,8 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha
} }
case .Dash: case .Dash:
/* // Comment: <!-- -->.
Comment: <!-- -->. // The grammar does not allow a comment to end in --->
The grammar does not allow a comment to end in --->
*/
expect(t, .Dash) expect(t, .Dash)
comment := scan_comment(t) or_return comment := scan_comment(t) or_return
@@ -395,23 +332,17 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha
} }
} else if open.kind == .Question { } else if open.kind == .Question {
/* // <?xml
<?xml
*/
next := scan(t) next := scan(t)
#partial switch next.kind { #partial switch next.kind {
case .Ident: case .Ident:
if len(next.text) == 3 && strings.equal_fold(next.text, "xml") { if len(next.text) == 3 && strings.equal_fold(next.text, "xml") {
parse_prologue(doc) or_return parse_prologue(doc) or_return
} else if len(doc.prologue) > 0 { } else if len(doc.prologue) > 0 {
/* // We've already seen a prologue.
We've already seen a prologue.
*/
return doc, .Too_Many_Prologs return doc, .Too_Many_Prologs
} else { } else {
/* // Could be `<?xml-stylesheet`, etc. Ignore it.
Could be `<?xml-stylesheet`, etc. Ignore it.
*/
skip_element(t) or_return skip_element(t) or_return
} }
case: case:
@@ -425,15 +356,11 @@ parse_bytes :: proc(data: []u8, options := DEFAULT_OPTIONS, path := "", error_ha
} }
case -1: case -1:
/* // End of file.
End of file.
*/
break loop break loop
case: case:
/* // This should be a tag's body text.
This should be a tag's body text.
*/
body_text := scan_string(t, t.offset) or_return body_text := scan_string(t, t.offset) or_return
needs_processing := .Unbox_CDATA in opts.flags needs_processing := .Unbox_CDATA in opts.flags
needs_processing |= .Decode_SGML_Entities in opts.flags needs_processing |= .Decode_SGML_Entities in opts.flags
@@ -613,9 +540,7 @@ parse_prologue :: proc(doc: ^Document) -> (err: Error) {
doc.encoding = .LATIN_1 doc.encoding = .LATIN_1
case: case:
/* // Unrecognized encoding, assume UTF-8.
Unrecognized encoding, assume UTF-8.
*/
error(t, offset, "[parse_prologue] Warning: Unrecognized encoding: %v\n", attr.val) error(t, offset, "[parse_prologue] Warning: Unrecognized encoding: %v\n", attr.val)
} }
@@ -675,9 +600,7 @@ parse_doctype :: proc(doc: ^Document) -> (err: Error) {
offset := t.offset offset := t.offset
skip_element(t) or_return skip_element(t) or_return
/* // -1 because the current offset is that of the closing tag, so the rest of the DOCTYPE tag ends just before it.
-1 because the current offset is that of the closing tag, so the rest of the DOCTYPE tag ends just before it.
*/
doc.doctype.rest = string(t.src[offset : t.offset - 1]) doc.doctype.rest = string(t.src[offset : t.offset - 1])
return .None return .None
} }