on master help sign in
30 files changed+2100−239
since splitsnapshots
unifiedsplit
.ghci+13−1 .github/workflows/ci.yml+33 .gitignore+11 CHANGES.txt+19 LICENSE+217 PULL_REQUEST_TEMPLATE.md+5 README.md+17−2 Setup.hs+2 afl.sh+40 cbits/fuzz.c+26 cbits/grammar.txt+129 cbits/hexml.c+170−115 cbits/hexml.h+19−16 cbits/main.c+46 gen/.ghci+3 gen/.ghcid+1 gen/Lib.hs+201 gen/Main.hs+70 gen/Parser.hs+76 gen/test.c+488 ghci.bat+2−1 go.bat−2 hexml.cabal+55 main.c−22 src/Main.hs+78−6 src/Text/XML/Hexml.hs+173−74 vs/hexml.sln+28 vs/hexml.vcxproj+153 xml/example.xml+23 xml/simple.xml+2
.ghci+13−1
@@ -1,5 +1,17 @@
1−:set -fwarn-unused-binds -fwarn-unused-imports
1+:set -Wunused-binds -Wunused-imports -Worphans
22:set -isrc
33:load Main
44
55:def test \xs -> return $ ":main " ++ xs
6+
7+:{
8+:def docs_ const $ return $ unlines
9+ [":!cabal configure"
10+ ,":!cabal haddock"]
11+:}
12+
13+:{
14+:def docs const $ return $ unlines
15+ [":docs_"
16+ ,":!start dist\\doc\\html\\hexml\\Text-XML-Hexml.html"]
17+:}
.github/workflows/ci.ymlnew file+33
@@ -0,0 +1,33 @@
1+name: ci
2+on:
3+ push:
4+ pull_request:
5+ schedule:
6+ - cron: '0 3 * * 6' # 3am Saturday
7+jobs:
8+ test:
9+ runs-on: ${{ matrix.os }}
10+
11+ strategy:
12+ fail-fast: false
13+ matrix:
14+ os: [ubuntu-latest]
15+ ghc: ['9.6', '9.4', '9.2', '9.0', '8.10', '8.8']
16+ include:
17+ - os: windows-latest
18+ - os: macOS-latest
19+
20+ steps:
21+ - run: git config --global core.autocrlf false
22+ - uses: actions/checkout@v2
23+ - uses: haskell/actions/setup@v2
24+ id: setup-haskell
25+ with:
26+ ghc-version: ${{ matrix.ghc }}
27+ - run: cabal v2-freeze --enable-tests
28+ - uses: actions/cache@v2
29+ with:
30+ path: ${{ steps.setup-haskell.outputs.cabal-store }}
31+ key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
32+ restore-keys: ${{ runner.os }}-${{ matrix.ghc }}-
33+ - uses: ndmitchell/neil@master
.gitignorenew file+11
@@ -0,0 +1,11 @@
1+/dist/
2+*.o
3+*.prof
4+*.suo
5+*.opendb
6+*.db
7+*.user
8+/.stack-work/
9+/issues/
10+/vs/x64/
11+/afl-results/
CHANGES.txtnew file+19
@@ -0,0 +1,19 @@
1+Changelog for Hexml
2+
3+0.3.4, released 2018-09-10
4+ #15, licensed under BSD-3-Clause OR Apache-2.0
5+0.3.3, released 2017-10-27
6+ #13, allow the . character in tags and attributes
7+0.3.2, released 2017-05-22
8+ #10, mark the small fast FFI calls as "unsafe" (up to 10x faster)
9+0.3.1, released 2016-12-27
10+ #9, don't walk off the end of the character table
11+0.3, released 2016-12-22
12+ #8, Mac compatibility
13+ Fix a bug when parsing closing comments
14+ #7, prefix all the C functions with hexml_
15+0.2, released 2016-12-14
16+ #6, remove buffer overruns
17+ #5, add lower bounds
18+0.1, released 2016-12-04
19+ Initial version
LICENSEnew file+217
@@ -0,0 +1,217 @@
1+Copyright Neil Mitchell 2016-2023.
2+
3+Licensed under either of:
4+
5+* BSD-3-Clause license (https://opensource.org/licenses/BSD-3-Clause)
6+* Apache License, version 2.0 (https://opensource.org/licenses/Apache-2.0)
7+
8+As a user, you may use this code under either license, at your option.
9+
10+
11+---------------------------------------------------------------------
12+BSD 3-Clause License
13+All rights reserved.
14+
15+Redistribution and use in source and binary forms, with or without
16+modification, are permitted provided that the following conditions
17+are met:
18+
19+ * Redistributions of source code must retain the above copyright
20+ notice, this list of conditions and the following disclaimer.
21+
22+ * Redistributions in binary form must reproduce the above
23+ copyright notice, this list of conditions and the following
24+ disclaimer in the documentation and/or other materials provided
25+ with the distribution.
26+
27+ * Neither the name of Neil Mitchell nor the names of other
28+ contributors may be used to endorse or promote products derived
29+ from this software without specific prior written permission.
30+
31+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
32+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
33+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
34+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
35+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
37+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
38+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
39+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
40+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
41+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42+
43+
44+---------------------------------------------------------------------
45+Apache License
46+Version 2.0, January 2004
47+<http://www.apache.org/licenses/>
48+
49+Terms and Conditions for use, reproduction, and distribution
50+1. Definitions
51+"License" shall mean the terms and conditions for use, reproduction,
52+and distribution as defined by Sections 1 through 9 of this document.
53+
54+"Licensor" shall mean the copyright owner or entity authorized by the
55+copyright owner that is granting the License.
56+
57+"Legal Entity" shall mean the union of the acting entity and all
58+other entities that control, are controlled by, or are under common
59+control with that entity. For the purposes of this definition,
60+"control" means (i) the power, direct or indirect, to cause the
61+direction or management of such entity, whether by contract or
62+otherwise, or (ii) ownership of fifty percent (50%) or more of the
63+outstanding shares, or (iii) beneficial ownership of such entity.
64+
65+"You" (or "Your") shall mean an individual or Legal Entity exercising
66+permissions granted by this License.
67+
68+"Source" form shall mean the preferred form for making modifications,
69+including but not limited to software source code, documentation
70+source, and configuration files.
71+
72+"Object" form shall mean any form resulting from mechanical
73+transformation or translation of a Source form, including but not
74+limited to compiled object code, generated documentation, and
75+conversions to other media types.
76+
77+"Work" shall mean the work of authorship, whether in Source or Object
78+form, made available under the License, as indicated by a copyright
79+notice that is included in or attached to the work).
80+
81+"Derivative Works" shall mean any work, whether in Source or Object
82+form, that is based on (or derived from) the Work and for which the
83+editorial revisions, annotations, elaborations, or other
84+modifications represent, as a whole, an original work of authorship.
85+For the purposes of this License, Derivative Works shall not include
86+works that remain separable from, or merely link (or bind by name) to
87+the interfaces of, the Work and Derivative Works thereof.
88+
89+"Contribution" shall mean any work of authorship, including the
90+original version of the Work and any modifications or additions to
91+that Work or Derivative Works thereof, that is intentionally
92+submitted to Licensor for inclusion in the Work by the copyright
93+owner or by an individual or Legal Entity authorized to submit on
94+behalf of the copyright owner. For the purposes of this definition,
95+"submitted" means any form of electronic, verbal, or written
96+communication sent to the Licensor or its representatives, including
97+but not limited to communication on electronic mailing lists, source
98+code control systems, and issue tracking systems that are managed by,
99+or on behalf of, the Licensor for the purpose of discussing and
100+improving the Work, but excluding communication that is conspicuously
101+marked or otherwise designated in writing by the copyright owner as
102+"Not a Contribution."
103+
104+"Contributor" shall mean Licensor and any individual or Legal Entity
105+on behalf of whom a Contribution has been received by Licensor and
106+subsequently incorporated within the Work.
107+
108+2. Grant of Copyright License
109+Subject to the terms and conditions of this License, each Contributor
110+hereby grants to You a perpetual, worldwide, non-exclusive,
111+no-charge, royalty-free, irrevocable copyright license to reproduce,
112+prepare Derivative Works of, publicly display, publicly perform,
113+sublicense, and distribute the Work and such Derivative Works in
114+Source or Object form.
115+
116+3. Grant of Patent License
117+Subject to the terms and conditions of this License, each Contributor
118+hereby grants to You a perpetual, worldwide, non-exclusive,
119+no-charge, royalty-free, irrevocable (except as stated in this
120+section) patent license to make, have made, use, offer to sell, sell,
121+import, and otherwise transfer the Work, where such license applies
122+only to those patent claims licensable by such Contributor that are
123+necessarily infringed by their Contribution(s) alone or by
124+combination of their Contribution(s) with the Work to which such
125+Contribution(s) was submitted. If You institute patent litigation
126+against any entity (including a cross-claim or counterclaim in a
127+lawsuit) alleging that the Work or a Contribution incorporated within
128+the Work constitutes direct or contributory patent infringement, then
129+any patent licenses granted to You under this License for that Work
130+shall terminate as of the date such litigation is filed.
131+
132+4. Redistribution
133+You may reproduce and distribute copies of the Work or Derivative
134+Works thereof in any medium, with or without modifications, and in
135+Source or Object form, provided that You meet the following
136+conditions:
137+
138+(a) You must give any other recipients of the Work or Derivative
139+Works a copy of this License; and
140+(b) You must cause any modified files to carry prominent notices
141+stating that You changed the files; and
142+(c) You must retain, in the Source form of any Derivative Works that
143+You distribute, all copyright, patent, trademark, and attribution
144+notices from the Source form of the Work, excluding those notices
145+that do not pertain to any part of the Derivative Works; and
146+(d) If the Work includes a "NOTICE" text file as part of its
147+distribution, then any Derivative Works that You distribute must
148+include a readable copy of the attribution notices contained within
149+such NOTICE file, excluding those notices that do not pertain to any
150+part of the Derivative Works, in at least one of the following
151+places: within a NOTICE text file distributed as part of the
152+Derivative Works; within the Source form or documentation, if
153+provided along with the Derivative Works; or, within a display
154+generated by the Derivative Works, if and wherever such third-party
155+notices normally appear. The contents of the NOTICE file are for
156+informational purposes only and do not modify the License. You may
157+add Your own attribution notices within Derivative Works that You
158+distribute, alongside or as an addendum to the NOTICE text from the
159+Work, provided that such additional attribution notices cannot be
160+construed as modifying the License.
161+You may add Your own copyright statement to Your modifications and
162+may provide additional or different license terms and conditions for
163+use, reproduction, or distribution of Your modifications, or for any
164+such Derivative Works as a whole, provided Your use, reproduction,
165+and distribution of the Work otherwise complies with the conditions
166+stated in this License.
167+
168+5. Submission of Contributions
169+Unless You explicitly state otherwise, any Contribution intentionally
170+submitted for inclusion in the Work by You to the Licensor shall be
171+under the terms and conditions of this License, without any
172+additional terms or conditions. Notwithstanding the above, nothing
173+herein shall supersede or modify the terms of any separate license
174+agreement you may have executed with Licensor regarding such
175+Contributions.
176+
177+6. Trademarks
178+This License does not grant permission to use the trade names,
179+trademarks, service marks, or product names of the Licensor, except
180+as required for reasonable and customary use in describing the origin
181+of the Work and reproducing the content of the NOTICE file.
182+
183+7. Disclaimer of Warranty
184+Unless required by applicable law or agreed to in writing, Licensor
185+provides the Work (and each Contributor provides its Contributions)
186+on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
187+either express or implied, including, without limitation, any
188+warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY,
189+or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for
190+determining the appropriateness of using or redistributing the Work
191+and assume any risks associated with Your exercise of permissions
192+under this License.
193+
194+8. Limitation of Liability
195+In no event and under no legal theory, whether in tort (including
196+negligence), contract, or otherwise, unless required by applicable
197+law (such as deliberate and grossly negligent acts) or agreed to in
198+writing, shall any Contributor be liable to You for damages,
199+including any direct, indirect, special, incidental, or consequential
200+damages of any character arising as a result of this License or out
201+of the use or inability to use the Work (including but not limited to
202+damages for loss of goodwill, work stoppage, computer failure or
203+malfunction, or any and all other commercial damages or losses), even
204+if such Contributor has been advised of the possibility of such
205+damages.
206+
207+9. Accepting Warranty or Additional Liability
208+While redistributing the Work or Derivative Works thereof, You may
209+choose to offer, and charge a fee for, acceptance of support,
210+warranty, indemnity, or other liability obligations and/or rights
211+consistent with this License. However, in accepting such obligations,
212+You may act only on Your own behalf and on Your sole responsibility,
213+not on behalf of any other Contributor, and only if You agree to
214+indemnify, defend, and hold each Contributor harmless for any
215+liability incurred by, or claims asserted against, such Contributor
216+by reason of your accepting any such warranty or additional
217+liability.
PULL_REQUEST_TEMPLATE.mdnew file+5
@@ -0,0 +1,5 @@
1+Thanks for the pull request!
2+
3+By raising this pull request you confirm you are licensing your contribution under all licenses that apply to this project (see LICENSE) and that you have no patents covering your contribution.
4+
5+If you care, my PR preferences are at https://github.com/ndmitchell/neil#contributions, but they're all guidelines, and I'm not too fussy - you don't have to read them.
README.md+17−2
@@ -1,2 +1,17 @@
1−# hexml
2−A bad XML parser
1+# Hexml [![Hackage version](https://img.shields.io/hackage/v/hexml.svg?label=Hackage)](https://hackage.haskell.org/package/hexml) [![Stackage version](https://www.stackage.org/package/hexml/badge/nightly?label=Stackage)](https://www.stackage.org/package/hexml) [![Build status](https://img.shields.io/github/actions/workflow/status/ndmitchell/hexml/ci.yml?branch=master)](https://github.com/ndmitchell/hexml/actions)
2+
3+An XML DOM-style parser, that only parses a subset of XML, but is designed to be fast. In particular:
4+
5+* Entities, e.g. `&amp;`, are not expanded.
6+* Not all the validity conditions are checked.
7+* No support for `<!DOCTYPE` related features.
8+
9+The name "hexml" is a combination of "Hex" (a curse) and "XML". The "X" should not be capitalised because the parser is more curse and less XML.
10+
11+Hexml may be suitable if you want to quickly parse XML, from known sources, and a full XML parser has been shown to be a bottleneck. As an alternative to hexml, which supports things like entities but is still pretty fast, see [Pugixml](http://pugixml.org/) (with a [Haskell binding](https://hackage.haskell.org/package/pugixml) - but be aware the Haskell binding [can segfault](https://github.com/philopon/pugixml-hs/issues/5)).
12+
13+Hexml is tested with [AFL](http://lcamtuf.coredump.cx/afl/).
14+
15+If you want lenses for Hexml, see [hexml-lens](http://hackage.haskell.org/package/hexml-lens).
16+
17+The optimisation work around Hexml spawned [Xeno](http://hackage.haskell.org/package/xeno), a Haskell-only alternative to Hexml. There is a [talk](https://ndmitchell.com/#hexml_12_oct_2017) covering the performance tricks of Hexml and Xeno.
Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
afl.shnew file+40
@@ -0,0 +1,40 @@
1+# Script based on code from https://github.com/ndmitchell/hexml/issues/6
2+# More examples of things to do are at that ticket
3+set -e
4+
5+echo DISABLED DUE TO SEGFAULT IN CLANG WHEN COMPILING AFL
6+echo See https://github.com/ndmitchell/hexml/issues/14
7+exit 0
8+
9+# First install AFL
10+if ! command -v afl-clang-fast > /dev/null ; then
11+ wget http://lcamtuf.coredump.cx/afl/releases/afl-latest.tgz
12+ tar xf afl-latest.tgz
13+ cd afl-*
14+ make
15+ cd llvm_mode && make && cd ..
16+ sudo make install
17+ cd libdislocator && make && sudo make install && cd ..
18+ cd ..
19+fi
20+
21+# Compile it
22+AFL_USE_ASAN=1 afl-clang-fast -O2 -Icbits cbits/fuzz.c -o $PWD/hexml-fuzz
23+
24+# Fuzz it
25+if ! grep -q core /proc/sys/kernel/core_pattern ; then
26+ echo core | sudo tee /proc/sys/kernel/core_pattern
27+fi
28+if [ -z "$CI" ] ; then
29+ afl-fuzz -T hexml -m none -x /usr/local/share/afl/dictionaries/xml.dict -i $PWD/xml -o $PWD/afl-results -- $PWD/hexml-fuzz @@
30+else
31+ AFL_EXIT_WHEN_DONE=1 timeout 5m afl-fuzz -T hexml -m none -x /usr/local/share/afl/dictionaries/xml.dict -i $PWD/xml -o $PWD/afl-results -- $PWD/hexml-fuzz @@ > /dev/null || true
32+ cat afl-results/fuzzer_stats
33+ grep "unique_crashes *: 0" afl-results/fuzzer_stats # fail if there are failures
34+fi
35+
36+# Minimize failures
37+# $ AFL_PRELOAD=/usr/local/lib/afl/libdislocator.so afl-cmin -i results/crashes/ -o results.shrunk -- $PWD/a.out @@
38+# $ mkdir results.min
39+# $ for x in `ls results.shrunk`; do AFL_PRELOAD=/usr/local/lib/afl/libdislocator.so afl-tmin -i results.shrunk/$x -o results.min/$x -- $PWD/a.out @@; done
40+# $ AFL_PRELOAD=/usr/local/lib/afl/libdislocator.so afl-analyze -i results.shrunk/id:000013,sig:11,src:000002,op:havoc,rep:4 -- ./a.out @@
cbits/fuzz.cnew file+26
@@ -0,0 +1,26 @@
1+#include <stdio.h>
2+#include <stdlib.h>
3+#include <string.h>
4+#include "hexml.c"
5+
6+int main(int ac, char** av)
7+{
8+#ifdef __AFL_HAVE_MANUAL_CONTROL
9+ __AFL_INIT();
10+#endif
11+
12+ FILE *f = fopen(av[1], "rb");
13+ fseek(f, 0, SEEK_END);
14+ size_t fsize = ftell(f);
15+ rewind(f);
16+
17+ char* string = malloc(fsize+1);
18+ memset(string, 0, fsize+1);
19+ (void) fread(string, fsize, 1, f);
20+ fclose(f);
21+
22+ document *doc = hexml_document_parse(string, fsize);
23+ hexml_document_free(doc);
24+ free(string);
25+ return 0;
26+}
cbits/grammar.txtnew file+129
@@ -0,0 +1,129 @@
1+-- Taken from https://www.w3.org/TR/xml11/ and simplified
2+
3+[1] document ::= content
4+[39] element ::= XMLDecl | EmptyElemTag | STag content ETag
5+[23] XMLDecl ::= '<?xml' (S Attribute)* S? '?>'
6+[10] AttValue ::= '"' [^<"] '"' | "'" [^<'] "'"
7+[44] EmptyElemTag ::= '<' Name (S Attribute)* S? '/>'
8+[25] Eq ::= S? '=' S?
9+[40] STag ::= '<' Name (S Attribute)* S? '>' [WFC: Unique Att Spec]
10+[41] Attribute ::= Name Eq AttValue
11+[43] content ::= (element | CDSect | Comment | [^<]+)*
12+[42] ETag ::= '</' Name S? '>'
13+[18] CDSect ::= CDStart CData CDEnd
14+[19] CDStart ::= '<![CDATA['
15+[20] CData ::= (Char* - (Char* ']]>' Char*))
16+[21] CDEnd ::= ']]>'
17+[15] Comment ::= '<!--' ((Char - '-') | ('-' (Char - '-')))* '-->'
18+
19+-- The original, mechanically extracted
20+
21+[1] document ::= ( prolog element Misc* ) - ( Char* RestrictedChar Char* )
22+[2] Char ::= [#x1-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] /* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */
23+[2a] RestrictedChar ::= [#x1-#x8] | [#xB-#xC] | [#xE-#x1F] | [#x7F-#x84] | [#x86-#x9F]
24+[3] S ::= (#x20 | #x9 | #xD | #xA)+
25+[4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]
26+[4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
27+[5] Name ::= NameStartChar (NameChar)*
28+[6] Names ::= Name (#x20 Name)*
29+[7] Nmtoken ::= (NameChar)+
30+[8] Nmtokens ::= Nmtoken (#x20 Nmtoken)*
31+[9] EntityValue ::= '"' ([^%&"] | PEReference | Reference)* '"'
32+| "'" ([^%&'] | PEReference | Reference)* "'"
33+[10] AttValue ::= '"' ([^<&"] | Reference)* '"'
34+| "'" ([^<&'] | Reference)* "'"
35+[11] SystemLiteral ::= ('"' [^"]* '"') | ("'" [^']* "'")
36+[12] PubidLiteral ::= '"' PubidChar* '"' | "'" (PubidChar - "'")* "'"
37+[13] PubidChar ::= #x20 | #xD | #xA | [a-zA-Z0-9] | [-'()+,./:=?;!*#@$_%]
38+[14] CharData ::= [^<&]* - ([^<&]* ']]>' [^<&]*)
39+[15] Comment ::= '<!--' ((Char - '-') | ('-' (Char - '-')))* '-->'
40+[16] PI ::= '<?' PITarget (S (Char* - (Char* '?>' Char*)))? '?>'
41+[17] PITarget ::= Name - (('X' | 'x') ('M' | 'm') ('L' | 'l'))
42+[18] CDSect ::= CDStart CData CDEnd
43+[19] CDStart ::= '<![CDATA['
44+[20] CData ::= (Char* - (Char* ']]>' Char*))
45+[21] CDEnd ::= ']]>'
46+[22] prolog ::= XMLDecl Misc* (doctypedecl Misc*)?
47+[23] XMLDecl ::= '<?xml' VersionInfo EncodingDecl? SDDecl? S? '?>'
48+[24] VersionInfo ::= S 'version' Eq ("'" VersionNum "'" | '"' VersionNum '"')
49+[25] Eq ::= S? '=' S?
50+[26] VersionNum ::= '1.1'
51+[27] Misc ::= Comment | PI | S
52+[28] doctypedecl ::= '<!DOCTYPE' S Name (S ExternalID)? S? ('[' intSubset ']' S?)? '>' [VC: Root Element Type] [WFC: External Subset]
53+[28a] DeclSep ::= PEReference | S [WFC: PE Between Declarations]
54+[28b] intSubset ::= (markupdecl | DeclSep)*
55+[29] markupdecl ::= elementdecl | AttlistDecl | EntityDecl | NotationDecl | PI | Comment [VC: Proper Declaration/PE Nesting] [WFC: PEs in Internal Subset]
56+[30] extSubset ::= TextDecl? extSubsetDecl
57+[31] extSubsetDecl ::= ( markupdecl | conditionalSect | DeclSep)*
58+[32] SDDecl ::= S 'standalone' Eq (("'" ('yes' | 'no') "'") | ('"' ('yes' | 'no') '"')) [VC: Standalone Document Declaration]
59+[39] element ::= EmptyElemTag
60+| STag content ETag [WFC: Element Type Match]
61+[VC: Element Valid]
62+[40] STag ::= '<' Name (S Attribute)* S? '>' [WFC: Unique Att Spec]
63+[41] Attribute ::= Name Eq AttValue [VC: Attribute Value Type]
64+[WFC: No External Entity References]
65+[WFC: No < in Attribute Values]
66+[42] ETag ::= '</' Name S? '>'
67+[43] content ::= CharData? ((element | Reference | CDSect | PI | Comment) CharData?)*
68+[44] EmptyElemTag ::= '<' Name (S Attribute)* S? '/>' [WFC: Unique Att Spec]
69+[45] elementdecl ::= '<!ELEMENT' S Name S contentspec S? '>' [VC: Unique Element Type Declaration]
70+[46] contentspec ::= 'EMPTY' | 'ANY' | Mixed | children
71+[47] children ::= (choice | seq) ('?' | '*' | '+')?
72+[48] cp ::= (Name | choice | seq) ('?' | '*' | '+')?
73+[49] choice ::= '(' S? cp ( S? '|' S? cp )+ S? ')' [VC: Proper Group/PE Nesting]
74+[50] seq ::= '(' S? cp ( S? ',' S? cp )* S? ')' [VC: Proper Group/PE Nesting]
75+[51] Mixed ::= '(' S? '#PCDATA' (S? '|' S? Name)* S? ')*'
76+| '(' S? '#PCDATA' S? ')' [VC: Proper Group/PE Nesting]
77+[VC: No Duplicate Types]
78+[52] AttlistDecl ::= '<!ATTLIST' S Name AttDef* S? '>'
79+[53] AttDef ::= S Name S AttType S DefaultDecl
80+[54] AttType ::= StringType | TokenizedType | EnumeratedType
81+[55] StringType ::= 'CDATA'
82+[56] TokenizedType ::= 'ID' [VC: ID]
83+[VC: One ID per Element Type]
84+[VC: ID Attribute Default]
85+| 'IDREF' [VC: IDREF]
86+| 'IDREFS' [VC: IDREF]
87+| 'ENTITY' [VC: Entity Name]
88+[57] EnumeratedType ::= NotationType | Enumeration
89+[58] NotationType ::= 'NOTATION' S '(' S? Name (S? '|' S? Name)* S? ')' [VC: Notation Attributes]
90+[VC: One Notation Per Element Type]
91+[VC: No Notation on Empty Element]
92+[VC: No Duplicate Tokens]
93+[59] Enumeration ::= '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')' [VC: Enumeration]
94+[VC: No Duplicate Tokens]
95+[60] DefaultDecl ::= '#REQUIRED' | '#IMPLIED'
96+| (('#FIXED' S)? AttValue) [VC: Required Attribute]
97+[VC: Attribute Default Value Syntactically Correct]
98+[WFC: No < in Attribute Values]
99+[VC: Fixed Attribute Default]
100+[WFC: No External Entity References]
101+[61] conditionalSect ::= includeSect | ignoreSect
102+[62] includeSect ::= '<![' S? 'INCLUDE' S? '[' extSubsetDecl ']]>' [VC: Proper Conditional Section/PE Nesting]
103+[63] ignoreSect ::= '<![' S? 'IGNORE' S? '[' ignoreSectContents* ']]>' [VC: Proper Conditional Section/PE Nesting]
104+[64] ignoreSectContents ::= Ignore ('<![' ignoreSectContents ']]>' Ignore)*
105+[65] Ignore ::= Char* - (Char* ('<![' | ']]>') Char*)
106+[66] CharRef ::= '&#' [0-9]+ ';'
107+| '&#x' [0-9a-fA-F]+ ';' [WFC: Legal Character]
108+[67] Reference ::= EntityRef | CharRef
109+[68] EntityRef ::= '&' Name ';' [WFC: Entity Declared]
110+[VC: Entity Declared]
111+[WFC: Parsed Entity]
112+[WFC: No Recursion]
113+[69] PEReference ::= '%' Name ';' [VC: Entity Declared]
114+[WFC: No Recursion]
115+[WFC: In DTD]
116+[70] EntityDecl ::= GEDecl | PEDecl
117+[71] GEDecl ::= '<!ENTITY' S Name S EntityDef S? '>'
118+[72] PEDecl ::= '<!ENTITY' S '%' S Name S PEDef S? '>'
119+[73] EntityDef ::= EntityValue | (ExternalID NDataDecl?)
120+[74] PEDef ::= EntityValue | ExternalID
121+[75] ExternalID ::= 'SYSTEM' S SystemLiteral
122+| 'PUBLIC' S PubidLiteral S SystemLiteral
123+[76] NDataDecl ::= S 'NDATA' S Name [VC: Notation Declared]
124+[77] TextDecl ::= '<?xml' VersionInfo? EncodingDecl S? '?>'
125+[78] extParsedEnt ::= ( TextDecl? content ) - ( Char* RestrictedChar Char* )
126+[80] EncodingDecl ::= S 'encoding' Eq ('"' EncName '"' | "'" EncName "'" )
127+[81] EncName ::= [A-Za-z] ([A-Za-z0-9._] | '-')* /* Encoding name contains only Latin characters */
128+[82] NotationDecl ::= '<!NOTATION' S Name S (ExternalID | PublicID) S? '>' [VC: Unique Notation Name]
129+[83] PublicID ::= 'PUBLIC' S PubidLiteral
cbits/hexml.c+170−115
@@ -1,5 +1,5 @@
11#include "hexml.h"
2−#include <malloc.h>
2+#include <stdlib.h>
33#include <string.h>
44#include <assert.h>
55#include <ctype.h>
@@ -10,18 +10,18 @@ typedef int bool;
1010/////////////////////////////////////////////////////////////////////
1111// TYPES
1212
13−int static inline end(str s) { return s.start + s.length; }
13+static inline int end(str s) { return s.start + s.length; }
1414
15−str static inline start_length(int32_t start, int32_t length)
15+static inline str start_length(int32_t start, int32_t length)
1616{
17− if (start < 0 || length < 0) assert(0);
17+ assert(start >= 0 && length >= 0);
1818 str res;
1919 res.start = start;
2020 res.length = length;
2121 return res;
2222}
2323
24−str static inline start_end(int32_t start, int32_t end)
24+static inline str start_end(int32_t start, int32_t end)
2525{
2626 return start_length(start, end - start);
2727}
@@ -40,7 +40,7 @@ typedef struct
4040 // have a cursor at the end, which is stack scoped, so children write out, then I do
4141 // when you commit, you copy over from end to front
4242
43− // nodes
43+ // nodes
4444 int size;
4545 int used_front; // front entries, stored for good
4646 int used_back; // back entries, stack based, copied into front
@@ -51,11 +51,11 @@ typedef struct
5151
5252struct document
5353{
54− char* body; // pointer to initial argument, not owned by us
54+ const char* body; // pointer to initial argument, not owned by us
5555
5656 // things only used while parsing
57− char* cursor; // pointer to where we are in body
58− char* end; // pointer to one past the last char
57+ const char* cursor; // pointer to where we are in body
58+ const char* end; // pointer to one past the last char
5959 // if cursor is > end we have gone past the end
6060
6161 char* error_message;
@@ -63,8 +63,8 @@ struct document
6363 attr_buffer attrs;
6464};
6565
66−int static inline doc_length(document* d) { return d->end - d->body; }
67−int static inline doc_position(document* d) { return d->cursor - d->body; }
66+static inline int doc_length(const document* d) { return (int) (d->end - d->body); }
67+static inline int doc_position(const document* d) { return (int) (d->cursor - d->body); }
6868
6969
7070/////////////////////////////////////////////////////////////////////
@@ -72,49 +72,45 @@ int static inline doc_position(document* d) { return d->cursor - d->body; }
7272
7373typedef struct
7474{
75− document* d;
75+ const document* d;
7676 char* buffer;
7777 int length;
7878 int cursor;
7979} render;
8080
81−void static inline render_char(render* r, char c)
81+static inline void render_char(render* r, char c)
8282{
8383 if (r->cursor < r->length)
8484 r->buffer[r->cursor] = c;
8585 r->cursor++;
8686}
8787
88−void static inline bound(char* msg, int index, int mn, int mx)
88+static inline void bound(int idx, int mn, int mx)
8989{
90− if (index < mn || index > mx)
91− {
92− //printf("Bounds checking failed %s, got %i, which should be in %i:%i\n", msg, index, mn, mx);
93− assert(0);
94− }
90+ assert(idx >= mn && idx <= mx);
9591}
9692
97−void static inline bound_str(char* msg, str s, int mn, int mx)
93+static inline void bound_str(str s, int mn, int mx)
9894{
99− if (s.length < 0) assert(0);
100− bound(msg, s.start, mn, mx);
101− bound(msg, end(s), mn, mx);
95+ assert(s.length >= 0);
96+ bound(s.start, mn, mx);
97+ bound(end(s), mn, mx);
10298}
10399
104−void render_str(render* r, str s)
100+static void render_str(render* r, str s)
105101{
106− bound_str("render_str", s, 0, doc_length(r->d));
102+ bound_str(s, 0, doc_length(r->d));
107103 for (int i = 0; i < s.length; i++)
108104 render_char(r, r->d->body[s.start + i]);
109105}
110106
111−void render_tag(render* r, node* n);
107+static void render_tag(render* r, const node* n);
112108
113−void render_content(render* r, node* n)
109+static void render_content(render* r, const node* n)
114110{
115− bound_str("render_conent inner", n->inner, 0, doc_length(r->d));
116− bound_str("render_conent nodes", n->nodes, 0, r->d->nodes.used_front);
117− bound_str("render_conent attrs", n->attrs, 0, r->d->attrs.used);
111+ bound_str(n->inner, 0, doc_length(r->d));
112+ bound_str(n->nodes, 0, r->d->nodes.used_front);
113+ bound_str(n->attrs, 0, r->d->attrs.used);
118114
119115 int done = n->inner.start;
120116 for (int i = 0; i < n->nodes.length; i++)
@@ -127,7 +123,7 @@ void render_content(render* r, node* n)
127123 render_str(r, start_end(done, end(n->inner)));
128124}
129125
130−void render_tag(render* r, node* n)
126+static void render_tag(render* r, const node* n)
131127{
132128 render_char(r, '<');
133129 render_str(r, n->name);
@@ -149,14 +145,18 @@ void render_tag(render* r, node* n)
149145 render_char(r, '>');
150146}
151147
152−int document_render(document* d, char* buffer, int length)
148+int hexml_node_render(const document* d, const node* n, char* buffer, int length)
153149{
154150 render r;
155151 r.d = d;
156152 r.buffer = buffer;
157153 r.length = length;
158154 r.cursor = 0;
159− render_content(&r, document_node(d));
155+ // The root node (and only the root node) has an empty length, so just render its innards
156+ if (n->name.length == 0)
157+ render_content(&r, n);
158+ else
159+ render_tag(&r, n);
160160 return r.cursor;
161161}
162162
@@ -164,10 +164,10 @@ int document_render(document* d, char* buffer, int length)
164164/////////////////////////////////////////////////////////////////////
165165// NOT THE PARSER
166166
167−char* document_error(document* d){return d->error_message;}
168−node* document_node(document* d){return &d->nodes.nodes[0];}
167+char* hexml_document_error(const document* d){return d->error_message;}
168+node* hexml_document_node(const document* d){return &d->nodes.nodes[0];}
169169
170−void document_free(document* d)
170+void hexml_document_free(document* d)
171171{
172172 free(d->error_message);
173173 free(d->nodes.alloc);
@@ -175,93 +175,94 @@ void document_free(document* d)
175175 free(d);
176176}
177177
178−node* node_children(document* d, node* n, int* res)
178+node* hexml_node_children(const document* d, const node* n, int* res)
179179{
180180 *res = n->nodes.length;
181181 return &d->nodes.nodes[n->nodes.start];
182182}
183183
184−attr* node_attributes(document* d, node* n, int* res)
184+attr* hexml_node_attributes(const document* d, const node* n, int* res)
185185{
186186 *res = n->attrs.length;
187187 return &d->attrs.attrs[n->attrs.start];
188188}
189189
190190
191−attr* node_attributeBy(document* d, node* n, char* s, int slen)
191+attr* hexml_node_attribute(const document* d, const node* n, const char* s, int slen)
192192{
193− assert(0);
193+ if (slen == -1) slen = (int) strlen(s);
194+ const int limit = end(n->attrs);
195+ for (int i = n->attrs.start; i < limit; i++)
196+ {
197+ attr* r = &d->attrs.attrs[i];
198+ if (r->name.length == slen && memcmp(s, &d->body[r->name.start], slen) == 0)
199+ return r;
200+ }
194201 return NULL;
195202}
196203
197204// Search for given strings within a node
198−node* node_firstChildBy(document* d, node* n, char* s, int slen)
205+node* hexml_node_child(const document* d, const node* parent, const node* prev, const char* s, int slen)
199206{
200− if (slen == -1) slen = strlen(s);
201− int i = n->nodes.start;
202− int limit = i + n->nodes.length;
207+ if (slen == -1) slen = (int) strlen(s);
208+ int i = prev == NULL ? parent->nodes.start : (int) (prev + 1 - d->nodes.nodes);
209+ const int limit = end(parent->nodes);
203210 for (; i < limit; i++)
204211 {
205− if (d->nodes.nodes[i].name.length == slen &&
206− memcmp(s, &d->body[d->nodes.nodes[i].name.start], slen) == 0)
207− return &d->nodes.nodes[i];
212+ node* r = &d->nodes.nodes[i];
213+ if (r->name.length == slen && memcmp(s, &d->body[r->name.start], slen) == 0)
214+ return r;
208215 }
209216 return NULL;
210217}
211218
212−node* node_nextChildBy(document* d, node* parent, node* prev, char* s, int slen)
213−{
214− assert(0);
215− return NULL;
216−}
217−
218219
219220/////////////////////////////////////////////////////////////////////
220221// PARSE TABLE
221222
222−const char tag_name1 = 0x1;
223−const char tag_name = 0x2;
224−const char tag_space = 0x4;
223+static const char tag_name1 = 0x1;
224+static const char tag_name = 0x2;
225+static const char tag_space = 0x4;
225226
226−char parse_table[256];
227+static char parse_table[256];
227228
228−void init_parse_table()
229+static void init_parse_table()
229230{
230231 static bool done = 0;
231232 if (done) return;
232233 for (int i = 0; i < 256; i++)
233234 {
234235 bool name1 = i == ':' || i == '_' || (i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z');
235− bool name = name1 || i == '-' || (i >= '0' && i <= '9');
236+ bool name = name1 || i == '-' || i == '.' || (i >= '0' && i <= '9');
236237 bool space = i == ' ' || i == '\t' || i == '\r' || i == '\n';
237238 parse_table[i] = (name1 ? tag_name1 : 0) | (name ? tag_name : 0) | (space ? tag_space : 0);
238239 }
239240 done = 1;
240241}
241242
242−bool static inline is(char c, char tag) { return parse_table[c] & tag; }
243−bool static inline is_name1(char c) { return is(c, tag_name1); }
244−bool static inline is_name(char c) { return is(c, tag_name); }
245−bool static inline is_space(char c) { return is(c, tag_space); }
243+static inline bool is(char c, char tag) { return parse_table[(unsigned char) c] & tag; }
244+static inline bool is_name1(char c) { return is(c, tag_name1); }
245+static inline bool is_name(char c) { return is(c, tag_name); }
246+static inline bool is_space(char c) { return is(c, tag_space); }
246247
247248
248249/////////////////////////////////////////////////////////////////////
249250// PARSER COMBINATORS
250251
251−char static inline peekAt(document* d, int i) { return d->cursor[i]; }
252−void static inline skip(document* d, int i) { d->cursor += i; }
253−char static inline peek(document* d) { return peekAt(d, 0); }
254−char static inline get(document* d) { char c = peek(d); skip(d, 1); return c; }
252+static inline char peek_at(const document* d, int i) { return d->cursor[i]; }
253+static inline char peek(const document* d) { return peek_at(d, 0); }
254+static inline void skip(document* d, int i) { d->cursor += i; }
255+static inline char get(document* d) { char c = peek(d); skip(d, 1); return c; }
255256
256257// Remove whitespace characters from the cursor while they are still whitespace
257−void static inline trim(document* d)
258+static inline void trim(document* d)
258259{
259260 while (is_space(peek(d)))
260261 skip(d, 1);
261262}
262263
263264// Find this character form the cursor onwards, if true adjust the cursor to that char, otherwise leave it at the end
264−bool find(document* d, char c)
265+static bool find(document* d, char c)
265266{
266267 char* x = memchr(d->cursor, c, d->end - d->cursor);
267268 if (x == NULL)
@@ -280,37 +281,47 @@ bool find(document* d, char c)
280281/////////////////////////////////////////////////////////////////////
281282// PARSING CODE
282283
283−void static inline node_alloc(node_buffer* b, int ask)
284+static inline void node_alloc(node_buffer* b, int ask)
284285{
285286 int space = b->size - b->used_back - b->used_front;
286287 if (space >= ask) return;
287− assert(0);
288+ int size2 = (b->size + 1000 + ask) * 2;
289+ node* buf2 = malloc(size2 * sizeof(node));
290+ assert(buf2);
291+ memcpy(buf2, b->nodes, b->used_front * sizeof(node));
292+ memcpy(&buf2[size2 - b->used_back], &b->nodes[b->size - b->used_back], b->used_back * sizeof(node));
293+ free(b->alloc);
294+ b->size = size2;
295+ b->nodes = buf2;
296+ b->alloc = buf2;
288297}
289298
290−void static inline attr_alloc(attr_buffer* b, int ask)
299+static inline void attr_alloc(attr_buffer* b, int ask)
291300{
292301 int space = b->size - b->used;
293302 if (space >= ask) return;
294303 int size2 = (b->size + 1000 + ask) * 2;
295304 attr* buf2 = malloc(size2 * sizeof(attr));
305+ assert(buf2);
296306 memcpy(buf2, b->attrs, b->used * sizeof(attr));
297307 free(b->alloc);
308+ b->size = size2;
298309 b->attrs = buf2;
299310 b->alloc = buf2;
300311}
301312
302−void set_error(document* d, char* msg)
313+static void set_error(document* d, const char* msg)
303314{
304315 if (d->error_message != NULL) return; // keep the first error message
305316 d->error_message = malloc(strlen(msg)+1);
317+ assert(d->error_message);
306318 strcpy(d->error_message, msg);
307319}
308320
309321// you now expect a name, perhaps preceeded by whitespace
310322// the name may be empty
311−str static inline parse_name(document* d)
323+static inline str parse_name(document* d)
312324{
313− trim(d);
314325 int start = doc_position(d);
315326 if (!is_name1(peek(d)))
316327 return start_length(start, 0);
@@ -320,10 +331,14 @@ str static inline parse_name(document* d)
320331 return start_end(start, doc_position(d));
321332}
322333
323−str static inline parse_attrval(document* d)
334+static inline str parse_attrval(document* d)
324335{
325336 trim(d);
326− if (peek(d) != '=') return start_length(0,0);
337+ if (peek(d) != '=')
338+ {
339+ set_error(d, "Expected = in attribute, but missing");
340+ return start_length(0, 0);
341+ }
327342 skip(d, 1);
328343 trim(d);
329344 char c = peek(d);
@@ -340,19 +355,22 @@ str static inline parse_attrval(document* d)
340355 return start_end(start, doc_position(d) - 1);
341356 }
342357 else
343− return parse_name(d);
358+ {
359+ set_error(d, "Invalid attribute");
360+ return start_length(0, 0);
361+ }
344362}
345363
346364
347365
348366// seen a tag name, now looking for attributes terminated by >
349367// puts the attributes it finds in the attribute buffer
350−str static inline parse_attributes(document* d)
368+static inline str parse_attributes(document* d)
351369{
352− str res;
353− res.start = d->attrs.used;
354− for (int i = 0; ; i++)
370+ int start = d->attrs.used;
371+ while (d->error_message == NULL)
355372 {
373+ trim(d);
356374 str name = parse_name(d);
357375 if (name.length == 0) break;
358376 attr_alloc(&d->attrs, 1);
@@ -360,34 +378,39 @@ str static inline parse_attributes(document* d)
360378 d->attrs.attrs[d->attrs.used].value = parse_attrval(d);
361379 d->attrs.used++;
362380 }
363− res.length = d->attrs.used - res.start;
364− return res;
381+ return start_end(start, d->attrs.used);
365382}
366383
367−str parse_content(document* d);
384+static str parse_content(document* d);
368385
369386
370387// Add a new entry into tag, am at a '<'
371−void static inline parse_tag(document* d)
388+static inline void parse_tag(document* d)
372389{
373390 node_alloc(&d->nodes, 1);
374391 d->nodes.used_back++;
375− int me = d->nodes.size - d->nodes.used_back;
392+ node* me = &d->nodes.nodes[d->nodes.size - d->nodes.used_back];
376393
377− d->nodes.nodes[me].outer.start = doc_position(d);
394+ me->outer.start = doc_position(d);
378395 char c = get(d);
379396 assert(c == '<');
380397 if (peek(d) == '?') skip(d, 1);
381− d->nodes.nodes[me].name = parse_name(d);
382− d->nodes.nodes[me].attrs = parse_attributes(d);
398+ me->name = parse_name(d);
399+ if (me->name.length == 0)
400+ {
401+ set_error(d, "Missing tag name");
402+ return;
403+ }
404+ me->attrs = parse_attributes(d);
405+ if (d->error_message != NULL) return;
383406
384− c = get(d);
385− if ((c == '/' || c == '?') && peek(d) == '>')
407+ c = peek(d);
408+ if ((c == '/' || c == '?') && peek_at(d, 1) == '>')
386409 {
387− skip(d, 1);
388− d->nodes.nodes[me].nodes = start_length(0, 0);
389− d->nodes.nodes[me].outer.length = start_end(d->nodes.nodes[me].outer.start, doc_position(d)).length;
390− d->nodes.nodes[me].inner = start_length(doc_position(d), 0);
410+ skip(d, 2);
411+ me->nodes = start_length(0, 0);
412+ me->outer.length = start_end(me->outer.start, doc_position(d)).length;
413+ me->inner = start_length(doc_position(d), 0);
391414 return;
392415 }
393416 else if (c != '>')
@@ -395,23 +418,28 @@ void static inline parse_tag(document* d)
395418 set_error(d, "Gunk at the end of the tag");
396419 return;
397420 }
398− d->nodes.nodes[me].inner.start = doc_position(d);
399− d->nodes.nodes[me].nodes = parse_content(d);
400− d->nodes.nodes[me].inner.length = start_end(d->nodes.nodes[me].inner.start, doc_position(d)).length;
421+ else
422+ skip(d, 1);
423+ me->inner.start = doc_position(d);
424+ str content = parse_content(d);
425+
426+ // parse_content may have allocated more nodes, so recompute me
427+ me = &d->nodes.nodes[d->nodes.size - d->nodes.used_back];
428+ me->nodes = content;
429+ me->inner.length = start_end(me->inner.start, doc_position(d)).length;
401430
402431 if (d->error_message != NULL) return;
403− if (peek(d) == '<' && peekAt(d, 1) == '/')
432+ if (peek(d) == '<' && peek_at(d, 1) == '/')
404433 {
405434 skip(d, 2);
406− trim(d);
407− if (d->end - d->cursor >= d->nodes.nodes[me].name.length &&
408− memcmp(d->cursor, &d->body[d->nodes.nodes[me].name.start], d->nodes.nodes[me].name.length) == 0)
435+ if (d->end - d->cursor >= me->name.length &&
436+ memcmp(d->cursor, &d->body[me->name.start], me->name.length) == 0)
409437 {
410− skip(d, d->nodes.nodes[me].name.length);
438+ skip(d, me->name.length);
411439 trim(d);
412440 if (get(d) == '>')
413441 {
414− d->nodes.nodes[me].outer.length = start_end(d->nodes.nodes[me].outer.start, doc_position(d)).length;
442+ me->outer.length = start_end(me->outer.start, doc_position(d)).length;
415443 return;
416444 }
417445 }
@@ -423,7 +451,7 @@ void static inline parse_tag(document* d)
423451
424452// Parser until </, return the index of your node children
425453// Not inline as it is recursive
426−str parse_content(document* d)
454+static str parse_content(document* d)
427455{
428456 int before = d->nodes.used_back;
429457 while (d->error_message == NULL)
@@ -433,11 +461,33 @@ str parse_content(document* d)
433461 {
434462 break;
435463 }
436− if (peekAt(d, 1) == '/')
464+ else if (peek_at(d, 1) == '/')
437465 {
438466 // have found a </
439467 break;
440468 }
469+ else if (peek_at(d, 1) == '!' && peek_at(d, 2) == '-' && peek_at(d, 3) == '-')
470+ {
471+ skip(d, 3);
472+ // you can't reuse the two '-' characters for the closing as well
473+ if (peek_at(d, 0) == '\0' || peek_at(d, 1) == '\0')
474+ {
475+ set_error(d, "Didn't get a closing comment");
476+ return start_end(0, 0);
477+ }
478+ skip(d, 2);
479+ for (;;)
480+ {
481+ if (!find(d, '>'))
482+ {
483+ set_error(d, "Didn't get a closing comment");
484+ return start_end(0, 0);
485+ }
486+ skip(d, 1);
487+ if (peek_at(d, -3) == '-' && peek_at(d, -2) == '-')
488+ break;
489+ }
490+ }
441491 else
442492 {
443493 parse_tag(d);
@@ -453,20 +503,23 @@ str parse_content(document* d)
453503 return res;
454504}
455505
506+// Based on looking at ~50Kb XML documents, they seem to have ~700 attributes
507+// and ~300 nodes, so size appropriately to cope with that.
456508typedef struct
457509{
458510 document document;
459511 attr attrs[1000];
460− node nodes[1000];
512+ node nodes[500];
461513} buffer;
462514
463−document* document_parse(char* s, int slen)
515+document* hexml_document_parse(const char* s, int slen)
464516{
465− if (slen == -1) slen = strlen(s);
517+ if (slen == -1) slen = (int) strlen(s);
466518 assert(s[slen] == 0);
467519 init_parse_table();
468520
469521 buffer* buf = malloc(sizeof(buffer));
522+ assert(buf);
470523 document* d = &buf->document;
471524 d->body = s;
472525 d->cursor = s;
@@ -476,7 +529,7 @@ document* document_parse(char* s, int slen)
476529 d->attrs.used = 0;
477530 d->attrs.attrs = buf->attrs;
478531 d->attrs.alloc = NULL;
479− d->nodes.size = 1000;
532+ d->nodes.size = 500;
480533 d->nodes.used_back = 0;
481534 d->nodes.used_front = 1;
482535 d->nodes.nodes = buf->nodes;
@@ -486,11 +539,13 @@ document* document_parse(char* s, int slen)
486539 d->nodes.nodes[0].outer = start_length(0, slen);
487540 d->nodes.nodes[0].inner = start_length(0, slen);
488541 d->nodes.nodes[0].attrs = start_length(0, 0);
489− d->nodes.nodes[0].nodes = parse_content(d);
490542
491− if (d->cursor < d->end && d->error_message == NULL)
492− {
543+ // Introduce an intermediate result, otherwise behaviour is undefined
544+ // because there is no guaranteed ordering between LHS and RHS evaluation
545+ str content = parse_content(d);
546+ d->nodes.nodes[0].nodes = content;
547+
548+ if (d->cursor < d->end)
493549 set_error(d, "Trailing junk at the end of the document");
494− }
495550 return d;
496551}
cbits/hexml.h+19−16
@@ -21,6 +21,7 @@ typedef struct
2121 str name; // tag name, e.g. <[foo]>
2222 str inner; // inner text, <foo>[bar]</foo>
2323 str outer; // outer text, [<foo>bar</foo>]
24+ // Not usable, but required to get the correct sizing when returning arrays of nodes
2425 str attrs; // all the attributes, in the attribute buffer (not usable)
2526 str nodes; // all the nodes, in the node buffer (not usable)
2627} node;
@@ -28,27 +29,29 @@ typedef struct
2829// Convention: if slen can be -1 for nul-terminated, or given explicitly
2930
3031// Parse a document, returns a potentially invalid document - use document_error to check
31−// Must use document_free to release the memory.
32−document* document_parse(char* s, int slen);
32+// Must use document_free to release the memory (even if invalid).
33+// The string must be nul terminated, e.g. slen != -1 ==> slen[0]
34+document* hexml_document_parse(const char* s, int slen);
3335
3436// Free the memory returned from document_parse.
35−void document_free(document* d);
37+void hexml_document_free(document* d);
3638
37−// generate a fresh document with the same semantics
38−// requires an input buffer, returns the size of the rendered document
39−int document_render(document* d, char* buffer, int length);
39+// Generate a fresh string with the same semantics as the node.
40+// Requires an input buffer, returns the size of the rendered document.
41+// Requires the string passed to document_parse to be valid.
42+int hexml_node_render(const document* d, const node* n, char* buffer, int length);
4043
41−// return either NULL (successful parse) or the error message
42−char* document_error(document* d);
44+// Return either NULL (successful parse) or the error message.
45+char* hexml_document_error(const document* d);
4346
44−// return the root node of the document - imagine the document is wrapped in <>$DOC</> tags
45−node* document_node(document* d);
47+// Return the root node of the document - imagine the document is wrapped in <>$DOC</> tags.
48+node* hexml_document_node(const document* d);
4649
4750// List all items within a node
48−node* node_children(document* d, node* n, int* res);
49−attr* node_attributes(document* d, node* n, int* res);
51+node* hexml_node_children(const document* d, const node* n, int* res);
52+attr* hexml_node_attributes(const document* d, const node* n, int* res);
5053
51−// Search for given strings within a node
52−node* node_firstChildBy(document* d, node* n, char* s, int slen);
53−node* node_nextChildBy(document* d, node* parent, node* prev, char* s, int slen);
54−attr* node_attributeBy(document* d, node* n, char* s, int slen);
54+// Search for given strings within a node, note that prev may be NULL
55+// Requires the string passed to document_parse to be valid.
56+node* hexml_node_child(const document* d, const node* parent, const node* prev, const char* s, int slen);
57+attr* hexml_node_attribute(const document* d, const node* n, const char* s, int slen);
cbits/main.cnew file+46
@@ -0,0 +1,46 @@
1+#include "hexml.h"
2+#include <stdio.h>
3+#include <malloc.h>
4+#include <time.h>
5+#include <string.h>
6+#include <assert.h>
7+
8+char* readFile(char* file)
9+{
10+ FILE* f = fopen(file, "rb");
11+ fseek(f, 0, SEEK_END);
12+ int len = ftell(f);
13+ fseek(f, 0, SEEK_SET);
14+ char* res = malloc(len + 1);
15+ assert(res);
16+ res[len] = 0;
17+ size_t n = fread(res, 1, len, f);
18+ assert(n == len);
19+ fclose(f);
20+ return res;
21+}
22+
23+char* example = "<?xml version=\"1.0\" lang=\"ja\"?><foo><bar baz=\"qux\">quux</bar><bar baz=\"bar\">hoge</bar><piyo>&gt;piyopiyo&lt;</piyo></foo><foo2 />";
24+
25+int main(int argc, char **argv)
26+{
27+ setbuf(stdout, NULL);
28+ char* body = argv[1] == NULL ? example : readFile(argv[1]);
29+ document* doc = hexml_document_parse(body, -1);
30+ char* err = hexml_document_error(doc);
31+ if (err == NULL)
32+ {
33+ int len = hexml_node_render(doc, hexml_document_node(doc), NULL, 0);
34+ char* s = malloc(len + 1);
35+ assert(s);
36+ hexml_node_render(doc, hexml_document_node(doc), s, len);
37+ s[len] = 0;
38+ printf("Parse successful\n"); // , %s\n", s);
39+ return 0;
40+ }
41+ else
42+ {
43+ printf("Parse failed, %s\n", err);
44+ return 1;
45+ }
46+}
gen/.ghcinew file+3
@@ -0,0 +1,3 @@
1+:set -fwarn-unused-binds -fwarn-unused-imports -fwarn-orphans
2+:load Main
3+:def test \xs -> return $ ":main" ++ xs
gen/.ghcidnew file+1
@@ -0,0 +1 @@
1+-c ghci --test=:test --reload=test.c
gen/Lib.hsnew file+201
@@ -0,0 +1,201 @@
1+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2+
3+module Lib(
4+ Parser,
5+ out, many, optional, match, lit, choice, abort, choices,
6+ run,
7+ compile
8+ ) where
9+
10+import Control.Monad.Extra
11+import Control.Monad.Trans.Writer
12+import Data.Maybe
13+import Data.List.Extra
14+import Data.Bits
15+import Data.Monoid
16+import Numeric
17+
18+
19+run :: Parser o () -> String -> ([(Int, o)], (Int, String), Maybe String)
20+run p = runC (toC $ toP p)
21+
22+compile :: Show o => Parser o () -> [String]
23+compile = prettyC . optimiseC . toC . toP
24+
25+---------------------------------------------------------------------
26+-- THE UNDERLYING PARSER TYPE
27+
28+data P o
29+ = Empty
30+ | Seq (P o) (P o)
31+ | Choice (P o) (P o)
32+ | Match CharSet
33+ | Many (P o)
34+ | Out o
35+ | Panic String
36+ deriving Show
37+
38+instance Monoid (P o) where
39+ mempty = Empty
40+ mappend = Seq
41+
42+newtype Parser o a = Parser (Writer (P o) a)
43+ deriving (Functor, Applicative, Monad)
44+
45+toP :: Parser o a -> P o
46+toP (Parser p) = execWriter p
47+
48+out :: o -> Parser o ()
49+out o = Parser $ tell $ Out o
50+
51+abort :: String -> Parser o ()
52+abort x = Parser $ tell $ Panic x
53+
54+many :: Parser o () -> Parser o ()
55+many x = Parser $ tell $ Many $ toP x
56+
57+match :: (Char -> Bool) -> Parser o ()
58+match f = Parser $ tell $ Match $ CharSet f
59+
60+lit :: String -> Parser o ()
61+lit xs = forM_ xs $ \x -> match (== x)
62+
63+choice :: Parser o () -> Parser o () -> Parser o ()
64+choice a b = Parser $ tell $ Choice (toP a) (toP b)
65+
66+choices :: [Parser o ()] -> Parser o ()
67+choices = foldr1 choice
68+
69+optional :: Parser o () -> Parser o ()
70+optional x = choice x $ pure ()
71+
72+
73+---------------------------------------------------------------------
74+-- CHARSET
75+
76+chars = ['\1'..'\xff']
77+
78+newtype CharSet = CharSet {fromCharSet :: Char -> Bool}
79+
80+instance Eq CharSet where
81+ CharSet f1 == CharSet f2 = all (\x -> f1 x == f2 x) chars
82+
83+instance Monoid CharSet where
84+ mempty = CharSet $ const False
85+ mappend (CharSet f1) (CharSet f2) = CharSet $ f1 ||^ f2
86+
87+instance Show CharSet where
88+ show (CharSet p) = "[" ++ f chars ++ "]"
89+ where
90+ showChar x | x `elem` "[]-" = ['\\',x]
91+ | otherwise = init $ tail $ show x
92+ f xs = case dropWhile (not . p) xs of
93+ [] -> []
94+ x:xs -> case span p xs of
95+ ([], rest) -> showChar x ++ f rest :: String
96+ (ys, rest) -> showChar x ++ "-" ++ showChar (last ys) ++ f rest
97+
98+-- | Given we know the first argument are all true, can the second argument ever be False
99+implies :: [CharSet] -> CharSet -> Bool
100+implies known x = all (fromCharSet x) poss
101+ where poss = filter (\c -> all (`fromCharSet` c) known) chars
102+
103+charSetC :: [CharSet] -> ([String], CharSet -> String)
104+charSetC sets =
105+ (["static const char table[256] = {" ++ intercalate "," ("0":map disp chars) ++ "};"]
106+ ,\x -> fromMaybe ("table[(int) *p] & " ++ showBit (bit $ fromJust $ elemIndex x hard)) $ simple x)
107+ where
108+ showBit x = "0x" ++ showHex (x :: Int) ""
109+ disp c = showBit $ foldr (.|.) 0 [bit i | (i, h) <- zipFrom 0 hard, fromCharSet h c]
110+
111+ hard = nub $ filter (isNothing . simple) sets
112+
113+ simple x | x == CharSet (const True) = Just "*p"
114+ | [x] <- filter (fromCharSet x) chars = Just $ "*p == " ++ show x
115+ | otherwise = Nothing
116+
117+---------------------------------------------------------------------
118+-- THE UNDERLYING C TYPE
119+
120+data C o
121+ = While CharSet [C o]
122+ | Stmt o
123+ | Next
124+ | If CharSet [C o] [C o]
125+ | Abort String
126+ deriving Show
127+
128+-- Can you leave this statement successfully having consumed no characters?
129+empty :: P o -> Bool
130+empty Empty = True
131+empty (Choice a b) = empty b
132+empty Many{} = True
133+empty Out{} = True
134+empty Match{} = False
135+empty Panic{} = False
136+empty (Seq a b) = empty a && empty b
137+
138+-- What characters can come next and you commit
139+peek :: P o -> CharSet
140+peek Empty = mempty
141+peek Out{} = mempty
142+peek (Match f) = f
143+peek Panic{} = CharSet $ const True
144+peek (Choice a b) = peek a <> peek b
145+peek (Many x) = peek x
146+peek (Seq a b) = peek a <> (if empty a then peek b else mempty)
147+
148+toC :: P o -> [C o]
149+toC Empty = []
150+toC (Choice a b) = [If (peek a) (toC a) (toC b)]
151+toC (Match f) = [If f [Next] [Abort "Failed to match"]]
152+toC (Many x) = [While (peek x) (toC x)]
153+toC (Out x) = [Stmt x]
154+toC (Panic x) = [Abort x]
155+toC (Seq a b) = toC a ++ toC b
156+
157+runC :: [C o] -> String -> ([(Int, o)], (Int, String), Maybe String)
158+runC = f 0
159+ where
160+ f i [] s = ([], (i, s), Nothing)
161+ f i (Abort x:_) s = ([], (i,s), Just x)
162+ f i (Next:_) [] = ([], (i, []), Just "Next off the end")
163+ f i (Next:r) (x:xs) = f (i+1) r xs
164+ f i (Stmt x:r) xs = let (v1,v2,v3) = f i r xs in ((i,x):v1, v2, v3)
165+ f i (If cond true false : r) xs
166+ | x:_ <- xs, fromCharSet cond x = f i (true ++ r) xs
167+ | otherwise = f i (false ++ r) xs
168+ f i (While cond x:r) xs = f i (If cond (x ++ [While cond x]) [] : r) xs
169+
170+
171+prettyC :: Show o => [C o] -> [String]
172+prettyC xs =
173+ prefix ++
174+ concatMap f xs
175+ where
176+ (prefix, cond) = charSetC $ concatMap charSets xs
177+ charSets (While c x) = c : concatMap charSets x
178+ charSets (If c t f) = c : concatMap charSets t ++ concatMap charSets f
179+ charSets _ = []
180+
181+ fi = map (" "++) . concatMap f
182+ f (Abort x) = ["P_Abort(" ++ show x ++ ");"]
183+ f Next = ["p++;"]
184+ f (Stmt x) = ["P_" ++ show x ++ ";"]
185+ f (If c true []) = ["if (" ++ cond c ++ "){"] ++ fi true ++ ["}"]
186+ f (If c true [false@If{}]) = ["if (" ++ cond c ++ "){"] ++ fi true ++ ["} else " ++ f1] ++ fs
187+ where f1:fs = f false
188+ f (If c true false) = ["if (" ++ cond c ++ "){"] ++ fi true ++ ["} else {"] ++ fi false ++ ["}"]
189+ f (While c x) = ["while (" ++ cond c ++ "){"] ++ fi x ++ ["}"]
190+
191+optimiseC :: [C o] -> [C o]
192+optimiseC = f []
193+ where
194+ f known [] = []
195+ f known (Abort x:_) = [Abort x]
196+ f known (Next:xs) = Next : f [] xs
197+ f known (Stmt x:xs) = Stmt x : f known xs
198+ f known (If cond true false : xs)
199+ | implies known cond = f known $ true ++ xs
200+ | otherwise = If cond (f (cond:known) true) (f known false) : f [] xs -- might be a Next in body
201+ f known (While cond x : xs) = While cond (f (cond:known) x) : f [] xs
gen/Main.hsnew file+70
@@ -0,0 +1,70 @@
1+
2+module Main(main) where
3+
4+import Lib
5+import Parser
6+import Control.Monad.Extra
7+import System.Process.Extra
8+import System.Directory
9+import Data.Maybe
10+import System.IO.Extra
11+import System.FilePath
12+import System.Info.Extra
13+
14+
15+main :: IO ()
16+main = do
17+ forM_ examples $ \(b, src) -> do
18+ putStrLn ""
19+ putStrLn src
20+ let (res, trail, err) = run html src
21+ print res
22+ print (trail, err)
23+ putStrLn $ rerender src res
24+ writeFileChanged "test.h" $ unlines $ compile html
25+ buildRule ["test.h","test.c"] ["gen" <.> (if isWindows then "exe" else "")] $
26+ system_ "gcc test.c -o gen -Werror -Wall -Wextra -DTEST"
27+ system_ "gen"
28+
29+writeFileChanged :: FilePath -> String -> IO ()
30+writeFileChanged file new = do
31+ old <- ifM (doesFileExist file) (readFile' file) (pure "")
32+ when (old /= new) $
33+ writeFile file new
34+
35+buildRule :: [FilePath] -> [FilePath] -> IO () -> IO ()
36+buildRule from to act = do
37+ let modTime x = ifM (doesFileExist x) (Just <$> getModificationTime x) (pure Nothing)
38+ from <- mapM modTime from
39+ to <- mapM modTime to
40+ when (any isNothing (from ++ to) || maximum from >= minimum to)
41+ act
42+
43+examples :: [(Bool, String)]
44+examples = take 1
45+ [(True, "<test foo='123 xyz'><inner /> value</test>")
46+ ,(True, "<test />")
47+ ,(True, "<test id='bob'>here<extra/>there</test>")
48+ ,(True, "<test /><close />")
49+ ,(True, "<test /><!-- comment > --><close />")
50+ ,(True, "<test id=\"bob value\" another-attr=\"test with <\">here </test> more text at the end<close />")
51+ ,(False, "<test></more>")
52+ ,(False, "<test")
53+ ,(True, "<?xml version=\"1.1\"?>\n<greeting>Hello, world!</greeting>")
54+ ]
55+
56+rerender :: String -> [(Int, Out)] -> String
57+rerender o = f
58+ where
59+ sub s e = take (e - s) $ drop s o
60+
61+ f ((_,Tag):(_,TagComment):xs) = f xs
62+ f ((_,Tag):xs) = "<" ++ f xs
63+ f ((_,TagOpen):xs) = ">" ++ f xs
64+ f ((_,TagOpenClose):xs) = "/>" ++ f xs
65+ f ((s,NameStart):(e,NameEnd):(_,TagClose):xs) = "/" ++ sub s e ++ ">" ++ f xs
66+ f ((s,NameStart):(e,NameEnd):xs) = sub s e ++ " " ++ f xs
67+ f ((s,QuoteStart):(e,QuoteEnd):xs) = "=\"" ++ sub s e ++ "\" " ++ f xs
68+ f ((_,AttribsStart):xs) = f xs
69+ f ((_,AttribsEnd):xs) = f xs
70+ f [] = []
gen/Parser.hsnew file+76
@@ -0,0 +1,76 @@
1+
2+module Parser(Out(..), html) where
3+
4+import Lib
5+
6+data Out
7+ = NameStart | NameEnd
8+ | QuoteStart | QuoteEnd
9+ | AttribsStart | AttribsEnd
10+ | Tag
11+ | TagComment | TagOpen | TagClose | TagOpenClose
12+ deriving Show
13+
14+is lower upper x = x >= lower && x <= upper
15+isName1 x = x == ':' || x == '_' || is 'a' 'z' x || is 'A' 'Z' x
16+isName x = isName1 x || x == '-' || x == '.' || is '0' '9' x
17+isSpace x = x == ' ' || x == '\t' || x == '\r' || x == '\n'
18+
19+whitespace = many $ match isSpace
20+
21+name = do
22+ out NameStart
23+ match isName1
24+ many $ match isName
25+ out NameEnd
26+ whitespace
27+
28+attrib = do
29+ name
30+ lit "=" `choice` abort "Expected = in attribute but missing"
31+ whitespace
32+ let quoted x = do
33+ lit [x]
34+ out QuoteStart
35+ many $ match (/= x)
36+ out QuoteEnd
37+ lit [x] `choice` abort ("Couldn't find closing attribute quote (expected " ++ [x] ++ ")")
38+ whitespace
39+ choices
40+ [quoted '\"'
41+ ,quoted '\''
42+ ,abort "Invalid attribute, expected quote (either ' or \")"]
43+
44+attribs = do
45+ out AttribsStart
46+ many attrib
47+ out AttribsEnd
48+
49+tag = do
50+ out Tag
51+ lit "<"
52+ let name_ = name `choice` abort "Missing tag name"
53+ choices
54+ [do lit "!--"
55+ many $ match (/= '-')
56+ -- FIXME: Not correct!!!
57+ lit "-->"
58+ out TagComment
59+ ,do lit "?"
60+ name_
61+ attribs
62+ lit "?>"
63+ out TagOpenClose
64+ ,do lit "/"
65+ name_
66+ lit ">"
67+ out TagClose
68+ ,do name_
69+ attribs
70+ choice
71+ (lit "/>" >> out TagOpenClose)
72+ (lit ">" >> out TagOpen)
73+ ]
74+
75+
76+html = many $ choice tag $ match $ const True
gen/test.cnew file+488
@@ -0,0 +1,488 @@
1+
2+#include "../cbits/hexml.h"
3+#include <stdlib.h>
4+#include <string.h>
5+#include <assert.h>
6+#include <stdio.h>
7+
8+/////////////////////////////////////////////////////////////////////
9+// TYPES
10+
11+static inline int end(str s) { return s.start + s.length; }
12+
13+static inline str start_length(int32_t start, int32_t length)
14+{
15+ assert(start >= 0 && length >= 0);
16+ str res;
17+ res.start = start;
18+ res.length = length;
19+ return res;
20+}
21+
22+static inline str start_end(int32_t start, int32_t end)
23+{
24+ return start_length(start, end - start);
25+}
26+
27+typedef struct
28+{
29+ int size;
30+ int used;
31+ attr* attrs; // dynamically allocated buffer
32+ attr* alloc; // what to call free on
33+} attr_buffer;
34+
35+typedef struct
36+{
37+ // have a cursor at the front, which is all the stuff I have written out, final
38+ // have a cursor at the end, which is stack scoped, so children write out, then I do
39+ // when you commit, you copy over from end to front
40+
41+ // nodes
42+ int size;
43+ int used_front; // front entries, stored for good
44+ int used_back; // back entries, stack based, copied into front on </close>
45+ // back entries which haven't yet been closed have nodes.length == -1
46+ node* nodes; // dynamically allocated buffer
47+ node* alloc; // what to call free on
48+} node_buffer;
49+
50+
51+struct document
52+{
53+ const char* body; // pointer to initial argument, not owned by us
54+ int body_len; // length of the body
55+
56+ char* error_message;
57+ node_buffer nodes;
58+ attr_buffer attrs;
59+};
60+
61+
62+/////////////////////////////////////////////////////////////////////
63+// RENDER CODE
64+
65+typedef struct
66+{
67+ const document* d; // Document, used to get at the body
68+ char* buffer; // Buffer we are writing too
69+ int length; // Length of buffer, do not write past the end.
70+ // No character is reserved for a NULL terminator.
71+ int cursor; // Cursor of where we are writing now
72+} render;
73+
74+static inline void render_char(render* r, char c)
75+{
76+ if (r->cursor < r->length)
77+ r->buffer[r->cursor] = c;
78+ r->cursor++;
79+}
80+
81+static inline void bound(int idx, int mn, int mx)
82+{
83+ assert(idx >= mn && idx <= mx);
84+}
85+
86+static inline void bound_str(str s, int mn, int mx)
87+{
88+ assert(s.length >= 0);
89+ bound(s.start, mn, mx);
90+ bound(end(s), mn, mx);
91+}
92+
93+static void render_str(render* r, str s)
94+{
95+ bound_str(s, 0, r->d->body_len);
96+ for (int i = 0; i < s.length; i++)
97+ render_char(r, r->d->body[s.start + i]);
98+}
99+
100+static void render_tag(render* r, const node* n);
101+
102+static void render_content(render* r, const node* n)
103+{
104+ bound_str(n->inner, 0, r->d->body_len);
105+ bound_str(n->nodes, 0, r->d->nodes.used_front);
106+ bound_str(n->attrs, 0, r->d->attrs.used);
107+
108+ int done = n->inner.start;
109+ for (int i = 0; i < n->nodes.length; i++)
110+ {
111+ node* x = &r->d->nodes.nodes[n->nodes.start + i];
112+ render_str(r, start_end(done, x->outer.start));
113+ done = end(x->outer);
114+ render_tag(r, x);
115+ }
116+ render_str(r, start_end(done, end(n->inner)));
117+}
118+
119+static void render_tag(render* r, const node* n)
120+{
121+ render_char(r, '<');
122+ render_str(r, n->name);
123+ for (int i = 0; i < n->attrs.length; i++)
124+ {
125+ attr* x = &r->d->attrs.attrs[n->attrs.start + i];
126+ render_char(r, ' ');
127+ render_str(r, x->name);
128+ render_char(r, '=');
129+ render_char(r, '\"');
130+ render_str(r, x->value);
131+ render_char(r, '\"');
132+ }
133+ render_char(r, '>');
134+ render_content(r, n);
135+ render_char(r, '<');
136+ render_char(r, '/');
137+ render_str(r, n->name);
138+ render_char(r, '>');
139+}
140+
141+int hexml_node_render(const document* d, const node* n, char* buffer, int length)
142+{
143+ render r;
144+ r.d = d;
145+ r.buffer = buffer;
146+ r.length = length;
147+ r.cursor = 0;
148+ // The root node (and only the root node) has an empty length, so just render its innards
149+ if (n->name.length == 0)
150+ render_content(&r, n);
151+ else
152+ render_tag(&r, n);
153+ return r.cursor;
154+}
155+
156+
157+/////////////////////////////////////////////////////////////////////
158+// NOT THE PARSER
159+
160+char* hexml_document_error(const document* d){return d->error_message;}
161+node* hexml_document_node(const document* d){return &d->nodes.nodes[0];}
162+
163+void hexml_document_free(document* d)
164+{
165+ free(d->error_message);
166+ free(d->nodes.alloc);
167+ free(d->attrs.alloc);
168+ free(d);
169+}
170+
171+node* hexml_node_children(const document* d, const node* n, int* res)
172+{
173+ *res = n->nodes.length;
174+ return &d->nodes.nodes[n->nodes.start];
175+}
176+
177+attr* hexml_node_attributes(const document* d, const node* n, int* res)
178+{
179+ *res = n->attrs.length;
180+ return &d->attrs.attrs[n->attrs.start];
181+}
182+
183+attr* hexml_node_attribute(const document* d, const node* n, const char* s, int slen)
184+{
185+ if (slen == -1) slen = (int) strlen(s);
186+ const int limit = end(n->attrs);
187+ for (int i = n->attrs.start; i < limit; i++)
188+ {
189+ attr* r = &d->attrs.attrs[i];
190+ if (r->name.length == slen && memcmp(s, &d->body[r->name.start], slen) == 0)
191+ return r;
192+ }
193+ return NULL;
194+}
195+
196+// Search for given strings within a node
197+node* hexml_node_child(const document* d, const node* parent, const node* prev, const char* s, int slen)
198+{
199+ if (slen == -1) slen = (int) strlen(s);
200+ int i = prev == NULL ? parent->nodes.start : (int) (prev + 1 - d->nodes.nodes);
201+ const int limit = end(parent->nodes);
202+ for (; i < limit; i++)
203+ {
204+ node* r = &d->nodes.nodes[i];
205+ if (r->name.length == slen && memcmp(s, &d->body[r->name.start], slen) == 0)
206+ return r;
207+ }
208+ return NULL;
209+}
210+
211+/////////////////////////////////////////////////////////////////////
212+// PARSING CODE
213+
214+static inline node* node_alloc(node_buffer* b)
215+{
216+ int space = b->size - b->used_back - b->used_front;
217+ if (space < 1)
218+ {
219+ int size2 = (b->size + 1000) * 2;
220+ node* buf2 = malloc(size2 * sizeof(node));
221+ assert(buf2);
222+ memcpy(buf2, b->nodes, b->used_front * sizeof(node));
223+ memcpy(&buf2[size2 - b->used_back], &b->nodes[b->size - b->used_back], b->used_back * sizeof(node));
224+ free(b->alloc);
225+ b->size = size2;
226+ b->nodes = buf2;
227+ b->alloc = buf2;
228+ }
229+ b->used_back++;
230+ return &b->nodes[b->size - b->used_back];
231+}
232+
233+// buffer[from .. from+n] = reverse (buffer[from .. from+n])
234+// The ranges completely overlap
235+static inline void reverse(node* buffer, int from, int n)
236+{
237+ for (int to = from + n - 1; to > from; from++, to--)
238+ {
239+ node temp = buffer[to];
240+ buffer[to] = buffer[from];
241+ buffer[from] = temp;
242+ }
243+}
244+
245+// buffer[to .. to+n] = reverse (buffer[from .. from+n])
246+// The ranges may not overlap
247+static inline void memcpy_reverse(node* buffer, int from, int to, int n)
248+{
249+ int end = from + n - 1;
250+ for (int i = 0; i < n; i++)
251+ buffer[to+i] = buffer[end-i];
252+}
253+
254+// buffer[to .. to+n] = reverse (buffer[from .. from+n])
255+// The ranges may overlap
256+static inline void memmove_reverse(node* buffer, int from, int to, int n)
257+{
258+ assert(from >= to); // always the case in my usages
259+ if (to + n < from)
260+ memcpy_reverse(buffer, from, to, n);
261+ else
262+ {
263+ int overlap = from - to;
264+ memcpy_reverse(buffer, from, to + n - overlap, overlap);
265+ reverse(buffer, to, n - overlap);
266+ }
267+}
268+
269+static inline str node_commit(node_buffer* b)
270+{
271+ // first search for an "open" node (nodes.length == -1)
272+ int open;
273+ for (open = b->size - b->used_back; open < b->size; open++)
274+ {
275+ if (b->nodes[open].nodes.length == -1)
276+ break;
277+ }
278+
279+ int n = open - (b->size - b->used_back);
280+ memmove_reverse(b->nodes, b->size - b->used_back, b->used_front, n);
281+ b->used_back -= n;
282+ b->used_front += n;
283+ return start_length(b->used_front - n, n);
284+}
285+
286+static inline attr* attr_alloc(attr_buffer* b)
287+{
288+ // Ensure there is at least one node left
289+ int space = b->size - b->used;
290+ if (space < 1)
291+ {
292+ int size2 = (b->size + 1000) * 2;
293+ attr* buf2 = malloc(size2 * sizeof(attr));
294+ assert(buf2);
295+ memcpy(buf2, b->attrs, b->used * sizeof(attr));
296+ free(b->alloc);
297+ b->size = size2;
298+ b->attrs = buf2;
299+ b->alloc = buf2;
300+ }
301+ b->used++;
302+ return &b->attrs[b->used - 1];
303+}
304+
305+static void set_error(document* d, const char* msg)
306+{
307+ if (d->error_message != NULL) return; // keep the first error message
308+ d->error_message = malloc(strlen(msg)+1);
309+ assert(d->error_message);
310+ strcpy(d->error_message, msg);
311+}
312+
313+static inline str gap(const char* ref, const char* start, const char* end)
314+{
315+ return start_end(start - ref, end - ref);
316+}
317+
318+#define P_Abort(x) return x
319+#define P_Tag tag_start = p;
320+#define P_NameStart name_start = p
321+#define P_NameEnd name_end = p
322+#define P_AttribsStart \
323+ me = node_alloc(&d->nodes); \
324+ me->attrs.start = d->attrs.used; \
325+ me->name = gap(d->body, name_start, name_end);
326+#define P_AttribsEnd \
327+ me->attrs = start_end(me->attrs.start, d->attrs.used);
328+#define P_QuoteStart quote_start = p
329+#define P_QuoteEnd \
330+ attr* attr = attr_alloc(&d->attrs); \
331+ attr->name = gap(d->body, name_start, name_end); \
332+ attr->value = gap(d->body, quote_start, p);
333+#define P_TagComment \
334+ // No action required - comments just flow as normal text
335+#define P_TagOpen \
336+ me->nodes.length = -1; \
337+ me->inner = gap(d->body, p, p); \
338+ me->outer = gap(d->body, tag_start, tag_start);
339+#define P_TagClose \
340+ str nodes = node_commit(&d->nodes); \
341+ if (d->nodes.used_back == 0) \
342+ return "Closing tag with no openning"; \
343+ me = &d->nodes.nodes[d->nodes.size - d->nodes.used_back]; \
344+ me->nodes = nodes; \
345+ if (me->name.length != gap(d->body, name_start, name_end).length || \
346+ memcmp(&d->body[me->name.start], name_start, me->name.length) != 0) \
347+ return "Mismatch in closing tag"; \
348+ me->inner = gap(d->body, &d->body[me->inner.start], tag_start); \
349+ me->outer = gap(d->body, &d->body[me->outer.start], p);
350+#define P_TagOpenClose \
351+ me->nodes = start_end(0, 0); \
352+ me->inner = gap(d->body, p, p); \
353+ me->outer = gap(d->body, tag_start, p);
354+
355+// Given the parsed string, return either NULL (success) or an error message (failure)
356+static const char* parser(const char* p, document* d)
357+{
358+ const char *tag_start; // Where the tag starts [<foo
359+ const char *name_start, *name_end; // Where a name is <[foo] or <foo [bar]=
360+ const char *quote_start; // Where an attribute quote is <foo bar='[123'>
361+ node* me; // The current node I'm working on
362+# include "test.h"
363+ return NULL;
364+}
365+
366+// Based on looking at ~50Kb XML documents, they seem to have ~700 attributes
367+// and ~300 nodes, so size appropriately to cope with that.
368+typedef struct
369+{
370+ document document;
371+ attr attrs[1000];
372+ node nodes[500];
373+} buffer;
374+
375+document* hexml_document_parse(const char* s, int slen)
376+{
377+ if (slen == -1) slen = (int) strlen(s);
378+ assert(s[slen] == 0);
379+
380+ buffer* buf = malloc(sizeof(buffer));
381+ assert(buf);
382+ document* d = &buf->document;
383+ d->body = s;
384+ d->body_len = slen;
385+ d->error_message = NULL;
386+ d->attrs.size = 1000;
387+ d->attrs.used = 0;
388+ d->attrs.attrs = buf->attrs;
389+ d->attrs.alloc = NULL;
390+ d->nodes.size = 500;
391+ d->nodes.used_back = 0;
392+ d->nodes.used_front = 1;
393+ d->nodes.nodes = buf->nodes;
394+ d->nodes.alloc = NULL;
395+
396+ d->nodes.nodes[0].name = start_length(0, 0);
397+ d->nodes.nodes[0].outer = start_length(0, slen);
398+ d->nodes.nodes[0].inner = start_length(0, slen);
399+ d->nodes.nodes[0].attrs = start_length(0, 0);
400+
401+ // Introduce an intermediate result, otherwise behaviour is undefined
402+ // because there is no guaranteed ordering between LHS and RHS evaluation
403+ const char* res = parser(s, d);
404+ if (res != NULL)
405+ set_error(d, res);
406+ else
407+ {
408+ d->nodes.nodes[0].nodes = node_commit(&d->nodes);
409+ if (d->nodes.used_back != 0)
410+ d->error_message = strdup("used_back failure");
411+ }
412+ return d;
413+}
414+
415+/////////////////////////////////////////////////////////////////////
416+// TEST HARNESS (temporary)
417+
418+#ifdef TEST
419+
420+static char* resolve(const document* d, str loc)
421+{
422+ char* buf = malloc(loc.length + 3);
423+ memcpy(&buf[1], &d->body[loc.start], loc.length);
424+ buf[0] = '\"';
425+ buf[loc.length+1] = '\"';
426+ buf[loc.length+2] = 0;
427+ return buf;
428+}
429+
430+static char* disp(str loc)
431+{
432+ char* buf = malloc(100);
433+ if (loc.length == 0)
434+ strcpy(buf, "[]");
435+ else
436+ sprintf(buf, "[%i..%i]", loc.start, end(loc)-1);
437+ return buf;
438+}
439+
440+int main()
441+{
442+ setbuf(stdout, NULL);
443+ const char* example = "<test foo='123 xyz' bar='t'><inner /> value</test>";
444+ document* d = hexml_document_parse(example, -1);
445+ if (d->error_message != NULL)
446+ printf("Parse failure: %s\n", d->error_message);
447+ else
448+ {
449+ char buf[1000];
450+ int len = hexml_node_render(d, hexml_document_node(d), buf, sizeof(buf));
451+ buf[len] = 0;
452+ printf("Result = %s\n", buf);
453+ printf("Used nodes = %i, attributes = %i\n", d->nodes.used_front, d->attrs.used);
454+
455+ for (int i = 0; i < d->attrs.used; i++)
456+ printf("Attr %i: %s=%s\n", i, resolve(d, d->attrs.attrs[i].name), resolve(d, d->attrs.attrs[i].value));
457+ for (int i = 0; i < d->nodes.used_front; i++)
458+ {
459+ printf("Node %i: %s attrs=%s, nodes=%s\n", i, resolve(d, d->nodes.nodes[i].name), disp(d->nodes.nodes[i].attrs), disp(d->nodes.nodes[i].nodes));
460+ printf(" inner %s\n outer %s\n", resolve(d, d->nodes.nodes[i].inner), resolve(d, d->nodes.nodes[i].outer));
461+ }
462+ }
463+ hexml_document_free(d);
464+ return 0;
465+}
466+
467+#endif
468+
469+/*
470+void main()
471+{
472+ setbuf(stdout, NULL);
473+ char* body = "<?xml version=\"1.0\" lang=\"ja\"?><foo><bar baz=\"qux\">quux</bar><bar baz=\"bar\">hoge</bar><piyo>&gt;piyopiyo&lt;</piyo></foo><foo2 />";
474+ body = "<test>X</test> ";
475+ document* doc = document_parse(body, -1);
476+ char* err = document_error(doc);
477+ if (err == NULL)
478+ {
479+ int len = document_render(doc, NULL, 0);
480+ char* s = malloc(len + 1);
481+ document_render(doc, s, len);
482+ s[len] = 0;
483+ printf("Parse successful, %s\n", s);
484+ }
485+ else
486+ printf("Parse failed, %s\n", err);
487+}
488+*/
ghci.bat+2−1
@@ -1 +1,2 @@
1−gcc -O3 -std=c99 -c cbits\hexml.c -o cbits\hexml.o && ghci.exe cbits\hexml.o %*
1+gcc -O2 -std=c99 -c cbits\hexml.c -o cbits\hexml.o -Wall -Wextra -Werror && ghci.exe cbits\hexml.o %*
2+rem gcc -O2 -std=c99 -c gen\test.c -o gen\test.o -Wall -Wextra -Werror && ghci.exe gen\test.o %*
go.batdeleted−2
@@ -1,2 +0,0 @@
1−rem sos -p .*.c -c "go 2>&1 | head -n12"
2−gcc -c hexml.c -O3 && gcc -c main.c -O3 && gcc -o hexml hexml.o main.o && hexml
hexml.cabalnew file+55
@@ -0,0 +1,55 @@
1+cabal-version: >= 1.18
2+build-type: Simple
3+name: hexml
4+version: 0.3.4
5+license: BSD3
6+x-license: BSD-3-Clause OR Apache-2.0
7+license-file: LICENSE
8+category: XML
9+author: Neil Mitchell <ndmitchell@gmail.com>
10+maintainer: Neil Mitchell <ndmitchell@gmail.com>
11+copyright: Neil Mitchell 2016-2023
12+synopsis: XML subset DOM parser
13+description:
14+ An XML DOM-style parser, that only parses a subset of XML, but is designed to be fast.
15+homepage: https://github.com/ndmitchell/hexml#readme
16+bug-reports: https://github.com/ndmitchell/hexml/issues
17+tested-with: GHC==9.6, GHC==9.4, GHC==9.2, GHC==9.0, GHC==8.10, GHC==8.8
18+extra-doc-files:
19+ README.md
20+ CHANGES.txt
21+extra-source-files:
22+ cbits/*.h
23+ cbits/*.c
24+
25+source-repository head
26+ type: git
27+ location: https://github.com/ndmitchell/hexml.git
28+
29+library
30+ hs-source-dirs: src
31+ default-language: Haskell2010
32+
33+ build-depends:
34+ base >= 4.5 && < 5,
35+ bytestring,
36+ extra >= 0.3
37+
38+ c-sources: cbits/hexml.c
39+ include-dirs: cbits
40+ includes: hexml.h
41+ install-includes: hexml.h
42+ cc-options: -std=c99
43+
44+ exposed-modules:
45+ Text.XML.Hexml
46+
47+test-suite hexml-test
48+ type: exitcode-stdio-1.0
49+ main-is: src/Main.hs
50+ default-language: Haskell2010
51+
52+ build-depends:
53+ base,
54+ bytestring,
55+ hexml
main.cdeleted−22
@@ -1,22 +0,0 @@
1−#include "hexml.h"
2−#include <stdio.h>
3−#include <malloc.h>
4−
5−void main()
6−{
7− setbuf(stdout, NULL);
8− char* body = "<?xml version=\"1.0\" lang=\"ja\"?><foo><bar baz=\"qux\">quux</bar><bar baz=\"bar\">hoge</bar><piyo>&gt;piyopiyo&lt;</piyo></foo><foo2 />";
9− body = "<test>X</test> ";
10− document* doc = document_parse(body, -1);
11− char* err = document_error(doc);
12− if (err == NULL)
13− {
14− int len = document_render(doc, NULL, 0);
15− char* s = malloc(len + 1);
16− document_render(doc, s, len);
17− s[len] = 0;
18− printf("Parse successful, %s\n", s);
19− }
20− else
21− printf("Parse failed, %s\n", err);
22−}
src/Main.hs+78−6
@@ -1,12 +1,84 @@
1+{-# LANGUAGE OverloadedStrings #-}
12
23module Main(main) where
34
4−import Text.XML.Hexml
5−import qualified Data.ByteString as BS
5+import Text.XML.Hexml as X
6+import qualified Data.ByteString.Char8 as BS
7+import Control.Monad
8+import Data.Monoid
9+import Data.Char
10+import Prelude
11+
12+
13+examples :: [(Bool, BS.ByteString)]
14+examples =
15+ [(True, "<test id='bob'>here<extra/>there</test>")
16+ ,(True, "<test /><close />")
17+ ,(True, "<test /><!-- comment > --><close />")
18+ ,(True, "<test id=\"bob value\" another-attr=\"test with <\">here </test> more text at the end<close />")
19+ ,(False, "<test></more>")
20+ ,(False, "<test")
21+ ,(True, "<?xml version=\"1.1\"?>\n<greeting>Hello, world!</greeting>")
22+ ,(True, "<foo bar.baz=\"qux\"></foo>")
23+ ]
624
725main :: IO ()
826main = do
9− bs <- BS.readFile "C:/Neil/notes/core.xml"
10− case parse bs of
11− Left err -> error $ show err
12− Right val -> print $ render val
27+ forM_ examples $ \(parses, src) ->
28+ case parse src of
29+ Left err -> when parses $ fail $ "Unexpected parse failure, " ++ show err
30+ Right doc -> do
31+ unless parses $ fail "Unexpected parse success"
32+ checkFind doc
33+ let r = render doc
34+ r === rerender doc
35+ let Right d = parse r
36+ r === render d
37+
38+ let Right doc = parse "<test id=\"1\" extra=\"2\" />\n<test id=\"2\" /><b><test id=\"3\" /></b><test id=\"4\" /><test />"
39+ map name (children doc) === ["test","test","b","test","test"]
40+ location (children doc !! 2) === (2,16)
41+ length (childrenBy doc "test") === 4
42+ length (childrenBy doc "b") === 1
43+ length (childrenBy doc "extra") === 0
44+ attributes (head $ children doc) === [Attribute "id" "1", Attribute "extra" "2"]
45+ map (`attributeBy` "id") (childrenBy doc "test") === map (fmap (Attribute "id")) [Just "1", Just "2", Just "4", Nothing]
46+
47+ Right _ <- pure $ parse $ "<test " <> BS.unwords [BS.pack $ "x" ++ show i ++ "='value'" | i <- [1..10000]] <> " />"
48+ Right _ <- pure $ parse $ BS.unlines $ replicate 10000 "<test x='value' />"
49+
50+ let attrs = ["usd:jpy","test","extra","more","stuff","jpy:usd","xxx","xxxx"]
51+ Right doc <- pure $ parse $ "<test " <> BS.unwords [x <> "='" <> x <> "'" | x <- attrs] <> ">middle</test>"
52+ [c] <- pure $ childrenBy doc "test"
53+ forM_ attrs $ \a -> attributeBy c a === Just (Attribute a a)
54+ forM_ ["missing","gone","nothing"] $ \a -> attributeBy c a === Nothing
55+ putStrLn "\nSuccess"
56+
57+
58+checkFind :: Node -> IO ()
59+checkFind n = do
60+ forM_ (attributes n) $ \a -> attributeBy n (attributeName a) === Just a
61+ attributeBy n "xxx" === Nothing
62+ let cs = children n
63+ forM_ ("xxx":map name cs) $ \c ->
64+ map outer (filter ((==) c . name) cs) === map outer (childrenBy n c)
65+ mapM_ checkFind $ children n
66+
67+
68+a === b = if a == b then putChar '.' else fail $ "mismatch, " ++ show a ++ " /= " ++ show b
69+
70+rerender :: Node -> BS.ByteString
71+rerender = inside
72+ where
73+ inside x = BS.concat $ map (either validStr node) $ contents x
74+ node x = "<" <> BS.unwords (validName (name x) : map attr (attributes x)) <> ">" <>
75+ inside x <>
76+ "</" <> name x <> ">"
77+ attr (Attribute a b) = validName a <> "=\"" <> validAttr b <> "\""
78+
79+ validName x | BS.all (\x -> isAlphaNum x || x `elem` ("-.:_" :: String)) x = x
80+ | otherwise = error "Invalid name"
81+ validAttr x | BS.notElem '\"' x = x
82+ | otherwise = error "Invalid attribute"
83+ validStr x | BS.notElem '<' x || BS.isInfixOf "<!--" x = x
84+ | otherwise = error $ show ("Invalid string", x)
src/Text/XML/Hexml.hs+173−74
@@ -1,106 +1,205 @@
1−{-# LANGUAGE RecordWildCards #-}
1+{-# LANGUAGE RecordWildCards, BangPatterns #-}
22
3+-- | A module for fast first-approximation parsing of XML.
4+-- Note that entities, e.g. @&amp;@, are not expanded.
35module Text.XML.Hexml(
4− Document, Node, Attribute(..),
5− parse, render, rootNode,
6− nodeName, nodeInner, nodeOuter,
7− nodeAttributes, nodeChildren, nodeContents,
8− nodeAttributeBy, nodeChildrenBy
6+ Node, Attribute(..),
7+ parse, render,
8+ location, name, inner, outer,
9+ attributes, children, contents,
10+ attributeBy, childrenBy
911 ) where
1012
13+import Control.Applicative
1114import Control.Monad
1215import Data.Int
1316import Foreign.C
1417import Foreign.Ptr
18+import Foreign.Marshal hiding (void)
1519import Foreign.ForeignPtr
1620import Foreign.Storable
1721import System.IO.Unsafe
1822import Data.Monoid
19−import qualified Data.ByteString as BS
23+import Data.Tuple.Extra
24+import qualified Data.ByteString.Char8 as BS
2025import qualified Data.ByteString.Unsafe as BS
2126import qualified Data.ByteString.Internal as BS
27+import Prelude
2228
2329data CDocument
2430data CNode
2531data CAttr
2632
27−data Str = Str {start :: {-# UNPACK #-} !Int32, length :: {-# UNPACK #-} !Int32}
33+szAttr = 2 * sizeOf (undefined :: Str)
34+szNode = 5 * sizeOf (undefined :: Str)
35+
36+
37+data Str = Str {strStart :: {-# UNPACK #-} !Int32, strLength :: {-# UNPACK #-} !Int32} deriving Show
38+
39+strEnd :: Str -> Int32
40+strEnd Str{..} = strStart + strLength
2841
2942instance Storable Str where
3043 sizeOf _ = 8
3144 alignment _ = alignment (0 :: Int64)
3245 peek p = Str <$> peekByteOff p 0 <*> peekByteOff p 4
33− poke p Str{..} = pokeByteOff p 0 start >> pokeByteOff p 4 length
46+ poke p Str{..} = pokeByteOff p 0 strStart >> pokeByteOff p 4 strLength
3447
35−foreign import ccall document_parse :: CString -> CInt -> IO (Ptr CDocument)
36−foreign import ccall document_free :: Ptr CDocument -> IO ()
37−foreign import ccall "&document_free" document_free_funptr :: FunPtr (Ptr CDocument -> IO ())
38−foreign import ccall document_render :: Ptr CDocument -> CString -> CInt -> IO CInt
39−foreign import ccall document_error :: Ptr CDocument -> IO CString
40−foreign import ccall document_node :: Ptr CDocument -> IO (Ptr CNode)
48+foreign import ccall hexml_document_parse :: CString -> CInt -> IO (Ptr CDocument)
49+foreign import ccall hexml_document_free :: Ptr CDocument -> IO ()
50+foreign import ccall "&hexml_document_free" hexml_document_free_funptr :: FunPtr (Ptr CDocument -> IO ())
51+foreign import ccall hexml_node_render :: Ptr CDocument -> Ptr CNode -> CString -> CInt -> IO CInt
52+foreign import ccall unsafe hexml_document_error :: Ptr CDocument -> IO CString
53+foreign import ccall unsafe hexml_document_node :: Ptr CDocument -> IO (Ptr CNode)
4154
42−foreign import ccall node_children :: Ptr CDocument -> Ptr CNode -> Ptr CInt -> Ptr CNode
43−foreign import ccall node_attributes :: Ptr CDocument -> Ptr CNode -> Ptr CInt -> Ptr CAttr
55+foreign import ccall unsafe hexml_node_children :: Ptr CDocument -> Ptr CNode -> Ptr CInt -> IO (Ptr CNode)
56+foreign import ccall unsafe hexml_node_attributes :: Ptr CDocument -> Ptr CNode -> Ptr CInt -> IO (Ptr CAttr)
4457
45−foreign import ccall node_firstChildBy :: Ptr CDocument -> Ptr CNode -> CString -> CInt -> IO (Ptr CNode)
46−foreign import ccall node_nextChildBy :: Ptr CDocument -> Ptr CNode -> Ptr CNode -> CString -> CInt -> IO (Ptr CNode)
47−foreign import ccall node_attributeBy :: Ptr CDocument -> Ptr CNode -> CString -> CInt -> IO (Ptr CAttr)
48−
49−data Document = Document BS.ByteString (ForeignPtr CDocument)
58+foreign import ccall unsafe hexml_node_child :: Ptr CDocument -> Ptr CNode -> Ptr CNode -> CString -> CInt -> IO (Ptr CNode)
59+foreign import ccall unsafe hexml_node_attribute :: Ptr CDocument -> Ptr CNode -> CString -> CInt -> IO (Ptr CAttr)
5060
61+-- | A node in an XML document, created by 'parse', then calling functions such
62+-- as 'children' on that initial 'Node'.
5163data Node = Node BS.ByteString (ForeignPtr CDocument) (Ptr CNode)
5264
53−data Attribute = Attribute BS.ByteString BS.ByteString
54−
55−parse :: BS.ByteString -> Either BS.ByteString Document
56−parse src = unsafePerformIO $ BS.unsafeUseAsCStringLen (src <> BS.singleton 0) $ \(str, len) -> do
57− doc <- document_parse str (fromIntegral len - 1)
58− err <- document_error doc
59− if err /= nullPtr then do
60− bs <- BS.packCString =<< document_error doc
61− document_free doc
62− return $ Left bs
63− else do
64− doc <- newForeignPtr document_free_funptr doc
65− return $ Right $ Document src doc
66−
67−render :: Document -> BS.ByteString
68−render (Document _ doc) = unsafePerformIO $ withForeignPtr doc $ \d -> do
69− i <- document_render d nullPtr 0
70− BS.create (fromIntegral i) $ \ptr -> void $ document_render d (castPtr ptr) i
71−
72−
73−rootNode :: Document -> Node
74−rootNode d@(Document src doc) = unsafePerformIO $ withForeignPtr doc $ \d -> do
75− Node src doc <$> document_node d
65+-- | An XML attribute, comprising of a name and a value. As an example,
66+-- @hello=\"world\"@ would produce @Attribute \"hello\" \"world\"@.
67+data Attribute = Attribute
68+ {attributeName :: BS.ByteString
69+ ,attributeValue :: BS.ByteString
70+ } deriving (Show, Eq, Ord)
71+
72+instance Show Node where
73+ show d = "Node " ++ show (BS.unpack $ outer d)
74+
75+
76+touchBS :: BS.ByteString -> IO ()
77+touchBS = touchForeignPtr . fst3 . BS.toForeignPtr
78+
79+
80+-- | Parse a ByteString as an XML document, returning a 'Left' error message, or a 'Right' document.
81+-- Note that the returned node will have a 'name' of @\"\"@, no 'attributes', and 'contents' as per the document.
82+-- Often the first child will be the @\<?xml ... ?\>@ element. For documents which comprise an XML node and a single
83+-- root element, use @'children' n !! 1@.
84+parse :: BS.ByteString -> Either BS.ByteString Node
85+parse src = do
86+ let src0 = src <> BS.singleton '\0'
87+ unsafePerformIO $ BS.unsafeUseAsCStringLen src0 $ \(str, len) -> do
88+ doc <- hexml_document_parse str (fromIntegral len - 1)
89+ err <- hexml_document_error doc
90+ if err /= nullPtr then do
91+ bs <- BS.packCString =<< hexml_document_error doc
92+ hexml_document_free doc
93+ pure $ Left bs
94+ else do
95+ node <- hexml_document_node doc
96+ doc <- newForeignPtr hexml_document_free_funptr doc
97+ pure $ Right $ Node src0 doc node
98+
99+-- | Given a node, rerender it to something with an equivalent parse tree.
100+-- Mostly useful for debugging - if you want the real source document use 'outer' instead.
101+render :: Node -> BS.ByteString
102+render (Node src doc n) = unsafePerformIO $ withForeignPtr doc $ \d -> do
103+ i <- hexml_node_render d n nullPtr 0
104+ res <- BS.create (fromIntegral i) $ \ptr -> void $ hexml_node_render d n (castPtr ptr) i
105+ touchBS src
106+ pure res
76107
77108applyStr :: BS.ByteString -> Str -> BS.ByteString
78−applyStr bs Str{..} = BS.take (fromIntegral length) $ BS.drop (fromIntegral start) bs
79−
80−nodePeek :: Int -> Node -> BS.ByteString
81−nodePeek i (Node src doc n) = unsafePerformIO $ withForeignPtr doc $ \_ -> do
82− applyStr src <$> peekElemOff (castPtr n) i
83−
84−nodeName :: Node -> BS.ByteString
85−nodeName = nodePeek 0
86−
87−nodeInner :: Node -> BS.ByteString
88−nodeInner = nodePeek 1
89−
90−nodeOuter :: Node -> BS.ByteString
91−nodeOuter = nodePeek 2
92−
93−nodeContents :: Node -> [Either Node BS.ByteString]
94−nodeContents = undefined
95−
96−nodeChildren :: Node -> [Node]
97−nodeChildren = node_children `seq` undefined
98−
99−nodeAttributes :: Node -> [Attribute]
100−nodeAttributes = node_attributes `seq` undefined
101−
102−nodeChildrenBy :: Node -> BS.ByteString -> [Node]
103−nodeChildrenBy = node_firstChildBy `seq` node_nextChildBy `seq` undefined
104−
105−nodeAttributeBy :: Node -> BS.ByteString -> Maybe Attribute
106−nodeAttributeBy = node_attributeBy `seq` undefined
109+applyStr bs Str{..} = BS.take (fromIntegral strLength) $ BS.drop (fromIntegral strStart) bs
110+
111+nodeStr :: Int -> Node -> Str
112+nodeStr i (Node src doc n) = unsafePerformIO $ withForeignPtr doc $ \_ -> peekElemOff (castPtr n) i
113+
114+nodeBS :: Int -> Node -> BS.ByteString
115+nodeBS i node@(Node src doc n) = applyStr src $ nodeStr i node
116+
117+attrPeek :: BS.ByteString -> ForeignPtr CDocument -> Ptr CAttr -> Attribute
118+attrPeek src doc a = unsafePerformIO $ withForeignPtr doc $ \_ -> do
119+ name <- applyStr src <$> peekElemOff (castPtr a) 0
120+ val <- applyStr src <$> peekElemOff (castPtr a) 1
121+ pure $ Attribute name val
122+
123+-- | Get the name of a node, e.g. @\<test /\>@ produces @\"test\"@.
124+name :: Node -> BS.ByteString
125+name = nodeBS 0
126+
127+-- | Get the inner text, from inside the tag, e.g. @\<test /\>@ produces @\"\"@
128+-- and @\<test\>hello\</test\>@ produces @\"hello\"@.
129+-- The result will have identical layout/spacing to the source document.
130+inner :: Node -> BS.ByteString
131+inner = nodeBS 1
132+
133+-- | Get the outer text, including the tag itself, e.g. @\<test /\>@ produces @\"\<test /\>\"@
134+-- and @\<test\>hello\</test\>@ produces @\"\<test\>hello\</test\>\"@.
135+-- The result will have identical layout/spacing to the source document.
136+outer :: Node -> BS.ByteString
137+outer = nodeBS 2
138+
139+-- | Get the contents of a node, including both the content strings (as 'Left', never blank) and
140+-- the direct child nodes (as 'Right').
141+-- If you only want the child nodes, use 'children'.
142+contents :: Node -> [Either BS.ByteString Node]
143+contents n@(Node src _ _) = f (strStart inner) outers
144+ where
145+ f i [] = string i (strEnd inner)
146+ f i ((x, n):xs) = string i (strStart x) ++ Right n : f (strEnd x) xs
147+
148+ string start end | start == end = []
149+ | otherwise = [Left $ applyStr src $ Str start (end - start)]
150+ inner = nodeStr 1 n
151+ outers = map (nodeStr 2 &&& id) $ children n
152+
153+-- | Get the direct child nodes of this node.
154+children :: Node -> [Node]
155+children (Node src doc n) = unsafePerformIO $ withForeignPtr doc $ \d ->
156+ alloca $ \count -> do
157+ res <- hexml_node_children d n count
158+ count <- fromIntegral <$> peek count
159+ pure [Node src doc $ plusPtr res $ i*szNode | i <- [0..count-1]]
160+
161+-- | Get the attributes of this node.
162+attributes :: Node -> [Attribute]
163+attributes (Node src doc n) = unsafePerformIO $ withForeignPtr doc $ \d ->
164+ alloca $ \count -> do
165+ res <- hexml_node_attributes d n count
166+ count <- fromIntegral <$> peek count
167+ pure [attrPeek src doc $ plusPtr res $ i*szAttr | i <- [0..count-1]]
168+
169+-- | Get the direct children of this node which have a specific name.
170+-- A more efficient version of:
171+--
172+-- > childrenBy p s = filter (\n -> name n == s) $ children p
173+childrenBy :: Node -> BS.ByteString -> [Node]
174+childrenBy (Node src doc n) str = go nullPtr
175+ where
176+ go old = unsafePerformIO $ withForeignPtr doc $ \d ->
177+ BS.unsafeUseAsCStringLen str $ \(bs, len) -> do
178+ r <- hexml_node_child d n old bs $ fromIntegral len
179+ touchBS src
180+ pure $ if r == nullPtr then [] else Node src doc r : go r
181+
182+-- | Get the first attribute of this node which has a specific name, if there is one.
183+-- A more efficient version of:
184+--
185+-- > attributeBy n s = listToMaybe $ filter (\(Attribute a _) -> a == s $ attributes n
186+attributeBy :: Node -> BS.ByteString -> Maybe Attribute
187+attributeBy (Node src doc n) str = unsafePerformIO $ withForeignPtr doc $ \d ->
188+ BS.unsafeUseAsCStringLen str $ \(bs, len) -> do
189+ r <- hexml_node_attribute d n bs $ fromIntegral len
190+ touchBS src
191+ pure $ if r == nullPtr then Nothing else Just $ attrPeek src doc r
192+
193+-- | Find the starting location of a node, the @<@ character.
194+-- The first character will be reported as @(line 1,column 1)@, because thats
195+-- how error messages typically do it.
196+location :: Node -> (Int, Int)
197+location n@(Node src _ _) = BS.foldl' f (pair 1 1) $ BS.take (fromIntegral i) src
198+ where
199+ pair !a !b = (a,b)
200+
201+ i = strStart $ nodeStr 2 n
202+ f (!line, !col) c
203+ | c == '\n' = pair (line+1) 1
204+ | c == '\t' = pair line (col+8)
205+ | otherwise = pair line (col+1)
vs/hexml.slnnew file+28
@@ -0,0 +1,28 @@
1+
2+Microsoft Visual Studio Solution File, Format Version 12.00
3+# Visual Studio 14
4+VisualStudioVersion = 14.0.25420.1
5+MinimumVisualStudioVersion = 10.0.40219.1
6+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "hexml", "hexml.vcxproj", "{9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}"
7+EndProject
8+Global
9+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+ Debug|x64 = Debug|x64
11+ Debug|x86 = Debug|x86
12+ Release|x64 = Release|x64
13+ Release|x86 = Release|x86
14+ EndGlobalSection
15+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
16+ {9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}.Debug|x64.ActiveCfg = Debug|x64
17+ {9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}.Debug|x64.Build.0 = Debug|x64
18+ {9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}.Debug|x86.ActiveCfg = Debug|Win32
19+ {9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}.Debug|x86.Build.0 = Debug|Win32
20+ {9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}.Release|x64.ActiveCfg = Release|x64
21+ {9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}.Release|x64.Build.0 = Release|x64
22+ {9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}.Release|x86.ActiveCfg = Release|Win32
23+ {9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}.Release|x86.Build.0 = Release|Win32
24+ EndGlobalSection
25+ GlobalSection(SolutionProperties) = preSolution
26+ HideSolutionNode = FALSE
27+ EndGlobalSection
28+EndGlobal
vs/hexml.vcxprojnew file+153
@@ -0,0 +1,153 @@
1+<?xml version="1.0" encoding="utf-8"?>
2+<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+ <ItemGroup Label="ProjectConfigurations">
4+ <ProjectConfiguration Include="Debug|Win32">
5+ <Configuration>Debug</Configuration>
6+ <Platform>Win32</Platform>
7+ </ProjectConfiguration>
8+ <ProjectConfiguration Include="Release|Win32">
9+ <Configuration>Release</Configuration>
10+ <Platform>Win32</Platform>
11+ </ProjectConfiguration>
12+ <ProjectConfiguration Include="Debug|x64">
13+ <Configuration>Debug</Configuration>
14+ <Platform>x64</Platform>
15+ </ProjectConfiguration>
16+ <ProjectConfiguration Include="Release|x64">
17+ <Configuration>Release</Configuration>
18+ <Platform>x64</Platform>
19+ </ProjectConfiguration>
20+ </ItemGroup>
21+ <ItemGroup>
22+ <ClCompile Include="..\cbits\hexml.c" />
23+ <ClInclude Include="..\cbits\hexml.h" />
24+ <ClCompile Include="..\cbits\main.c" />
25+ </ItemGroup>
26+ <PropertyGroup Label="Globals">
27+ <ProjectGuid>{9DA5F6EE-A35E-4ED6-ABC2-A7707392E6EB}</ProjectGuid>
28+ <Keyword>Win32Proj</Keyword>
29+ <RootNamespace>hexml</RootNamespace>
30+ <WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion>
31+ </PropertyGroup>
32+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
33+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
34+ <ConfigurationType>Application</ConfigurationType>
35+ <UseDebugLibraries>true</UseDebugLibraries>
36+ <PlatformToolset>v140</PlatformToolset>
37+ <CharacterSet>Unicode</CharacterSet>
38+ </PropertyGroup>
39+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
40+ <ConfigurationType>Application</ConfigurationType>
41+ <UseDebugLibraries>false</UseDebugLibraries>
42+ <PlatformToolset>v140</PlatformToolset>
43+ <WholeProgramOptimization>true</WholeProgramOptimization>
44+ <CharacterSet>Unicode</CharacterSet>
45+ </PropertyGroup>
46+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
47+ <ConfigurationType>Application</ConfigurationType>
48+ <UseDebugLibraries>true</UseDebugLibraries>
49+ <PlatformToolset>v140</PlatformToolset>
50+ <CharacterSet>Unicode</CharacterSet>
51+ </PropertyGroup>
52+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
53+ <ConfigurationType>Application</ConfigurationType>
54+ <UseDebugLibraries>false</UseDebugLibraries>
55+ <PlatformToolset>v140</PlatformToolset>
56+ <WholeProgramOptimization>true</WholeProgramOptimization>
57+ <CharacterSet>Unicode</CharacterSet>
58+ </PropertyGroup>
59+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
60+ <ImportGroup Label="ExtensionSettings">
61+ </ImportGroup>
62+ <ImportGroup Label="Shared">
63+ </ImportGroup>
64+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
65+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
66+ </ImportGroup>
67+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
68+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
69+ </ImportGroup>
70+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
71+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
72+ </ImportGroup>
73+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
74+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
75+ </ImportGroup>
76+ <PropertyGroup Label="UserMacros" />
77+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
78+ <LinkIncremental>true</LinkIncremental>
79+ </PropertyGroup>
80+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
81+ <LinkIncremental>true</LinkIncremental>
82+ </PropertyGroup>
83+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
84+ <LinkIncremental>false</LinkIncremental>
85+ </PropertyGroup>
86+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
87+ <LinkIncremental>false</LinkIncremental>
88+ </PropertyGroup>
89+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
90+ <ClCompile>
91+ <PrecompiledHeader>
92+ </PrecompiledHeader>
93+ <WarningLevel>Level3</WarningLevel>
94+ <Optimization>Disabled</Optimization>
95+ <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions);_CRT_SECURE_NO_DEPRECATE</PreprocessorDefinitions>
96+ </ClCompile>
97+ <Link>
98+ <SubSystem>Console</SubSystem>
99+ <GenerateDebugInformation>true</GenerateDebugInformation>
100+ </Link>
101+ </ItemDefinitionGroup>
102+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
103+ <ClCompile>
104+ <PrecompiledHeader>
105+ </PrecompiledHeader>
106+ <WarningLevel>Level3</WarningLevel>
107+ <Optimization>Disabled</Optimization>
108+ <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions);_CRT_SECURE_NO_DEPRECATE</PreprocessorDefinitions>
109+ <AdditionalIncludeDirectories>..\cbits</AdditionalIncludeDirectories>
110+ </ClCompile>
111+ <Link>
112+ <SubSystem>Console</SubSystem>
113+ <GenerateDebugInformation>true</GenerateDebugInformation>
114+ </Link>
115+ </ItemDefinitionGroup>
116+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
117+ <ClCompile>
118+ <WarningLevel>Level3</WarningLevel>
119+ <PrecompiledHeader>
120+ </PrecompiledHeader>
121+ <Optimization>MaxSpeed</Optimization>
122+ <FunctionLevelLinking>true</FunctionLevelLinking>
123+ <IntrinsicFunctions>true</IntrinsicFunctions>
124+ <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
125+ </ClCompile>
126+ <Link>
127+ <SubSystem>Console</SubSystem>
128+ <EnableCOMDATFolding>true</EnableCOMDATFolding>
129+ <OptimizeReferences>true</OptimizeReferences>
130+ <GenerateDebugInformation>true</GenerateDebugInformation>
131+ </Link>
132+ </ItemDefinitionGroup>
133+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
134+ <ClCompile>
135+ <WarningLevel>Level3</WarningLevel>
136+ <PrecompiledHeader>
137+ </PrecompiledHeader>
138+ <Optimization>MaxSpeed</Optimization>
139+ <FunctionLevelLinking>true</FunctionLevelLinking>
140+ <IntrinsicFunctions>true</IntrinsicFunctions>
141+ <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
142+ </ClCompile>
143+ <Link>
144+ <SubSystem>Console</SubSystem>
145+ <EnableCOMDATFolding>true</EnableCOMDATFolding>
146+ <OptimizeReferences>true</OptimizeReferences>
147+ <GenerateDebugInformation>true</GenerateDebugInformation>
148+ </Link>
149+ </ItemDefinitionGroup>
150+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
151+ <ImportGroup Label="ExtensionTargets">
152+ </ImportGroup>
153+</Project>
xml/example.xmlnew file+23
@@ -0,0 +1,23 @@
1+<?xml version="1.0"?>
2+<catalog>
3+ <book id="bk101">
4+ <author>Gambardella, Matthew</author>
5+ <title>XML Developer's Guide</title>
6+ <genre>Computer</genre>
7+ <price>44.95</price>
8+ <publish_date>2000-10-01</publish_date>
9+ <description>An in-depth look at creating applications
10+ with XML.</description>
11+ </book>
12+ <!-- more books -->
13+ <book id="bk102">
14+ <author>Ralls, Kim</author>
15+ <title>Midnight Rain</title>
16+ <genre>Fantasy</genre>
17+ <price>5.95</price>
18+ <publish_date>2000-12-16</publish_date>
19+ <description>A former architect battles corporate zombies,
20+ an evil sorceress, and her own childhood to become queen
21+ of the world.</description>
22+ </book>
23+</catalog>
xml/simple.xmlnew file+2
@@ -0,0 +1,2 @@
1+<a b="c">d</a>
2+