mangrove (empty) → 0.1.0.0
raw patch · 66 files changed
+13076/−0 lines, 66 filesdep +HUnitdep +aesondep +base
Dependencies added: HUnit, aeson, base, bytestring, containers, filepath, mangrove, text, transformers, unordered-containers, utility-ht, vector, willow
Files
- CHANGELOG.md +20/−0
- LICENSE +373/−0
- README.md +21/−0
- README.project.md +127/−0
- data/entities.json +2233/−0
- mangrove.cabal +223/−0
- src/Web/Mangrove/Parse/Common/Character.hs +202/−0
- src/Web/Mangrove/Parse/Common/Error.hs +128/−0
- src/Web/Mangrove/Parse/Encoding/Preprocess.hs +118/−0
- src/Web/Mangrove/Parse/Tokenize.hs +189/−0
- src/Web/Mangrove/Parse/Tokenize/CData.hs +65/−0
- src/Web/Mangrove/Parse/Tokenize/Character.hs +396/−0
- src/Web/Mangrove/Parse/Tokenize/Comment.hs +155/−0
- src/Web/Mangrove/Parse/Tokenize/Common.hs +778/−0
- src/Web/Mangrove/Parse/Tokenize/Data.hs +34/−0
- src/Web/Mangrove/Parse/Tokenize/Dispatcher.hs +42/−0
- src/Web/Mangrove/Parse/Tokenize/Dispatcher.hs-boot +14/−0
- src/Web/Mangrove/Parse/Tokenize/Doctype.hs +383/−0
- src/Web/Mangrove/Parse/Tokenize/PlainText.hs +31/−0
- src/Web/Mangrove/Parse/Tokenize/RCData.hs +35/−0
- src/Web/Mangrove/Parse/Tokenize/RawText.hs +33/−0
- src/Web/Mangrove/Parse/Tokenize/ScriptData.hs +51/−0
- src/Web/Mangrove/Parse/Tokenize/ScriptDataDoubleEscaped.hs +172/−0
- src/Web/Mangrove/Parse/Tokenize/ScriptDataEscaped.hs +103/−0
- src/Web/Mangrove/Parse/Tokenize/Tag.hs +505/−0
- src/Web/Mangrove/Parse/Tree.hs +288/−0
- src/Web/Mangrove/Parse/Tree/AfterAfterBody.hs +47/−0
- src/Web/Mangrove/Parse/Tree/AfterAfterFrameset.hs +40/−0
- src/Web/Mangrove/Parse/Tree/AfterBody.hs +54/−0
- src/Web/Mangrove/Parse/Tree/AfterFrameset.hs +46/−0
- src/Web/Mangrove/Parse/Tree/AfterHead.hs +85/−0
- src/Web/Mangrove/Parse/Tree/BeforeHead.hs +56/−0
- src/Web/Mangrove/Parse/Tree/BeforeHtml.hs +52/−0
- src/Web/Mangrove/Parse/Tree/Common.hs +1258/−0
- src/Web/Mangrove/Parse/Tree/Dispatcher.hs +123/−0
- src/Web/Mangrove/Parse/Tree/Dispatcher.hs-boot +17/−0
- src/Web/Mangrove/Parse/Tree/Foreign.hs +212/−0
- src/Web/Mangrove/Parse/Tree/InBody.hs +971/−0
- src/Web/Mangrove/Parse/Tree/InBody.hs-boot +15/−0
- src/Web/Mangrove/Parse/Tree/InCaption.hs +104/−0
- src/Web/Mangrove/Parse/Tree/InCell.hs +100/−0
- src/Web/Mangrove/Parse/Tree/InColumnGroup.hs +69/−0
- src/Web/Mangrove/Parse/Tree/InFrameset.hs +71/−0
- src/Web/Mangrove/Parse/Tree/InHead.hs +151/−0
- src/Web/Mangrove/Parse/Tree/InHeadNoscript.hs +67/−0
- src/Web/Mangrove/Parse/Tree/InRow.hs +101/−0
- src/Web/Mangrove/Parse/Tree/InSelect.hs +119/−0
- src/Web/Mangrove/Parse/Tree/InSelectInTable.hs +68/−0
- src/Web/Mangrove/Parse/Tree/InTable.hs +167/−0
- src/Web/Mangrove/Parse/Tree/InTableBody.hs +99/−0
- src/Web/Mangrove/Parse/Tree/InTableText.hs +75/−0
- src/Web/Mangrove/Parse/Tree/InTemplate.hs +111/−0
- src/Web/Mangrove/Parse/Tree/InText.hs +88/−0
- src/Web/Mangrove/Parse/Tree/Initial.hs +136/−0
- src/Web/Mangrove/Parse/Tree/Patch.hs +860/−0
- src/Web/Mangrove/Parse/Tree/Patch/Fold.hs +274/−0
- test/Test/Mangrove/Html5Lib.hs +31/−0
- test/Test/Mangrove/Html5Lib/Common.hs +13/−0
- test/Test/Mangrove/Html5Lib/Tokenizer.hs +96/−0
- test/Test/Mangrove/Html5Lib/Tokenizer/JSON.hs +179/−0
- test/Test/Mangrove/Html5Lib/TreeConstruction.hs +68/−0
- test/Test/Mangrove/Html5Lib/TreeConstruction/Parser.hs +208/−0
- test/Test/Mangrove/Unit.hs +28/−0
- test/Test/Mangrove/Unit/Common.hs +23/−0
- test/Test/Mangrove/Unit/Tokenize.hs +26/−0
- test/Test/Mangrove/Unit/Tokenize/Tag.hs +49/−0
+ CHANGELOG.md view
@@ -0,0 +1,20 @@+# Revision history++Version numbering for 'mangrove' follows the [standard Haskell versioning +policy](https://pvp.haskell.org), where the first *two* numbers together +indicate the major version of the package, the third the minor, and the fourth +the patch; a fifth number is optional and indicates a revision purely to the +documentation and/or packaging.+++## v/html/0.1.0.0. -- 2021-04-16++### Added++* f/1.1. Parse HTML(5) documents into a very basic tree (see however the known + issues), or the intermediate character/token streams++### Known issues++* i/1. Handling for some cases of mis-nested tags has not yet been + implemented, throwing an error to terminate parsing
+ LICENSE view
@@ -0,0 +1,373 @@+Mozilla Public License Version 2.0+==================================++1. Definitions+--------------++1.1. "Contributor"+ means each individual or legal entity that creates, contributes to+ the creation of, or owns Covered Software.++1.2. "Contributor Version"+ means the combination of the Contributions of others (if any) used+ by a Contributor and that particular Contributor's Contribution.++1.3. "Contribution"+ means Covered Software of a particular Contributor.++1.4. "Covered Software"+ means Source Code Form to which the initial Contributor has attached+ the notice in Exhibit A, the Executable Form of such Source Code+ Form, and Modifications of such Source Code Form, in each case+ including portions thereof.++1.5. "Incompatible With Secondary Licenses"+ means++ (a) that the initial Contributor has attached the notice described+ in Exhibit B to the Covered Software; or++ (b) that the Covered Software was made available under the terms of+ version 1.1 or earlier of the License, but not also under the+ terms of a Secondary License.++1.6. "Executable Form"+ means any form of the work other than Source Code Form.++1.7. "Larger Work"+ means a work that combines Covered Software with other material, in+ a separate file or files, that is not Covered Software.++1.8. "License"+ means this document.++1.9. "Licensable"+ means having the right to grant, to the maximum extent possible,+ whether at the time of the initial grant or subsequently, any and+ all of the rights conveyed by this License.++1.10. "Modifications"+ means any of the following:++ (a) any file in Source Code Form that results from an addition to,+ deletion from, or modification of the contents of Covered+ Software; or++ (b) any new file in Source Code Form that contains any Covered+ Software.++1.11. "Patent Claims" of a Contributor+ means any patent claim(s), including without limitation, method,+ process, and apparatus claims, in any patent Licensable by such+ Contributor that would be infringed, but for the grant of the+ License, by the making, using, selling, offering for sale, having+ made, import, or transfer of either its Contributions or its+ Contributor Version.++1.12. "Secondary License"+ means either the GNU General Public License, Version 2.0, the GNU+ Lesser General Public License, Version 2.1, the GNU Affero General+ Public License, Version 3.0, or any later versions of those+ licenses.++1.13. "Source Code Form"+ means the form of the work preferred for making modifications.++1.14. "You" (or "Your")+ means an individual or a legal entity exercising rights under this+ License. For legal entities, "You" includes any entity that+ controls, is controlled by, or is under common control with You. For+ purposes of this definition, "control" means (a) the power, direct+ or indirect, to cause the direction or management of such entity,+ whether by contract or otherwise, or (b) ownership of more than+ fifty percent (50%) of the outstanding shares or beneficial+ ownership of such entity.++2. License Grants and Conditions+--------------------------------++2.1. Grants++Each Contributor hereby grants You a world-wide, royalty-free,+non-exclusive license:++(a) under intellectual property rights (other than patent or trademark)+ Licensable by such Contributor to use, reproduce, make available,+ modify, display, perform, distribute, and otherwise exploit its+ Contributions, either on an unmodified basis, with Modifications, or+ as part of a Larger Work; and++(b) under Patent Claims of such Contributor to make, use, sell, offer+ for sale, have made, import, and otherwise transfer either its+ Contributions or its Contributor Version.++2.2. Effective Date++The licenses granted in Section 2.1 with respect to any Contribution+become effective for each Contribution on the date the Contributor first+distributes such Contribution.++2.3. Limitations on Grant Scope++The licenses granted in this Section 2 are the only rights granted under+this License. No additional rights or licenses will be implied from the+distribution or licensing of Covered Software under this License.+Notwithstanding Section 2.1(b) above, no patent license is granted by a+Contributor:++(a) for any code that a Contributor has removed from Covered Software;+ or++(b) for infringements caused by: (i) Your and any other third party's+ modifications of Covered Software, or (ii) the combination of its+ Contributions with other software (except as part of its Contributor+ Version); or++(c) under Patent Claims infringed by Covered Software in the absence of+ its Contributions.++This License does not grant any rights in the trademarks, service marks,+or logos of any Contributor (except as may be necessary to comply with+the notice requirements in Section 3.4).++2.4. Subsequent Licenses++No Contributor makes additional grants as a result of Your choice to+distribute the Covered Software under a subsequent version of this+License (see Section 10.2) or under the terms of a Secondary License (if+permitted under the terms of Section 3.3).++2.5. Representation++Each Contributor represents that the Contributor believes its+Contributions are its original creation(s) or it has sufficient rights+to grant the rights to its Contributions conveyed by this License.++2.6. Fair Use++This License is not intended to limit any rights You have under+applicable copyright doctrines of fair use, fair dealing, or other+equivalents.++2.7. Conditions++Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted+in Section 2.1.++3. Responsibilities+-------------------++3.1. Distribution of Source Form++All distribution of Covered Software in Source Code Form, including any+Modifications that You create or to which You contribute, must be under+the terms of this License. You must inform recipients that the Source+Code Form of the Covered Software is governed by the terms of this+License, and how they can obtain a copy of this License. You may not+attempt to alter or restrict the recipients' rights in the Source Code+Form.++3.2. Distribution of Executable Form++If You distribute Covered Software in Executable Form then:++(a) such Covered Software must also be made available in Source Code+ Form, as described in Section 3.1, and You must inform recipients of+ the Executable Form how they can obtain a copy of such Source Code+ Form by reasonable means in a timely manner, at a charge no more+ than the cost of distribution to the recipient; and++(b) You may distribute such Executable Form under the terms of this+ License, or sublicense it under different terms, provided that the+ license for the Executable Form does not attempt to limit or alter+ the recipients' rights in the Source Code Form under this License.++3.3. Distribution of a Larger Work++You may create and distribute a Larger Work under terms of Your choice,+provided that You also comply with the requirements of this License for+the Covered Software. If the Larger Work is a combination of Covered+Software with a work governed by one or more Secondary Licenses, and the+Covered Software is not Incompatible With Secondary Licenses, this+License permits You to additionally distribute such Covered Software+under the terms of such Secondary License(s), so that the recipient of+the Larger Work may, at their option, further distribute the Covered+Software under the terms of either this License or such Secondary+License(s).++3.4. Notices++You may not remove or alter the substance of any license notices+(including copyright notices, patent notices, disclaimers of warranty,+or limitations of liability) contained within the Source Code Form of+the Covered Software, except that You may alter any license notices to+the extent required to remedy known factual inaccuracies.++3.5. Application of Additional Terms++You may choose to offer, and to charge a fee for, warranty, support,+indemnity or liability obligations to one or more recipients of Covered+Software. However, You may do so only on Your own behalf, and not on+behalf of any Contributor. You must make it absolutely clear that any+such warranty, support, indemnity, or liability obligation is offered by+You alone, and You hereby agree to indemnify every Contributor for any+liability incurred by such Contributor as a result of warranty, support,+indemnity or liability terms You offer. You may include additional+disclaimers of warranty and limitations of liability specific to any+jurisdiction.++4. Inability to Comply Due to Statute or Regulation+---------------------------------------------------++If it is impossible for You to comply with any of the terms of this+License with respect to some or all of the Covered Software due to+statute, judicial order, or regulation then You must: (a) comply with+the terms of this License to the maximum extent possible; and (b)+describe the limitations and the code they affect. Such description must+be placed in a text file included with all distributions of the Covered+Software under this License. Except to the extent prohibited by statute+or regulation, such description must be sufficiently detailed for a+recipient of ordinary skill to be able to understand it.++5. Termination+--------------++5.1. The rights granted under this License will terminate automatically+if You fail to comply with any of its terms. However, if You become+compliant, then the rights granted under this License from a particular+Contributor are reinstated (a) provisionally, unless and until such+Contributor explicitly and finally terminates Your grants, and (b) on an+ongoing basis, if such Contributor fails to notify You of the+non-compliance by some reasonable means prior to 60 days after You have+come back into compliance. Moreover, Your grants from a particular+Contributor are reinstated on an ongoing basis if such Contributor+notifies You of the non-compliance by some reasonable means, this is the+first time You have received notice of non-compliance with this License+from such Contributor, and You become compliant prior to 30 days after+Your receipt of the notice.++5.2. If You initiate litigation against any entity by asserting a patent+infringement claim (excluding declaratory judgment actions,+counter-claims, and cross-claims) alleging that a Contributor Version+directly or indirectly infringes any patent, then the rights granted to+You by any and all Contributors for the Covered Software under Section+2.1 of this License shall terminate.++5.3. In the event of termination under Sections 5.1 or 5.2 above, all+end user license agreements (excluding distributors and resellers) which+have been validly granted by You or Your distributors under this License+prior to termination shall survive termination.++************************************************************************+* *+* 6. Disclaimer of Warranty *+* ------------------------- *+* *+* Covered Software is provided under this License on an "as is" *+* basis, without warranty of any kind, either expressed, implied, or *+* statutory, including, without limitation, warranties that the *+* Covered Software is free of defects, merchantable, fit for a *+* particular purpose or non-infringing. The entire risk as to the *+* quality and performance of the Covered Software is with You. *+* Should any Covered Software prove defective in any respect, You *+* (not any Contributor) assume the cost of any necessary servicing, *+* repair, or correction. This disclaimer of warranty constitutes an *+* essential part of this License. No use of any Covered Software is *+* authorized under this License except under this disclaimer. *+* *+************************************************************************++************************************************************************+* *+* 7. Limitation of Liability *+* -------------------------- *+* *+* Under no circumstances and under no legal theory, whether tort *+* (including negligence), contract, or otherwise, shall any *+* Contributor, or anyone who distributes Covered Software as *+* permitted above, be liable to You for any direct, indirect, *+* special, incidental, or consequential damages of any character *+* including, without limitation, damages for lost profits, loss of *+* goodwill, work stoppage, computer failure or malfunction, or any *+* and all other commercial damages or losses, even if such party *+* shall have been informed of the possibility of such damages. This *+* limitation of liability shall not apply to liability for death or *+* personal injury resulting from such party's negligence to the *+* extent applicable law prohibits such limitation. Some *+* jurisdictions do not allow the exclusion or limitation of *+* incidental or consequential damages, so this exclusion and *+* limitation may not apply to You. *+* *+************************************************************************++8. Litigation+-------------++Any litigation relating to this License may be brought only in the+courts of a jurisdiction where the defendant maintains its principal+place of business and such litigation shall be governed by laws of that+jurisdiction, without reference to its conflict-of-law provisions.+Nothing in this Section shall prevent a party's ability to bring+cross-claims or counter-claims.++9. Miscellaneous+----------------++This License represents the complete agreement concerning the subject+matter hereof. If any provision of this License is held to be+unenforceable, such provision shall be reformed only to the extent+necessary to make it enforceable. Any law or regulation which provides+that the language of a contract shall be construed against the drafter+shall not be used to construe this License against a Contributor.++10. Versions of the License+---------------------------++10.1. New Versions++Mozilla Foundation is the license steward. Except as provided in Section+10.3, no one other than the license steward has the right to modify or+publish new versions of this License. Each version will be given a+distinguishing version number.++10.2. Effect of New Versions++You may distribute the Covered Software under the terms of the version+of the License under which You originally received the Covered Software,+or under the terms of any subsequent version published by the license+steward.++10.3. Modified Versions++If you create software not governed by this License, and you want to+create a new license for such software, you may create and use a+modified version of this License if you rename the license and remove+any references to the name of the license steward (except to note that+such modified license differs from this License).++10.4. Distributing Source Code Form that is Incompatible With Secondary+Licenses++If You choose to distribute Source Code Form that is Incompatible With+Secondary Licenses under the terms of this version of the License, the+notice described in Exhibit B of this License must be attached.++Exhibit A - Source Code Form License Notice+-------------------------------------------++ This Source Code Form is subject to the terms of the Mozilla Public+ License, v. 2.0. If a copy of the MPL was not distributed with this+ file, You can obtain one at http://mozilla.org/MPL/2.0/.++If it is not possible or desirable to put the notice in a particular+file, then You may include the notice in a location (such as a LICENSE+file in a relevant directory) where a recipient would be likely to look+for such a notice.++You may add additional accurate notices of copyright ownership.++Exhibit B - "Incompatible With Secondary Licenses" Notice+---------------------------------------------------------++ This Source Code Form is "Incompatible With Secondary Licenses", as+ defined by the Mozilla Public License, v. 2.0.
+ README.md view
@@ -0,0 +1,21 @@+# About++'mangrove' provides an HTML5-compatible parser for web documents, implemented +in Haskell. In keeping with the immutable data paradigms, an emphasis has been +placed on avoiding side effects and mutable structures rather than strictly +following the official algorithms. The resulting document tree can be returned +to `willow` to be styled and rendered.++This readme is rather sparse, as it has been written for a subfolder of the +complete repository; for full info on the project, see the primary readme in +either [this directory](README.project.md), [its parent](../README.md), or the +[online host](https://code.eitilt.life/src/willow/README.md), whichever of +those links may work.+++# Coverage reporting++Unfortunately, the invocation of hpc by cabal-install <= 3.4.0.0 doesn't work +properly when multiple packages are developed as part of the same project. +Until the next version is released, I recommend that you don't enable coverage +reports for `mangrove`, in order for the tests themselves to run correctly.
+ README.project.md view
@@ -0,0 +1,127 @@+# About++I enjoy working in the terminal, but none of the existing pure-text web +browsers quite provide the full-featured experience I'm looking for; quite good +for retro computing, not as comfortable as current standards of development. +On the graphical side, there are a number of extensions which tack keyboard +controls onto existing browsers, but the resulting join is necessarily a bit +ugly (there *are* a few browsers which have been specifically designed around +the keyboard -- [qutebrowser](https://qutebrowser.org) stands out among them -- +but they all still have their own pain points in turn). So, I'm making my own. +There's no way this will ever be able to compete with the big names[1], but I +hope to gradually grow, one stable step at a time, and eventually reach +something I find comfortable to use.++Instructions for writing other packages using these libraries are generated as +Hackage pages; those can be found in the standard locations (Hackage, the +`hoogle` CLI, `~/.cabal/store/*/share/doc/`, etc.), and are mirrored to the +project web page[2].+++# Cloning++In order to ensure the modules provide all relevant information, this file has +been added to the `extra-source-files` list in addition to the README specific +to that module, but to avoid duplicate patches, the copy/link has not been +added to Darcs. If you have obtained this via `cabal get` or a single-module +tarball, everything should be good. If instead you have cloned this through +Darcs or otherwise downloaded the entire repository, you won't have the +duplicate in the place Cabal expects it to be. That shouldn't be an issue +unless you try packaging it up for some reason, but if you do run into trouble:++ ln README.md mangrove/README.project.md+ ln README.md willow/README.project.md+++# Contribution++Warnings are only useful to developers; while an end user seeing a warning +*might* feel inclined to fix the program, chances are it's just going to be +ignored as the list of built files scrolls by. Reflecting that, a build flag +`dev` has been added to all packages to toggle the printing of warnings, and +potentially other developer aids in the future. If you decide to help improve +this code, please enable it:++ cabal configure -fdev++Depending on the size of your hack, I welcome either a diff file, or you can +bundle your complete darcs patches with `darcs send`. Either way, attach the +changes to an email addressed to ag@eitilt.life and I'll see about adding it to +the codebase.++## Patch format++If you do send the latter, every patch should have a comment with a (at least +mildly) descriptive name prefixed with a tag indicating the general category +addressed by the patch: for fixing issues, that is an "i/" followed by the +issue number (without leading zeros, *unlike* the reference file); for general +development, "f/" followed by the best topic in the `FEATURES` key; for +completeness, version tags are "v/" followed by the package reference followed +by the version. All are terminated with a final period.++Please do not squash patches. Each patch should represent a minimal but +complete change: certainly enough context to successfully compile, hopefully +enough context to not break any previously-passing tests, and potentially +combining closely-related changes at your discretion, but if you wind up trying +to decide between multiple tags or you're adding all of your work over the +entire day, you should probably look closely at whether you can break the patch +apart any farther.++Additionally, make good use of the `--ask-deps` flag. Until I get a CI +integration together, it's easy to miss a dependency, but do your best to +select anything which your patch may require to build successfully.+++## External test suites++Several test suites have already been written against the HTML specification +(including some spillover into other web technology), and I have taken +advantage that previous work to ensure the correctness of the modules here. +Rather than duplicate the storage and have to worry about keeping my mirror +up-to-date, I've instead just set up the tests to look for the repositories in +specific locations, and rely on the programmer to clone them manually.++If possible, these should be saved in the primary data folders; however, as the +test data is shared between multiple packages, a naïve setup will take up a lot +of unnecessary space. If you're only building a single package, then you're +already good and can just use the `data/` folder in this same directory. If +you do have the full repo, Haskell can easily handle symlinks, so I recommend +cloning the data to a single location and then linking to it from the other +package(s); the top-level directory has been used in the examples, but you can +also just choose, e.g., `willow/data/` and link the other packages to that.++### html5lib++The supplemental html5lib conformance suite may be enabled via the Cabal flag +`html5lib`. Before doing so, however, the test data must be downloaded to +`data/test/html5lib-tests`:++ git clone 'https://github.com/html5lib/html5lib-tests' data/test/html5lib-tests++And, unless you only downloaded a single module:++ ln -s data/test/html5lib-tests mangrove/data/test+ ln -s data/test/html5lib-tests willow/data/test++Followed either way by:++ cabal configure --enable-tests -fhtml5lib+ cabal v2-test mangrove:html5lib willow:html5lib++### wpt++Likewise, most of the web-platform-tests conformance suite (Cabal flag `wpt`) +requires the directory `data/test/wpt` to be populated:++ git clone 'https://github.com/web-platform-tests/wpt' data/test/wpt++ ln -s data/test/wpt willow/data/test++ cabal configure --enable-tests -fwpt+ cabal v2-test willow:wpt+++# Footnotes++[1]: https://drewdevault.com/2020/03/18/Reckless-limitless-scope.html+[2]: https://ag.eitilt.life/willow/
+ data/entities.json view
@@ -0,0 +1,2233 @@+{+ "Æ": { "codepoints": [198], "characters": "\u00C6" },+ "Æ": { "codepoints": [198], "characters": "\u00C6" },+ "&": { "codepoints": [38], "characters": "\u0026" },+ "&": { "codepoints": [38], "characters": "\u0026" },+ "Á": { "codepoints": [193], "characters": "\u00C1" },+ "Á": { "codepoints": [193], "characters": "\u00C1" },+ "Ă": { "codepoints": [258], "characters": "\u0102" },+ "Â": { "codepoints": [194], "characters": "\u00C2" },+ "Â": { "codepoints": [194], "characters": "\u00C2" },+ "А": { "codepoints": [1040], "characters": "\u0410" },+ "𝔄": { "codepoints": [120068], "characters": "\uD835\uDD04" },+ "À": { "codepoints": [192], "characters": "\u00C0" },+ "À": { "codepoints": [192], "characters": "\u00C0" },+ "Α": { "codepoints": [913], "characters": "\u0391" },+ "Ā": { "codepoints": [256], "characters": "\u0100" },+ "⩓": { "codepoints": [10835], "characters": "\u2A53" },+ "Ą": { "codepoints": [260], "characters": "\u0104" },+ "𝔸": { "codepoints": [120120], "characters": "\uD835\uDD38" },+ "⁡": { "codepoints": [8289], "characters": "\u2061" },+ "Å": { "codepoints": [197], "characters": "\u00C5" },+ "Å": { "codepoints": [197], "characters": "\u00C5" },+ "𝒜": { "codepoints": [119964], "characters": "\uD835\uDC9C" },+ "≔": { "codepoints": [8788], "characters": "\u2254" },+ "Ã": { "codepoints": [195], "characters": "\u00C3" },+ "Ã": { "codepoints": [195], "characters": "\u00C3" },+ "Ä": { "codepoints": [196], "characters": "\u00C4" },+ "Ä": { "codepoints": [196], "characters": "\u00C4" },+ "∖": { "codepoints": [8726], "characters": "\u2216" },+ "⫧": { "codepoints": [10983], "characters": "\u2AE7" },+ "⌆": { "codepoints": [8966], "characters": "\u2306" },+ "Б": { "codepoints": [1041], "characters": "\u0411" },+ "∵": { "codepoints": [8757], "characters": "\u2235" },+ "ℬ": { "codepoints": [8492], "characters": "\u212C" },+ "Β": { "codepoints": [914], "characters": "\u0392" },+ "𝔅": { "codepoints": [120069], "characters": "\uD835\uDD05" },+ "𝔹": { "codepoints": [120121], "characters": "\uD835\uDD39" },+ "˘": { "codepoints": [728], "characters": "\u02D8" },+ "ℬ": { "codepoints": [8492], "characters": "\u212C" },+ "≎": { "codepoints": [8782], "characters": "\u224E" },+ "Ч": { "codepoints": [1063], "characters": "\u0427" },+ "©": { "codepoints": [169], "characters": "\u00A9" },+ "©": { "codepoints": [169], "characters": "\u00A9" },+ "Ć": { "codepoints": [262], "characters": "\u0106" },+ "⋒": { "codepoints": [8914], "characters": "\u22D2" },+ "ⅅ": { "codepoints": [8517], "characters": "\u2145" },+ "ℭ": { "codepoints": [8493], "characters": "\u212D" },+ "Č": { "codepoints": [268], "characters": "\u010C" },+ "Ç": { "codepoints": [199], "characters": "\u00C7" },+ "Ç": { "codepoints": [199], "characters": "\u00C7" },+ "Ĉ": { "codepoints": [264], "characters": "\u0108" },+ "∰": { "codepoints": [8752], "characters": "\u2230" },+ "Ċ": { "codepoints": [266], "characters": "\u010A" },+ "¸": { "codepoints": [184], "characters": "\u00B8" },+ "·": { "codepoints": [183], "characters": "\u00B7" },+ "ℭ": { "codepoints": [8493], "characters": "\u212D" },+ "Χ": { "codepoints": [935], "characters": "\u03A7" },+ "⊙": { "codepoints": [8857], "characters": "\u2299" },+ "⊖": { "codepoints": [8854], "characters": "\u2296" },+ "⊕": { "codepoints": [8853], "characters": "\u2295" },+ "⊗": { "codepoints": [8855], "characters": "\u2297" },+ "∲": { "codepoints": [8754], "characters": "\u2232" },+ "”": { "codepoints": [8221], "characters": "\u201D" },+ "’": { "codepoints": [8217], "characters": "\u2019" },+ "∷": { "codepoints": [8759], "characters": "\u2237" },+ "⩴": { "codepoints": [10868], "characters": "\u2A74" },+ "≡": { "codepoints": [8801], "characters": "\u2261" },+ "∯": { "codepoints": [8751], "characters": "\u222F" },+ "∮": { "codepoints": [8750], "characters": "\u222E" },+ "ℂ": { "codepoints": [8450], "characters": "\u2102" },+ "∐": { "codepoints": [8720], "characters": "\u2210" },+ "∳": { "codepoints": [8755], "characters": "\u2233" },+ "⨯": { "codepoints": [10799], "characters": "\u2A2F" },+ "𝒞": { "codepoints": [119966], "characters": "\uD835\uDC9E" },+ "⋓": { "codepoints": [8915], "characters": "\u22D3" },+ "≍": { "codepoints": [8781], "characters": "\u224D" },+ "ⅅ": { "codepoints": [8517], "characters": "\u2145" },+ "⤑": { "codepoints": [10513], "characters": "\u2911" },+ "Ђ": { "codepoints": [1026], "characters": "\u0402" },+ "Ѕ": { "codepoints": [1029], "characters": "\u0405" },+ "Џ": { "codepoints": [1039], "characters": "\u040F" },+ "‡": { "codepoints": [8225], "characters": "\u2021" },+ "↡": { "codepoints": [8609], "characters": "\u21A1" },+ "⫤": { "codepoints": [10980], "characters": "\u2AE4" },+ "Ď": { "codepoints": [270], "characters": "\u010E" },+ "Д": { "codepoints": [1044], "characters": "\u0414" },+ "∇": { "codepoints": [8711], "characters": "\u2207" },+ "Δ": { "codepoints": [916], "characters": "\u0394" },+ "𝔇": { "codepoints": [120071], "characters": "\uD835\uDD07" },+ "´": { "codepoints": [180], "characters": "\u00B4" },+ "˙": { "codepoints": [729], "characters": "\u02D9" },+ "˝": { "codepoints": [733], "characters": "\u02DD" },+ "`": { "codepoints": [96], "characters": "\u0060" },+ "˜": { "codepoints": [732], "characters": "\u02DC" },+ "⋄": { "codepoints": [8900], "characters": "\u22C4" },+ "ⅆ": { "codepoints": [8518], "characters": "\u2146" },+ "𝔻": { "codepoints": [120123], "characters": "\uD835\uDD3B" },+ "¨": { "codepoints": [168], "characters": "\u00A8" },+ "⃜": { "codepoints": [8412], "characters": "\u20DC" },+ "≐": { "codepoints": [8784], "characters": "\u2250" },+ "∯": { "codepoints": [8751], "characters": "\u222F" },+ "¨": { "codepoints": [168], "characters": "\u00A8" },+ "⇓": { "codepoints": [8659], "characters": "\u21D3" },+ "⇐": { "codepoints": [8656], "characters": "\u21D0" },+ "⇔": { "codepoints": [8660], "characters": "\u21D4" },+ "⫤": { "codepoints": [10980], "characters": "\u2AE4" },+ "⟸": { "codepoints": [10232], "characters": "\u27F8" },+ "⟺": { "codepoints": [10234], "characters": "\u27FA" },+ "⟹": { "codepoints": [10233], "characters": "\u27F9" },+ "⇒": { "codepoints": [8658], "characters": "\u21D2" },+ "⊨": { "codepoints": [8872], "characters": "\u22A8" },+ "⇑": { "codepoints": [8657], "characters": "\u21D1" },+ "⇕": { "codepoints": [8661], "characters": "\u21D5" },+ "∥": { "codepoints": [8741], "characters": "\u2225" },+ "↓": { "codepoints": [8595], "characters": "\u2193" },+ "⤓": { "codepoints": [10515], "characters": "\u2913" },+ "⇵": { "codepoints": [8693], "characters": "\u21F5" },+ "̑": { "codepoints": [785], "characters": "\u0311" },+ "⥐": { "codepoints": [10576], "characters": "\u2950" },+ "⥞": { "codepoints": [10590], "characters": "\u295E" },+ "↽": { "codepoints": [8637], "characters": "\u21BD" },+ "⥖": { "codepoints": [10582], "characters": "\u2956" },+ "⥟": { "codepoints": [10591], "characters": "\u295F" },+ "⇁": { "codepoints": [8641], "characters": "\u21C1" },+ "⥗": { "codepoints": [10583], "characters": "\u2957" },+ "⊤": { "codepoints": [8868], "characters": "\u22A4" },+ "↧": { "codepoints": [8615], "characters": "\u21A7" },+ "⇓": { "codepoints": [8659], "characters": "\u21D3" },+ "𝒟": { "codepoints": [119967], "characters": "\uD835\uDC9F" },+ "Đ": { "codepoints": [272], "characters": "\u0110" },+ "Ŋ": { "codepoints": [330], "characters": "\u014A" },+ "Ð": { "codepoints": [208], "characters": "\u00D0" },+ "Ð": { "codepoints": [208], "characters": "\u00D0" },+ "É": { "codepoints": [201], "characters": "\u00C9" },+ "É": { "codepoints": [201], "characters": "\u00C9" },+ "Ě": { "codepoints": [282], "characters": "\u011A" },+ "Ê": { "codepoints": [202], "characters": "\u00CA" },+ "Ê": { "codepoints": [202], "characters": "\u00CA" },+ "Э": { "codepoints": [1069], "characters": "\u042D" },+ "Ė": { "codepoints": [278], "characters": "\u0116" },+ "𝔈": { "codepoints": [120072], "characters": "\uD835\uDD08" },+ "È": { "codepoints": [200], "characters": "\u00C8" },+ "È": { "codepoints": [200], "characters": "\u00C8" },+ "∈": { "codepoints": [8712], "characters": "\u2208" },+ "Ē": { "codepoints": [274], "characters": "\u0112" },+ "◻": { "codepoints": [9723], "characters": "\u25FB" },+ "▫": { "codepoints": [9643], "characters": "\u25AB" },+ "Ę": { "codepoints": [280], "characters": "\u0118" },+ "𝔼": { "codepoints": [120124], "characters": "\uD835\uDD3C" },+ "Ε": { "codepoints": [917], "characters": "\u0395" },+ "⩵": { "codepoints": [10869], "characters": "\u2A75" },+ "≂": { "codepoints": [8770], "characters": "\u2242" },+ "⇌": { "codepoints": [8652], "characters": "\u21CC" },+ "ℰ": { "codepoints": [8496], "characters": "\u2130" },+ "⩳": { "codepoints": [10867], "characters": "\u2A73" },+ "Η": { "codepoints": [919], "characters": "\u0397" },+ "Ë": { "codepoints": [203], "characters": "\u00CB" },+ "Ë": { "codepoints": [203], "characters": "\u00CB" },+ "∃": { "codepoints": [8707], "characters": "\u2203" },+ "ⅇ": { "codepoints": [8519], "characters": "\u2147" },+ "Ф": { "codepoints": [1060], "characters": "\u0424" },+ "𝔉": { "codepoints": [120073], "characters": "\uD835\uDD09" },+ "◼": { "codepoints": [9724], "characters": "\u25FC" },+ "▪": { "codepoints": [9642], "characters": "\u25AA" },+ "𝔽": { "codepoints": [120125], "characters": "\uD835\uDD3D" },+ "∀": { "codepoints": [8704], "characters": "\u2200" },+ "ℱ": { "codepoints": [8497], "characters": "\u2131" },+ "ℱ": { "codepoints": [8497], "characters": "\u2131" },+ "Ѓ": { "codepoints": [1027], "characters": "\u0403" },+ ">": { "codepoints": [62], "characters": "\u003E" },+ ">": { "codepoints": [62], "characters": "\u003E" },+ "Γ": { "codepoints": [915], "characters": "\u0393" },+ "Ϝ": { "codepoints": [988], "characters": "\u03DC" },+ "Ğ": { "codepoints": [286], "characters": "\u011E" },+ "Ģ": { "codepoints": [290], "characters": "\u0122" },+ "Ĝ": { "codepoints": [284], "characters": "\u011C" },+ "Г": { "codepoints": [1043], "characters": "\u0413" },+ "Ġ": { "codepoints": [288], "characters": "\u0120" },+ "𝔊": { "codepoints": [120074], "characters": "\uD835\uDD0A" },+ "⋙": { "codepoints": [8921], "characters": "\u22D9" },+ "𝔾": { "codepoints": [120126], "characters": "\uD835\uDD3E" },+ "≥": { "codepoints": [8805], "characters": "\u2265" },+ "⋛": { "codepoints": [8923], "characters": "\u22DB" },+ "≧": { "codepoints": [8807], "characters": "\u2267" },+ "⪢": { "codepoints": [10914], "characters": "\u2AA2" },+ "≷": { "codepoints": [8823], "characters": "\u2277" },+ "⩾": { "codepoints": [10878], "characters": "\u2A7E" },+ "≳": { "codepoints": [8819], "characters": "\u2273" },+ "𝒢": { "codepoints": [119970], "characters": "\uD835\uDCA2" },+ "≫": { "codepoints": [8811], "characters": "\u226B" },+ "Ъ": { "codepoints": [1066], "characters": "\u042A" },+ "ˇ": { "codepoints": [711], "characters": "\u02C7" },+ "^": { "codepoints": [94], "characters": "\u005E" },+ "Ĥ": { "codepoints": [292], "characters": "\u0124" },+ "ℌ": { "codepoints": [8460], "characters": "\u210C" },+ "ℋ": { "codepoints": [8459], "characters": "\u210B" },+ "ℍ": { "codepoints": [8461], "characters": "\u210D" },+ "─": { "codepoints": [9472], "characters": "\u2500" },+ "ℋ": { "codepoints": [8459], "characters": "\u210B" },+ "Ħ": { "codepoints": [294], "characters": "\u0126" },+ "≎": { "codepoints": [8782], "characters": "\u224E" },+ "≏": { "codepoints": [8783], "characters": "\u224F" },+ "Е": { "codepoints": [1045], "characters": "\u0415" },+ "IJ": { "codepoints": [306], "characters": "\u0132" },+ "Ё": { "codepoints": [1025], "characters": "\u0401" },+ "Í": { "codepoints": [205], "characters": "\u00CD" },+ "Í": { "codepoints": [205], "characters": "\u00CD" },+ "Î": { "codepoints": [206], "characters": "\u00CE" },+ "Î": { "codepoints": [206], "characters": "\u00CE" },+ "И": { "codepoints": [1048], "characters": "\u0418" },+ "İ": { "codepoints": [304], "characters": "\u0130" },+ "ℑ": { "codepoints": [8465], "characters": "\u2111" },+ "Ì": { "codepoints": [204], "characters": "\u00CC" },+ "Ì": { "codepoints": [204], "characters": "\u00CC" },+ "ℑ": { "codepoints": [8465], "characters": "\u2111" },+ "Ī": { "codepoints": [298], "characters": "\u012A" },+ "ⅈ": { "codepoints": [8520], "characters": "\u2148" },+ "⇒": { "codepoints": [8658], "characters": "\u21D2" },+ "∬": { "codepoints": [8748], "characters": "\u222C" },+ "∫": { "codepoints": [8747], "characters": "\u222B" },+ "⋂": { "codepoints": [8898], "characters": "\u22C2" },+ "⁣": { "codepoints": [8291], "characters": "\u2063" },+ "⁢": { "codepoints": [8290], "characters": "\u2062" },+ "Į": { "codepoints": [302], "characters": "\u012E" },+ "𝕀": { "codepoints": [120128], "characters": "\uD835\uDD40" },+ "Ι": { "codepoints": [921], "characters": "\u0399" },+ "ℐ": { "codepoints": [8464], "characters": "\u2110" },+ "Ĩ": { "codepoints": [296], "characters": "\u0128" },+ "І": { "codepoints": [1030], "characters": "\u0406" },+ "Ï": { "codepoints": [207], "characters": "\u00CF" },+ "Ï": { "codepoints": [207], "characters": "\u00CF" },+ "Ĵ": { "codepoints": [308], "characters": "\u0134" },+ "Й": { "codepoints": [1049], "characters": "\u0419" },+ "𝔍": { "codepoints": [120077], "characters": "\uD835\uDD0D" },+ "𝕁": { "codepoints": [120129], "characters": "\uD835\uDD41" },+ "𝒥": { "codepoints": [119973], "characters": "\uD835\uDCA5" },+ "Ј": { "codepoints": [1032], "characters": "\u0408" },+ "Є": { "codepoints": [1028], "characters": "\u0404" },+ "Х": { "codepoints": [1061], "characters": "\u0425" },+ "Ќ": { "codepoints": [1036], "characters": "\u040C" },+ "Κ": { "codepoints": [922], "characters": "\u039A" },+ "Ķ": { "codepoints": [310], "characters": "\u0136" },+ "К": { "codepoints": [1050], "characters": "\u041A" },+ "𝔎": { "codepoints": [120078], "characters": "\uD835\uDD0E" },+ "𝕂": { "codepoints": [120130], "characters": "\uD835\uDD42" },+ "𝒦": { "codepoints": [119974], "characters": "\uD835\uDCA6" },+ "Љ": { "codepoints": [1033], "characters": "\u0409" },+ "<": { "codepoints": [60], "characters": "\u003C" },+ "<": { "codepoints": [60], "characters": "\u003C" },+ "Ĺ": { "codepoints": [313], "characters": "\u0139" },+ "Λ": { "codepoints": [923], "characters": "\u039B" },+ "⟪": { "codepoints": [10218], "characters": "\u27EA" },+ "ℒ": { "codepoints": [8466], "characters": "\u2112" },+ "↞": { "codepoints": [8606], "characters": "\u219E" },+ "Ľ": { "codepoints": [317], "characters": "\u013D" },+ "Ļ": { "codepoints": [315], "characters": "\u013B" },+ "Л": { "codepoints": [1051], "characters": "\u041B" },+ "⟨": { "codepoints": [10216], "characters": "\u27E8" },+ "←": { "codepoints": [8592], "characters": "\u2190" },+ "⇤": { "codepoints": [8676], "characters": "\u21E4" },+ "⇆": { "codepoints": [8646], "characters": "\u21C6" },+ "⌈": { "codepoints": [8968], "characters": "\u2308" },+ "⟦": { "codepoints": [10214], "characters": "\u27E6" },+ "⥡": { "codepoints": [10593], "characters": "\u2961" },+ "⇃": { "codepoints": [8643], "characters": "\u21C3" },+ "⥙": { "codepoints": [10585], "characters": "\u2959" },+ "⌊": { "codepoints": [8970], "characters": "\u230A" },+ "↔": { "codepoints": [8596], "characters": "\u2194" },+ "⥎": { "codepoints": [10574], "characters": "\u294E" },+ "⊣": { "codepoints": [8867], "characters": "\u22A3" },+ "↤": { "codepoints": [8612], "characters": "\u21A4" },+ "⥚": { "codepoints": [10586], "characters": "\u295A" },+ "⊲": { "codepoints": [8882], "characters": "\u22B2" },+ "⧏": { "codepoints": [10703], "characters": "\u29CF" },+ "⊴": { "codepoints": [8884], "characters": "\u22B4" },+ "⥑": { "codepoints": [10577], "characters": "\u2951" },+ "⥠": { "codepoints": [10592], "characters": "\u2960" },+ "↿": { "codepoints": [8639], "characters": "\u21BF" },+ "⥘": { "codepoints": [10584], "characters": "\u2958" },+ "↼": { "codepoints": [8636], "characters": "\u21BC" },+ "⥒": { "codepoints": [10578], "characters": "\u2952" },+ "⇐": { "codepoints": [8656], "characters": "\u21D0" },+ "⇔": { "codepoints": [8660], "characters": "\u21D4" },+ "⋚": { "codepoints": [8922], "characters": "\u22DA" },+ "≦": { "codepoints": [8806], "characters": "\u2266" },+ "≶": { "codepoints": [8822], "characters": "\u2276" },+ "⪡": { "codepoints": [10913], "characters": "\u2AA1" },+ "⩽": { "codepoints": [10877], "characters": "\u2A7D" },+ "≲": { "codepoints": [8818], "characters": "\u2272" },+ "𝔏": { "codepoints": [120079], "characters": "\uD835\uDD0F" },+ "⋘": { "codepoints": [8920], "characters": "\u22D8" },+ "⇚": { "codepoints": [8666], "characters": "\u21DA" },+ "Ŀ": { "codepoints": [319], "characters": "\u013F" },+ "⟵": { "codepoints": [10229], "characters": "\u27F5" },+ "⟷": { "codepoints": [10231], "characters": "\u27F7" },+ "⟶": { "codepoints": [10230], "characters": "\u27F6" },+ "⟸": { "codepoints": [10232], "characters": "\u27F8" },+ "⟺": { "codepoints": [10234], "characters": "\u27FA" },+ "⟹": { "codepoints": [10233], "characters": "\u27F9" },+ "𝕃": { "codepoints": [120131], "characters": "\uD835\uDD43" },+ "↙": { "codepoints": [8601], "characters": "\u2199" },+ "↘": { "codepoints": [8600], "characters": "\u2198" },+ "ℒ": { "codepoints": [8466], "characters": "\u2112" },+ "↰": { "codepoints": [8624], "characters": "\u21B0" },+ "Ł": { "codepoints": [321], "characters": "\u0141" },+ "≪": { "codepoints": [8810], "characters": "\u226A" },+ "⤅": { "codepoints": [10501], "characters": "\u2905" },+ "М": { "codepoints": [1052], "characters": "\u041C" },+ " ": { "codepoints": [8287], "characters": "\u205F" },+ "ℳ": { "codepoints": [8499], "characters": "\u2133" },+ "𝔐": { "codepoints": [120080], "characters": "\uD835\uDD10" },+ "∓": { "codepoints": [8723], "characters": "\u2213" },+ "𝕄": { "codepoints": [120132], "characters": "\uD835\uDD44" },+ "ℳ": { "codepoints": [8499], "characters": "\u2133" },+ "Μ": { "codepoints": [924], "characters": "\u039C" },+ "Њ": { "codepoints": [1034], "characters": "\u040A" },+ "Ń": { "codepoints": [323], "characters": "\u0143" },+ "Ň": { "codepoints": [327], "characters": "\u0147" },+ "Ņ": { "codepoints": [325], "characters": "\u0145" },+ "Н": { "codepoints": [1053], "characters": "\u041D" },+ "​": { "codepoints": [8203], "characters": "\u200B" },+ "​": { "codepoints": [8203], "characters": "\u200B" },+ "​": { "codepoints": [8203], "characters": "\u200B" },+ "​": { "codepoints": [8203], "characters": "\u200B" },+ "≫": { "codepoints": [8811], "characters": "\u226B" },+ "≪": { "codepoints": [8810], "characters": "\u226A" },+ "
": { "codepoints": [10], "characters": "\u000A" },+ "𝔑": { "codepoints": [120081], "characters": "\uD835\uDD11" },+ "⁠": { "codepoints": [8288], "characters": "\u2060" },+ " ": { "codepoints": [160], "characters": "\u00A0" },+ "ℕ": { "codepoints": [8469], "characters": "\u2115" },+ "⫬": { "codepoints": [10988], "characters": "\u2AEC" },+ "≢": { "codepoints": [8802], "characters": "\u2262" },+ "≭": { "codepoints": [8813], "characters": "\u226D" },+ "∦": { "codepoints": [8742], "characters": "\u2226" },+ "∉": { "codepoints": [8713], "characters": "\u2209" },+ "≠": { "codepoints": [8800], "characters": "\u2260" },+ "≂̸": { "codepoints": [8770, 824], "characters": "\u2242\u0338" },+ "∄": { "codepoints": [8708], "characters": "\u2204" },+ "≯": { "codepoints": [8815], "characters": "\u226F" },+ "≱": { "codepoints": [8817], "characters": "\u2271" },+ "≧̸": { "codepoints": [8807, 824], "characters": "\u2267\u0338" },+ "≫̸": { "codepoints": [8811, 824], "characters": "\u226B\u0338" },+ "≹": { "codepoints": [8825], "characters": "\u2279" },+ "⩾̸": { "codepoints": [10878, 824], "characters": "\u2A7E\u0338" },+ "≵": { "codepoints": [8821], "characters": "\u2275" },+ "≎̸": { "codepoints": [8782, 824], "characters": "\u224E\u0338" },+ "≏̸": { "codepoints": [8783, 824], "characters": "\u224F\u0338" },+ "⋪": { "codepoints": [8938], "characters": "\u22EA" },+ "⧏̸": { "codepoints": [10703, 824], "characters": "\u29CF\u0338" },+ "⋬": { "codepoints": [8940], "characters": "\u22EC" },+ "≮": { "codepoints": [8814], "characters": "\u226E" },+ "≰": { "codepoints": [8816], "characters": "\u2270" },+ "≸": { "codepoints": [8824], "characters": "\u2278" },+ "≪̸": { "codepoints": [8810, 824], "characters": "\u226A\u0338" },+ "⩽̸": { "codepoints": [10877, 824], "characters": "\u2A7D\u0338" },+ "≴": { "codepoints": [8820], "characters": "\u2274" },+ "⪢̸": { "codepoints": [10914, 824], "characters": "\u2AA2\u0338" },+ "⪡̸": { "codepoints": [10913, 824], "characters": "\u2AA1\u0338" },+ "⊀": { "codepoints": [8832], "characters": "\u2280" },+ "⪯̸": { "codepoints": [10927, 824], "characters": "\u2AAF\u0338" },+ "⋠": { "codepoints": [8928], "characters": "\u22E0" },+ "∌": { "codepoints": [8716], "characters": "\u220C" },+ "⋫": { "codepoints": [8939], "characters": "\u22EB" },+ "⧐̸": { "codepoints": [10704, 824], "characters": "\u29D0\u0338" },+ "⋭": { "codepoints": [8941], "characters": "\u22ED" },+ "⊏̸": { "codepoints": [8847, 824], "characters": "\u228F\u0338" },+ "⋢": { "codepoints": [8930], "characters": "\u22E2" },+ "⊐̸": { "codepoints": [8848, 824], "characters": "\u2290\u0338" },+ "⋣": { "codepoints": [8931], "characters": "\u22E3" },+ "⊂⃒": { "codepoints": [8834, 8402], "characters": "\u2282\u20D2" },+ "⊈": { "codepoints": [8840], "characters": "\u2288" },+ "⊁": { "codepoints": [8833], "characters": "\u2281" },+ "⪰̸": { "codepoints": [10928, 824], "characters": "\u2AB0\u0338" },+ "⋡": { "codepoints": [8929], "characters": "\u22E1" },+ "≿̸": { "codepoints": [8831, 824], "characters": "\u227F\u0338" },+ "⊃⃒": { "codepoints": [8835, 8402], "characters": "\u2283\u20D2" },+ "⊉": { "codepoints": [8841], "characters": "\u2289" },+ "≁": { "codepoints": [8769], "characters": "\u2241" },+ "≄": { "codepoints": [8772], "characters": "\u2244" },+ "≇": { "codepoints": [8775], "characters": "\u2247" },+ "≉": { "codepoints": [8777], "characters": "\u2249" },+ "∤": { "codepoints": [8740], "characters": "\u2224" },+ "𝒩": { "codepoints": [119977], "characters": "\uD835\uDCA9" },+ "Ñ": { "codepoints": [209], "characters": "\u00D1" },+ "Ñ": { "codepoints": [209], "characters": "\u00D1" },+ "Ν": { "codepoints": [925], "characters": "\u039D" },+ "Œ": { "codepoints": [338], "characters": "\u0152" },+ "Ó": { "codepoints": [211], "characters": "\u00D3" },+ "Ó": { "codepoints": [211], "characters": "\u00D3" },+ "Ô": { "codepoints": [212], "characters": "\u00D4" },+ "Ô": { "codepoints": [212], "characters": "\u00D4" },+ "О": { "codepoints": [1054], "characters": "\u041E" },+ "Ő": { "codepoints": [336], "characters": "\u0150" },+ "𝔒": { "codepoints": [120082], "characters": "\uD835\uDD12" },+ "Ò": { "codepoints": [210], "characters": "\u00D2" },+ "Ò": { "codepoints": [210], "characters": "\u00D2" },+ "Ō": { "codepoints": [332], "characters": "\u014C" },+ "Ω": { "codepoints": [937], "characters": "\u03A9" },+ "Ο": { "codepoints": [927], "characters": "\u039F" },+ "𝕆": { "codepoints": [120134], "characters": "\uD835\uDD46" },+ "“": { "codepoints": [8220], "characters": "\u201C" },+ "‘": { "codepoints": [8216], "characters": "\u2018" },+ "⩔": { "codepoints": [10836], "characters": "\u2A54" },+ "𝒪": { "codepoints": [119978], "characters": "\uD835\uDCAA" },+ "Ø": { "codepoints": [216], "characters": "\u00D8" },+ "Ø": { "codepoints": [216], "characters": "\u00D8" },+ "Õ": { "codepoints": [213], "characters": "\u00D5" },+ "Õ": { "codepoints": [213], "characters": "\u00D5" },+ "⨷": { "codepoints": [10807], "characters": "\u2A37" },+ "Ö": { "codepoints": [214], "characters": "\u00D6" },+ "Ö": { "codepoints": [214], "characters": "\u00D6" },+ "‾": { "codepoints": [8254], "characters": "\u203E" },+ "⏞": { "codepoints": [9182], "characters": "\u23DE" },+ "⎴": { "codepoints": [9140], "characters": "\u23B4" },+ "⏜": { "codepoints": [9180], "characters": "\u23DC" },+ "∂": { "codepoints": [8706], "characters": "\u2202" },+ "П": { "codepoints": [1055], "characters": "\u041F" },+ "𝔓": { "codepoints": [120083], "characters": "\uD835\uDD13" },+ "Φ": { "codepoints": [934], "characters": "\u03A6" },+ "Π": { "codepoints": [928], "characters": "\u03A0" },+ "±": { "codepoints": [177], "characters": "\u00B1" },+ "ℌ": { "codepoints": [8460], "characters": "\u210C" },+ "ℙ": { "codepoints": [8473], "characters": "\u2119" },+ "⪻": { "codepoints": [10939], "characters": "\u2ABB" },+ "≺": { "codepoints": [8826], "characters": "\u227A" },+ "⪯": { "codepoints": [10927], "characters": "\u2AAF" },+ "≼": { "codepoints": [8828], "characters": "\u227C" },+ "≾": { "codepoints": [8830], "characters": "\u227E" },+ "″": { "codepoints": [8243], "characters": "\u2033" },+ "∏": { "codepoints": [8719], "characters": "\u220F" },+ "∷": { "codepoints": [8759], "characters": "\u2237" },+ "∝": { "codepoints": [8733], "characters": "\u221D" },+ "𝒫": { "codepoints": [119979], "characters": "\uD835\uDCAB" },+ "Ψ": { "codepoints": [936], "characters": "\u03A8" },+ """: { "codepoints": [34], "characters": "\u0022" },+ """: { "codepoints": [34], "characters": "\u0022" },+ "𝔔": { "codepoints": [120084], "characters": "\uD835\uDD14" },+ "ℚ": { "codepoints": [8474], "characters": "\u211A" },+ "𝒬": { "codepoints": [119980], "characters": "\uD835\uDCAC" },+ "⤐": { "codepoints": [10512], "characters": "\u2910" },+ "®": { "codepoints": [174], "characters": "\u00AE" },+ "®": { "codepoints": [174], "characters": "\u00AE" },+ "Ŕ": { "codepoints": [340], "characters": "\u0154" },+ "⟫": { "codepoints": [10219], "characters": "\u27EB" },+ "↠": { "codepoints": [8608], "characters": "\u21A0" },+ "⤖": { "codepoints": [10518], "characters": "\u2916" },+ "Ř": { "codepoints": [344], "characters": "\u0158" },+ "Ŗ": { "codepoints": [342], "characters": "\u0156" },+ "Р": { "codepoints": [1056], "characters": "\u0420" },+ "ℜ": { "codepoints": [8476], "characters": "\u211C" },+ "∋": { "codepoints": [8715], "characters": "\u220B" },+ "⇋": { "codepoints": [8651], "characters": "\u21CB" },+ "⥯": { "codepoints": [10607], "characters": "\u296F" },+ "ℜ": { "codepoints": [8476], "characters": "\u211C" },+ "Ρ": { "codepoints": [929], "characters": "\u03A1" },+ "⟩": { "codepoints": [10217], "characters": "\u27E9" },+ "→": { "codepoints": [8594], "characters": "\u2192" },+ "⇥": { "codepoints": [8677], "characters": "\u21E5" },+ "⇄": { "codepoints": [8644], "characters": "\u21C4" },+ "⌉": { "codepoints": [8969], "characters": "\u2309" },+ "⟧": { "codepoints": [10215], "characters": "\u27E7" },+ "⥝": { "codepoints": [10589], "characters": "\u295D" },+ "⇂": { "codepoints": [8642], "characters": "\u21C2" },+ "⥕": { "codepoints": [10581], "characters": "\u2955" },+ "⌋": { "codepoints": [8971], "characters": "\u230B" },+ "⊢": { "codepoints": [8866], "characters": "\u22A2" },+ "↦": { "codepoints": [8614], "characters": "\u21A6" },+ "⥛": { "codepoints": [10587], "characters": "\u295B" },+ "⊳": { "codepoints": [8883], "characters": "\u22B3" },+ "⧐": { "codepoints": [10704], "characters": "\u29D0" },+ "⊵": { "codepoints": [8885], "characters": "\u22B5" },+ "⥏": { "codepoints": [10575], "characters": "\u294F" },+ "⥜": { "codepoints": [10588], "characters": "\u295C" },+ "↾": { "codepoints": [8638], "characters": "\u21BE" },+ "⥔": { "codepoints": [10580], "characters": "\u2954" },+ "⇀": { "codepoints": [8640], "characters": "\u21C0" },+ "⥓": { "codepoints": [10579], "characters": "\u2953" },+ "⇒": { "codepoints": [8658], "characters": "\u21D2" },+ "ℝ": { "codepoints": [8477], "characters": "\u211D" },+ "⥰": { "codepoints": [10608], "characters": "\u2970" },+ "⇛": { "codepoints": [8667], "characters": "\u21DB" },+ "ℛ": { "codepoints": [8475], "characters": "\u211B" },+ "↱": { "codepoints": [8625], "characters": "\u21B1" },+ "⧴": { "codepoints": [10740], "characters": "\u29F4" },+ "Щ": { "codepoints": [1065], "characters": "\u0429" },+ "Ш": { "codepoints": [1064], "characters": "\u0428" },+ "Ь": { "codepoints": [1068], "characters": "\u042C" },+ "Ś": { "codepoints": [346], "characters": "\u015A" },+ "⪼": { "codepoints": [10940], "characters": "\u2ABC" },+ "Š": { "codepoints": [352], "characters": "\u0160" },+ "Ş": { "codepoints": [350], "characters": "\u015E" },+ "Ŝ": { "codepoints": [348], "characters": "\u015C" },+ "С": { "codepoints": [1057], "characters": "\u0421" },+ "𝔖": { "codepoints": [120086], "characters": "\uD835\uDD16" },+ "↓": { "codepoints": [8595], "characters": "\u2193" },+ "←": { "codepoints": [8592], "characters": "\u2190" },+ "→": { "codepoints": [8594], "characters": "\u2192" },+ "↑": { "codepoints": [8593], "characters": "\u2191" },+ "Σ": { "codepoints": [931], "characters": "\u03A3" },+ "∘": { "codepoints": [8728], "characters": "\u2218" },+ "𝕊": { "codepoints": [120138], "characters": "\uD835\uDD4A" },+ "√": { "codepoints": [8730], "characters": "\u221A" },+ "□": { "codepoints": [9633], "characters": "\u25A1" },+ "⊓": { "codepoints": [8851], "characters": "\u2293" },+ "⊏": { "codepoints": [8847], "characters": "\u228F" },+ "⊑": { "codepoints": [8849], "characters": "\u2291" },+ "⊐": { "codepoints": [8848], "characters": "\u2290" },+ "⊒": { "codepoints": [8850], "characters": "\u2292" },+ "⊔": { "codepoints": [8852], "characters": "\u2294" },+ "𝒮": { "codepoints": [119982], "characters": "\uD835\uDCAE" },+ "⋆": { "codepoints": [8902], "characters": "\u22C6" },+ "⋐": { "codepoints": [8912], "characters": "\u22D0" },+ "⋐": { "codepoints": [8912], "characters": "\u22D0" },+ "⊆": { "codepoints": [8838], "characters": "\u2286" },+ "≻": { "codepoints": [8827], "characters": "\u227B" },+ "⪰": { "codepoints": [10928], "characters": "\u2AB0" },+ "≽": { "codepoints": [8829], "characters": "\u227D" },+ "≿": { "codepoints": [8831], "characters": "\u227F" },+ "∋": { "codepoints": [8715], "characters": "\u220B" },+ "∑": { "codepoints": [8721], "characters": "\u2211" },+ "⋑": { "codepoints": [8913], "characters": "\u22D1" },+ "⊃": { "codepoints": [8835], "characters": "\u2283" },+ "⊇": { "codepoints": [8839], "characters": "\u2287" },+ "⋑": { "codepoints": [8913], "characters": "\u22D1" },+ "Þ": { "codepoints": [222], "characters": "\u00DE" },+ "Þ": { "codepoints": [222], "characters": "\u00DE" },+ "™": { "codepoints": [8482], "characters": "\u2122" },+ "Ћ": { "codepoints": [1035], "characters": "\u040B" },+ "Ц": { "codepoints": [1062], "characters": "\u0426" },+ "	": { "codepoints": [9], "characters": "\u0009" },+ "Τ": { "codepoints": [932], "characters": "\u03A4" },+ "Ť": { "codepoints": [356], "characters": "\u0164" },+ "Ţ": { "codepoints": [354], "characters": "\u0162" },+ "Т": { "codepoints": [1058], "characters": "\u0422" },+ "𝔗": { "codepoints": [120087], "characters": "\uD835\uDD17" },+ "∴": { "codepoints": [8756], "characters": "\u2234" },+ "Θ": { "codepoints": [920], "characters": "\u0398" },+ "  ": { "codepoints": [8287, 8202], "characters": "\u205F\u200A" },+ " ": { "codepoints": [8201], "characters": "\u2009" },+ "∼": { "codepoints": [8764], "characters": "\u223C" },+ "≃": { "codepoints": [8771], "characters": "\u2243" },+ "≅": { "codepoints": [8773], "characters": "\u2245" },+ "≈": { "codepoints": [8776], "characters": "\u2248" },+ "𝕋": { "codepoints": [120139], "characters": "\uD835\uDD4B" },+ "⃛": { "codepoints": [8411], "characters": "\u20DB" },+ "𝒯": { "codepoints": [119983], "characters": "\uD835\uDCAF" },+ "Ŧ": { "codepoints": [358], "characters": "\u0166" },+ "Ú": { "codepoints": [218], "characters": "\u00DA" },+ "Ú": { "codepoints": [218], "characters": "\u00DA" },+ "↟": { "codepoints": [8607], "characters": "\u219F" },+ "⥉": { "codepoints": [10569], "characters": "\u2949" },+ "Ў": { "codepoints": [1038], "characters": "\u040E" },+ "Ŭ": { "codepoints": [364], "characters": "\u016C" },+ "Û": { "codepoints": [219], "characters": "\u00DB" },+ "Û": { "codepoints": [219], "characters": "\u00DB" },+ "У": { "codepoints": [1059], "characters": "\u0423" },+ "Ű": { "codepoints": [368], "characters": "\u0170" },+ "𝔘": { "codepoints": [120088], "characters": "\uD835\uDD18" },+ "Ù": { "codepoints": [217], "characters": "\u00D9" },+ "Ù": { "codepoints": [217], "characters": "\u00D9" },+ "Ū": { "codepoints": [362], "characters": "\u016A" },+ "_": { "codepoints": [95], "characters": "\u005F" },+ "⏟": { "codepoints": [9183], "characters": "\u23DF" },+ "⎵": { "codepoints": [9141], "characters": "\u23B5" },+ "⏝": { "codepoints": [9181], "characters": "\u23DD" },+ "⋃": { "codepoints": [8899], "characters": "\u22C3" },+ "⊎": { "codepoints": [8846], "characters": "\u228E" },+ "Ų": { "codepoints": [370], "characters": "\u0172" },+ "𝕌": { "codepoints": [120140], "characters": "\uD835\uDD4C" },+ "↑": { "codepoints": [8593], "characters": "\u2191" },+ "⤒": { "codepoints": [10514], "characters": "\u2912" },+ "⇅": { "codepoints": [8645], "characters": "\u21C5" },+ "↕": { "codepoints": [8597], "characters": "\u2195" },+ "⥮": { "codepoints": [10606], "characters": "\u296E" },+ "⊥": { "codepoints": [8869], "characters": "\u22A5" },+ "↥": { "codepoints": [8613], "characters": "\u21A5" },+ "⇑": { "codepoints": [8657], "characters": "\u21D1" },+ "⇕": { "codepoints": [8661], "characters": "\u21D5" },+ "↖": { "codepoints": [8598], "characters": "\u2196" },+ "↗": { "codepoints": [8599], "characters": "\u2197" },+ "ϒ": { "codepoints": [978], "characters": "\u03D2" },+ "Υ": { "codepoints": [933], "characters": "\u03A5" },+ "Ů": { "codepoints": [366], "characters": "\u016E" },+ "𝒰": { "codepoints": [119984], "characters": "\uD835\uDCB0" },+ "Ũ": { "codepoints": [360], "characters": "\u0168" },+ "Ü": { "codepoints": [220], "characters": "\u00DC" },+ "Ü": { "codepoints": [220], "characters": "\u00DC" },+ "⊫": { "codepoints": [8875], "characters": "\u22AB" },+ "⫫": { "codepoints": [10987], "characters": "\u2AEB" },+ "В": { "codepoints": [1042], "characters": "\u0412" },+ "⊩": { "codepoints": [8873], "characters": "\u22A9" },+ "⫦": { "codepoints": [10982], "characters": "\u2AE6" },+ "⋁": { "codepoints": [8897], "characters": "\u22C1" },+ "‖": { "codepoints": [8214], "characters": "\u2016" },+ "‖": { "codepoints": [8214], "characters": "\u2016" },+ "∣": { "codepoints": [8739], "characters": "\u2223" },+ "|": { "codepoints": [124], "characters": "\u007C" },+ "❘": { "codepoints": [10072], "characters": "\u2758" },+ "≀": { "codepoints": [8768], "characters": "\u2240" },+ " ": { "codepoints": [8202], "characters": "\u200A" },+ "𝔙": { "codepoints": [120089], "characters": "\uD835\uDD19" },+ "𝕍": { "codepoints": [120141], "characters": "\uD835\uDD4D" },+ "𝒱": { "codepoints": [119985], "characters": "\uD835\uDCB1" },+ "⊪": { "codepoints": [8874], "characters": "\u22AA" },+ "Ŵ": { "codepoints": [372], "characters": "\u0174" },+ "⋀": { "codepoints": [8896], "characters": "\u22C0" },+ "𝔚": { "codepoints": [120090], "characters": "\uD835\uDD1A" },+ "𝕎": { "codepoints": [120142], "characters": "\uD835\uDD4E" },+ "𝒲": { "codepoints": [119986], "characters": "\uD835\uDCB2" },+ "𝔛": { "codepoints": [120091], "characters": "\uD835\uDD1B" },+ "Ξ": { "codepoints": [926], "characters": "\u039E" },+ "𝕏": { "codepoints": [120143], "characters": "\uD835\uDD4F" },+ "𝒳": { "codepoints": [119987], "characters": "\uD835\uDCB3" },+ "Я": { "codepoints": [1071], "characters": "\u042F" },+ "Ї": { "codepoints": [1031], "characters": "\u0407" },+ "Ю": { "codepoints": [1070], "characters": "\u042E" },+ "Ý": { "codepoints": [221], "characters": "\u00DD" },+ "Ý": { "codepoints": [221], "characters": "\u00DD" },+ "Ŷ": { "codepoints": [374], "characters": "\u0176" },+ "Ы": { "codepoints": [1067], "characters": "\u042B" },+ "𝔜": { "codepoints": [120092], "characters": "\uD835\uDD1C" },+ "𝕐": { "codepoints": [120144], "characters": "\uD835\uDD50" },+ "𝒴": { "codepoints": [119988], "characters": "\uD835\uDCB4" },+ "Ÿ": { "codepoints": [376], "characters": "\u0178" },+ "Ж": { "codepoints": [1046], "characters": "\u0416" },+ "Ź": { "codepoints": [377], "characters": "\u0179" },+ "Ž": { "codepoints": [381], "characters": "\u017D" },+ "З": { "codepoints": [1047], "characters": "\u0417" },+ "Ż": { "codepoints": [379], "characters": "\u017B" },+ "​": { "codepoints": [8203], "characters": "\u200B" },+ "Ζ": { "codepoints": [918], "characters": "\u0396" },+ "ℨ": { "codepoints": [8488], "characters": "\u2128" },+ "ℤ": { "codepoints": [8484], "characters": "\u2124" },+ "𝒵": { "codepoints": [119989], "characters": "\uD835\uDCB5" },+ "á": { "codepoints": [225], "characters": "\u00E1" },+ "á": { "codepoints": [225], "characters": "\u00E1" },+ "ă": { "codepoints": [259], "characters": "\u0103" },+ "∾": { "codepoints": [8766], "characters": "\u223E" },+ "∾̳": { "codepoints": [8766, 819], "characters": "\u223E\u0333" },+ "∿": { "codepoints": [8767], "characters": "\u223F" },+ "â": { "codepoints": [226], "characters": "\u00E2" },+ "â": { "codepoints": [226], "characters": "\u00E2" },+ "´": { "codepoints": [180], "characters": "\u00B4" },+ "´": { "codepoints": [180], "characters": "\u00B4" },+ "а": { "codepoints": [1072], "characters": "\u0430" },+ "æ": { "codepoints": [230], "characters": "\u00E6" },+ "æ": { "codepoints": [230], "characters": "\u00E6" },+ "⁡": { "codepoints": [8289], "characters": "\u2061" },+ "𝔞": { "codepoints": [120094], "characters": "\uD835\uDD1E" },+ "à": { "codepoints": [224], "characters": "\u00E0" },+ "à": { "codepoints": [224], "characters": "\u00E0" },+ "ℵ": { "codepoints": [8501], "characters": "\u2135" },+ "ℵ": { "codepoints": [8501], "characters": "\u2135" },+ "α": { "codepoints": [945], "characters": "\u03B1" },+ "ā": { "codepoints": [257], "characters": "\u0101" },+ "⨿": { "codepoints": [10815], "characters": "\u2A3F" },+ "&": { "codepoints": [38], "characters": "\u0026" },+ "&": { "codepoints": [38], "characters": "\u0026" },+ "∧": { "codepoints": [8743], "characters": "\u2227" },+ "⩕": { "codepoints": [10837], "characters": "\u2A55" },+ "⩜": { "codepoints": [10844], "characters": "\u2A5C" },+ "⩘": { "codepoints": [10840], "characters": "\u2A58" },+ "⩚": { "codepoints": [10842], "characters": "\u2A5A" },+ "∠": { "codepoints": [8736], "characters": "\u2220" },+ "⦤": { "codepoints": [10660], "characters": "\u29A4" },+ "∠": { "codepoints": [8736], "characters": "\u2220" },+ "∡": { "codepoints": [8737], "characters": "\u2221" },+ "⦨": { "codepoints": [10664], "characters": "\u29A8" },+ "⦩": { "codepoints": [10665], "characters": "\u29A9" },+ "⦪": { "codepoints": [10666], "characters": "\u29AA" },+ "⦫": { "codepoints": [10667], "characters": "\u29AB" },+ "⦬": { "codepoints": [10668], "characters": "\u29AC" },+ "⦭": { "codepoints": [10669], "characters": "\u29AD" },+ "⦮": { "codepoints": [10670], "characters": "\u29AE" },+ "⦯": { "codepoints": [10671], "characters": "\u29AF" },+ "∟": { "codepoints": [8735], "characters": "\u221F" },+ "⊾": { "codepoints": [8894], "characters": "\u22BE" },+ "⦝": { "codepoints": [10653], "characters": "\u299D" },+ "∢": { "codepoints": [8738], "characters": "\u2222" },+ "Å": { "codepoints": [197], "characters": "\u00C5" },+ "⍼": { "codepoints": [9084], "characters": "\u237C" },+ "ą": { "codepoints": [261], "characters": "\u0105" },+ "𝕒": { "codepoints": [120146], "characters": "\uD835\uDD52" },+ "≈": { "codepoints": [8776], "characters": "\u2248" },+ "⩰": { "codepoints": [10864], "characters": "\u2A70" },+ "⩯": { "codepoints": [10863], "characters": "\u2A6F" },+ "≊": { "codepoints": [8778], "characters": "\u224A" },+ "≋": { "codepoints": [8779], "characters": "\u224B" },+ "'": { "codepoints": [39], "characters": "\u0027" },+ "≈": { "codepoints": [8776], "characters": "\u2248" },+ "≊": { "codepoints": [8778], "characters": "\u224A" },+ "å": { "codepoints": [229], "characters": "\u00E5" },+ "å": { "codepoints": [229], "characters": "\u00E5" },+ "𝒶": { "codepoints": [119990], "characters": "\uD835\uDCB6" },+ "*": { "codepoints": [42], "characters": "\u002A" },+ "≈": { "codepoints": [8776], "characters": "\u2248" },+ "≍": { "codepoints": [8781], "characters": "\u224D" },+ "ã": { "codepoints": [227], "characters": "\u00E3" },+ "ã": { "codepoints": [227], "characters": "\u00E3" },+ "ä": { "codepoints": [228], "characters": "\u00E4" },+ "ä": { "codepoints": [228], "characters": "\u00E4" },+ "∳": { "codepoints": [8755], "characters": "\u2233" },+ "⨑": { "codepoints": [10769], "characters": "\u2A11" },+ "⫭": { "codepoints": [10989], "characters": "\u2AED" },+ "≌": { "codepoints": [8780], "characters": "\u224C" },+ "϶": { "codepoints": [1014], "characters": "\u03F6" },+ "‵": { "codepoints": [8245], "characters": "\u2035" },+ "∽": { "codepoints": [8765], "characters": "\u223D" },+ "⋍": { "codepoints": [8909], "characters": "\u22CD" },+ "⊽": { "codepoints": [8893], "characters": "\u22BD" },+ "⌅": { "codepoints": [8965], "characters": "\u2305" },+ "⌅": { "codepoints": [8965], "characters": "\u2305" },+ "⎵": { "codepoints": [9141], "characters": "\u23B5" },+ "⎶": { "codepoints": [9142], "characters": "\u23B6" },+ "≌": { "codepoints": [8780], "characters": "\u224C" },+ "б": { "codepoints": [1073], "characters": "\u0431" },+ "„": { "codepoints": [8222], "characters": "\u201E" },+ "∵": { "codepoints": [8757], "characters": "\u2235" },+ "∵": { "codepoints": [8757], "characters": "\u2235" },+ "⦰": { "codepoints": [10672], "characters": "\u29B0" },+ "϶": { "codepoints": [1014], "characters": "\u03F6" },+ "ℬ": { "codepoints": [8492], "characters": "\u212C" },+ "β": { "codepoints": [946], "characters": "\u03B2" },+ "ℶ": { "codepoints": [8502], "characters": "\u2136" },+ "≬": { "codepoints": [8812], "characters": "\u226C" },+ "𝔟": { "codepoints": [120095], "characters": "\uD835\uDD1F" },+ "⋂": { "codepoints": [8898], "characters": "\u22C2" },+ "◯": { "codepoints": [9711], "characters": "\u25EF" },+ "⋃": { "codepoints": [8899], "characters": "\u22C3" },+ "⨀": { "codepoints": [10752], "characters": "\u2A00" },+ "⨁": { "codepoints": [10753], "characters": "\u2A01" },+ "⨂": { "codepoints": [10754], "characters": "\u2A02" },+ "⨆": { "codepoints": [10758], "characters": "\u2A06" },+ "★": { "codepoints": [9733], "characters": "\u2605" },+ "▽": { "codepoints": [9661], "characters": "\u25BD" },+ "△": { "codepoints": [9651], "characters": "\u25B3" },+ "⨄": { "codepoints": [10756], "characters": "\u2A04" },+ "⋁": { "codepoints": [8897], "characters": "\u22C1" },+ "⋀": { "codepoints": [8896], "characters": "\u22C0" },+ "⤍": { "codepoints": [10509], "characters": "\u290D" },+ "⧫": { "codepoints": [10731], "characters": "\u29EB" },+ "▪": { "codepoints": [9642], "characters": "\u25AA" },+ "▴": { "codepoints": [9652], "characters": "\u25B4" },+ "▾": { "codepoints": [9662], "characters": "\u25BE" },+ "◂": { "codepoints": [9666], "characters": "\u25C2" },+ "▸": { "codepoints": [9656], "characters": "\u25B8" },+ "␣": { "codepoints": [9251], "characters": "\u2423" },+ "▒": { "codepoints": [9618], "characters": "\u2592" },+ "░": { "codepoints": [9617], "characters": "\u2591" },+ "▓": { "codepoints": [9619], "characters": "\u2593" },+ "█": { "codepoints": [9608], "characters": "\u2588" },+ "=⃥": { "codepoints": [61, 8421], "characters": "\u003D\u20E5" },+ "≡⃥": { "codepoints": [8801, 8421], "characters": "\u2261\u20E5" },+ "⌐": { "codepoints": [8976], "characters": "\u2310" },+ "𝕓": { "codepoints": [120147], "characters": "\uD835\uDD53" },+ "⊥": { "codepoints": [8869], "characters": "\u22A5" },+ "⊥": { "codepoints": [8869], "characters": "\u22A5" },+ "⋈": { "codepoints": [8904], "characters": "\u22C8" },+ "╗": { "codepoints": [9559], "characters": "\u2557" },+ "╔": { "codepoints": [9556], "characters": "\u2554" },+ "╖": { "codepoints": [9558], "characters": "\u2556" },+ "╓": { "codepoints": [9555], "characters": "\u2553" },+ "═": { "codepoints": [9552], "characters": "\u2550" },+ "╦": { "codepoints": [9574], "characters": "\u2566" },+ "╩": { "codepoints": [9577], "characters": "\u2569" },+ "╤": { "codepoints": [9572], "characters": "\u2564" },+ "╧": { "codepoints": [9575], "characters": "\u2567" },+ "╝": { "codepoints": [9565], "characters": "\u255D" },+ "╚": { "codepoints": [9562], "characters": "\u255A" },+ "╜": { "codepoints": [9564], "characters": "\u255C" },+ "╙": { "codepoints": [9561], "characters": "\u2559" },+ "║": { "codepoints": [9553], "characters": "\u2551" },+ "╬": { "codepoints": [9580], "characters": "\u256C" },+ "╣": { "codepoints": [9571], "characters": "\u2563" },+ "╠": { "codepoints": [9568], "characters": "\u2560" },+ "╫": { "codepoints": [9579], "characters": "\u256B" },+ "╢": { "codepoints": [9570], "characters": "\u2562" },+ "╟": { "codepoints": [9567], "characters": "\u255F" },+ "⧉": { "codepoints": [10697], "characters": "\u29C9" },+ "╕": { "codepoints": [9557], "characters": "\u2555" },+ "╒": { "codepoints": [9554], "characters": "\u2552" },+ "┐": { "codepoints": [9488], "characters": "\u2510" },+ "┌": { "codepoints": [9484], "characters": "\u250C" },+ "─": { "codepoints": [9472], "characters": "\u2500" },+ "╥": { "codepoints": [9573], "characters": "\u2565" },+ "╨": { "codepoints": [9576], "characters": "\u2568" },+ "┬": { "codepoints": [9516], "characters": "\u252C" },+ "┴": { "codepoints": [9524], "characters": "\u2534" },+ "⊟": { "codepoints": [8863], "characters": "\u229F" },+ "⊞": { "codepoints": [8862], "characters": "\u229E" },+ "⊠": { "codepoints": [8864], "characters": "\u22A0" },+ "╛": { "codepoints": [9563], "characters": "\u255B" },+ "╘": { "codepoints": [9560], "characters": "\u2558" },+ "┘": { "codepoints": [9496], "characters": "\u2518" },+ "└": { "codepoints": [9492], "characters": "\u2514" },+ "│": { "codepoints": [9474], "characters": "\u2502" },+ "╪": { "codepoints": [9578], "characters": "\u256A" },+ "╡": { "codepoints": [9569], "characters": "\u2561" },+ "╞": { "codepoints": [9566], "characters": "\u255E" },+ "┼": { "codepoints": [9532], "characters": "\u253C" },+ "┤": { "codepoints": [9508], "characters": "\u2524" },+ "├": { "codepoints": [9500], "characters": "\u251C" },+ "‵": { "codepoints": [8245], "characters": "\u2035" },+ "˘": { "codepoints": [728], "characters": "\u02D8" },+ "¦": { "codepoints": [166], "characters": "\u00A6" },+ "¦": { "codepoints": [166], "characters": "\u00A6" },+ "𝒷": { "codepoints": [119991], "characters": "\uD835\uDCB7" },+ "⁏": { "codepoints": [8271], "characters": "\u204F" },+ "∽": { "codepoints": [8765], "characters": "\u223D" },+ "⋍": { "codepoints": [8909], "characters": "\u22CD" },+ "\": { "codepoints": [92], "characters": "\u005C" },+ "⧅": { "codepoints": [10693], "characters": "\u29C5" },+ "⟈": { "codepoints": [10184], "characters": "\u27C8" },+ "•": { "codepoints": [8226], "characters": "\u2022" },+ "•": { "codepoints": [8226], "characters": "\u2022" },+ "≎": { "codepoints": [8782], "characters": "\u224E" },+ "⪮": { "codepoints": [10926], "characters": "\u2AAE" },+ "≏": { "codepoints": [8783], "characters": "\u224F" },+ "≏": { "codepoints": [8783], "characters": "\u224F" },+ "ć": { "codepoints": [263], "characters": "\u0107" },+ "∩": { "codepoints": [8745], "characters": "\u2229" },+ "⩄": { "codepoints": [10820], "characters": "\u2A44" },+ "⩉": { "codepoints": [10825], "characters": "\u2A49" },+ "⩋": { "codepoints": [10827], "characters": "\u2A4B" },+ "⩇": { "codepoints": [10823], "characters": "\u2A47" },+ "⩀": { "codepoints": [10816], "characters": "\u2A40" },+ "∩︀": { "codepoints": [8745, 65024], "characters": "\u2229\uFE00" },+ "⁁": { "codepoints": [8257], "characters": "\u2041" },+ "ˇ": { "codepoints": [711], "characters": "\u02C7" },+ "⩍": { "codepoints": [10829], "characters": "\u2A4D" },+ "č": { "codepoints": [269], "characters": "\u010D" },+ "ç": { "codepoints": [231], "characters": "\u00E7" },+ "ç": { "codepoints": [231], "characters": "\u00E7" },+ "ĉ": { "codepoints": [265], "characters": "\u0109" },+ "⩌": { "codepoints": [10828], "characters": "\u2A4C" },+ "⩐": { "codepoints": [10832], "characters": "\u2A50" },+ "ċ": { "codepoints": [267], "characters": "\u010B" },+ "¸": { "codepoints": [184], "characters": "\u00B8" },+ "¸": { "codepoints": [184], "characters": "\u00B8" },+ "⦲": { "codepoints": [10674], "characters": "\u29B2" },+ "¢": { "codepoints": [162], "characters": "\u00A2" },+ "¢": { "codepoints": [162], "characters": "\u00A2" },+ "·": { "codepoints": [183], "characters": "\u00B7" },+ "𝔠": { "codepoints": [120096], "characters": "\uD835\uDD20" },+ "ч": { "codepoints": [1095], "characters": "\u0447" },+ "✓": { "codepoints": [10003], "characters": "\u2713" },+ "✓": { "codepoints": [10003], "characters": "\u2713" },+ "χ": { "codepoints": [967], "characters": "\u03C7" },+ "○": { "codepoints": [9675], "characters": "\u25CB" },+ "⧃": { "codepoints": [10691], "characters": "\u29C3" },+ "ˆ": { "codepoints": [710], "characters": "\u02C6" },+ "≗": { "codepoints": [8791], "characters": "\u2257" },+ "↺": { "codepoints": [8634], "characters": "\u21BA" },+ "↻": { "codepoints": [8635], "characters": "\u21BB" },+ "®": { "codepoints": [174], "characters": "\u00AE" },+ "Ⓢ": { "codepoints": [9416], "characters": "\u24C8" },+ "⊛": { "codepoints": [8859], "characters": "\u229B" },+ "⊚": { "codepoints": [8858], "characters": "\u229A" },+ "⊝": { "codepoints": [8861], "characters": "\u229D" },+ "≗": { "codepoints": [8791], "characters": "\u2257" },+ "⨐": { "codepoints": [10768], "characters": "\u2A10" },+ "⫯": { "codepoints": [10991], "characters": "\u2AEF" },+ "⧂": { "codepoints": [10690], "characters": "\u29C2" },+ "♣": { "codepoints": [9827], "characters": "\u2663" },+ "♣": { "codepoints": [9827], "characters": "\u2663" },+ ":": { "codepoints": [58], "characters": "\u003A" },+ "≔": { "codepoints": [8788], "characters": "\u2254" },+ "≔": { "codepoints": [8788], "characters": "\u2254" },+ ",": { "codepoints": [44], "characters": "\u002C" },+ "@": { "codepoints": [64], "characters": "\u0040" },+ "∁": { "codepoints": [8705], "characters": "\u2201" },+ "∘": { "codepoints": [8728], "characters": "\u2218" },+ "∁": { "codepoints": [8705], "characters": "\u2201" },+ "ℂ": { "codepoints": [8450], "characters": "\u2102" },+ "≅": { "codepoints": [8773], "characters": "\u2245" },+ "⩭": { "codepoints": [10861], "characters": "\u2A6D" },+ "∮": { "codepoints": [8750], "characters": "\u222E" },+ "𝕔": { "codepoints": [120148], "characters": "\uD835\uDD54" },+ "∐": { "codepoints": [8720], "characters": "\u2210" },+ "©": { "codepoints": [169], "characters": "\u00A9" },+ "©": { "codepoints": [169], "characters": "\u00A9" },+ "℗": { "codepoints": [8471], "characters": "\u2117" },+ "↵": { "codepoints": [8629], "characters": "\u21B5" },+ "✗": { "codepoints": [10007], "characters": "\u2717" },+ "𝒸": { "codepoints": [119992], "characters": "\uD835\uDCB8" },+ "⫏": { "codepoints": [10959], "characters": "\u2ACF" },+ "⫑": { "codepoints": [10961], "characters": "\u2AD1" },+ "⫐": { "codepoints": [10960], "characters": "\u2AD0" },+ "⫒": { "codepoints": [10962], "characters": "\u2AD2" },+ "⋯": { "codepoints": [8943], "characters": "\u22EF" },+ "⤸": { "codepoints": [10552], "characters": "\u2938" },+ "⤵": { "codepoints": [10549], "characters": "\u2935" },+ "⋞": { "codepoints": [8926], "characters": "\u22DE" },+ "⋟": { "codepoints": [8927], "characters": "\u22DF" },+ "↶": { "codepoints": [8630], "characters": "\u21B6" },+ "⤽": { "codepoints": [10557], "characters": "\u293D" },+ "∪": { "codepoints": [8746], "characters": "\u222A" },+ "⩈": { "codepoints": [10824], "characters": "\u2A48" },+ "⩆": { "codepoints": [10822], "characters": "\u2A46" },+ "⩊": { "codepoints": [10826], "characters": "\u2A4A" },+ "⊍": { "codepoints": [8845], "characters": "\u228D" },+ "⩅": { "codepoints": [10821], "characters": "\u2A45" },+ "∪︀": { "codepoints": [8746, 65024], "characters": "\u222A\uFE00" },+ "↷": { "codepoints": [8631], "characters": "\u21B7" },+ "⤼": { "codepoints": [10556], "characters": "\u293C" },+ "⋞": { "codepoints": [8926], "characters": "\u22DE" },+ "⋟": { "codepoints": [8927], "characters": "\u22DF" },+ "⋎": { "codepoints": [8910], "characters": "\u22CE" },+ "⋏": { "codepoints": [8911], "characters": "\u22CF" },+ "¤": { "codepoints": [164], "characters": "\u00A4" },+ "¤": { "codepoints": [164], "characters": "\u00A4" },+ "↶": { "codepoints": [8630], "characters": "\u21B6" },+ "↷": { "codepoints": [8631], "characters": "\u21B7" },+ "⋎": { "codepoints": [8910], "characters": "\u22CE" },+ "⋏": { "codepoints": [8911], "characters": "\u22CF" },+ "∲": { "codepoints": [8754], "characters": "\u2232" },+ "∱": { "codepoints": [8753], "characters": "\u2231" },+ "⌭": { "codepoints": [9005], "characters": "\u232D" },+ "⇓": { "codepoints": [8659], "characters": "\u21D3" },+ "⥥": { "codepoints": [10597], "characters": "\u2965" },+ "†": { "codepoints": [8224], "characters": "\u2020" },+ "ℸ": { "codepoints": [8504], "characters": "\u2138" },+ "↓": { "codepoints": [8595], "characters": "\u2193" },+ "‐": { "codepoints": [8208], "characters": "\u2010" },+ "⊣": { "codepoints": [8867], "characters": "\u22A3" },+ "⤏": { "codepoints": [10511], "characters": "\u290F" },+ "˝": { "codepoints": [733], "characters": "\u02DD" },+ "ď": { "codepoints": [271], "characters": "\u010F" },+ "д": { "codepoints": [1076], "characters": "\u0434" },+ "ⅆ": { "codepoints": [8518], "characters": "\u2146" },+ "‡": { "codepoints": [8225], "characters": "\u2021" },+ "⇊": { "codepoints": [8650], "characters": "\u21CA" },+ "⩷": { "codepoints": [10871], "characters": "\u2A77" },+ "°": { "codepoints": [176], "characters": "\u00B0" },+ "°": { "codepoints": [176], "characters": "\u00B0" },+ "δ": { "codepoints": [948], "characters": "\u03B4" },+ "⦱": { "codepoints": [10673], "characters": "\u29B1" },+ "⥿": { "codepoints": [10623], "characters": "\u297F" },+ "𝔡": { "codepoints": [120097], "characters": "\uD835\uDD21" },+ "⇃": { "codepoints": [8643], "characters": "\u21C3" },+ "⇂": { "codepoints": [8642], "characters": "\u21C2" },+ "⋄": { "codepoints": [8900], "characters": "\u22C4" },+ "⋄": { "codepoints": [8900], "characters": "\u22C4" },+ "♦": { "codepoints": [9830], "characters": "\u2666" },+ "♦": { "codepoints": [9830], "characters": "\u2666" },+ "¨": { "codepoints": [168], "characters": "\u00A8" },+ "ϝ": { "codepoints": [989], "characters": "\u03DD" },+ "⋲": { "codepoints": [8946], "characters": "\u22F2" },+ "÷": { "codepoints": [247], "characters": "\u00F7" },+ "÷": { "codepoints": [247], "characters": "\u00F7" },+ "÷": { "codepoints": [247], "characters": "\u00F7" },+ "⋇": { "codepoints": [8903], "characters": "\u22C7" },+ "⋇": { "codepoints": [8903], "characters": "\u22C7" },+ "ђ": { "codepoints": [1106], "characters": "\u0452" },+ "⌞": { "codepoints": [8990], "characters": "\u231E" },+ "⌍": { "codepoints": [8973], "characters": "\u230D" },+ "$": { "codepoints": [36], "characters": "\u0024" },+ "𝕕": { "codepoints": [120149], "characters": "\uD835\uDD55" },+ "˙": { "codepoints": [729], "characters": "\u02D9" },+ "≐": { "codepoints": [8784], "characters": "\u2250" },+ "≑": { "codepoints": [8785], "characters": "\u2251" },+ "∸": { "codepoints": [8760], "characters": "\u2238" },+ "∔": { "codepoints": [8724], "characters": "\u2214" },+ "⊡": { "codepoints": [8865], "characters": "\u22A1" },+ "⌆": { "codepoints": [8966], "characters": "\u2306" },+ "↓": { "codepoints": [8595], "characters": "\u2193" },+ "⇊": { "codepoints": [8650], "characters": "\u21CA" },+ "⇃": { "codepoints": [8643], "characters": "\u21C3" },+ "⇂": { "codepoints": [8642], "characters": "\u21C2" },+ "⤐": { "codepoints": [10512], "characters": "\u2910" },+ "⌟": { "codepoints": [8991], "characters": "\u231F" },+ "⌌": { "codepoints": [8972], "characters": "\u230C" },+ "𝒹": { "codepoints": [119993], "characters": "\uD835\uDCB9" },+ "ѕ": { "codepoints": [1109], "characters": "\u0455" },+ "⧶": { "codepoints": [10742], "characters": "\u29F6" },+ "đ": { "codepoints": [273], "characters": "\u0111" },+ "⋱": { "codepoints": [8945], "characters": "\u22F1" },+ "▿": { "codepoints": [9663], "characters": "\u25BF" },+ "▾": { "codepoints": [9662], "characters": "\u25BE" },+ "⇵": { "codepoints": [8693], "characters": "\u21F5" },+ "⥯": { "codepoints": [10607], "characters": "\u296F" },+ "⦦": { "codepoints": [10662], "characters": "\u29A6" },+ "џ": { "codepoints": [1119], "characters": "\u045F" },+ "⟿": { "codepoints": [10239], "characters": "\u27FF" },+ "⩷": { "codepoints": [10871], "characters": "\u2A77" },+ "≑": { "codepoints": [8785], "characters": "\u2251" },+ "é": { "codepoints": [233], "characters": "\u00E9" },+ "é": { "codepoints": [233], "characters": "\u00E9" },+ "⩮": { "codepoints": [10862], "characters": "\u2A6E" },+ "ě": { "codepoints": [283], "characters": "\u011B" },+ "≖": { "codepoints": [8790], "characters": "\u2256" },+ "ê": { "codepoints": [234], "characters": "\u00EA" },+ "ê": { "codepoints": [234], "characters": "\u00EA" },+ "≕": { "codepoints": [8789], "characters": "\u2255" },+ "э": { "codepoints": [1101], "characters": "\u044D" },+ "ė": { "codepoints": [279], "characters": "\u0117" },+ "ⅇ": { "codepoints": [8519], "characters": "\u2147" },+ "≒": { "codepoints": [8786], "characters": "\u2252" },+ "𝔢": { "codepoints": [120098], "characters": "\uD835\uDD22" },+ "⪚": { "codepoints": [10906], "characters": "\u2A9A" },+ "è": { "codepoints": [232], "characters": "\u00E8" },+ "è": { "codepoints": [232], "characters": "\u00E8" },+ "⪖": { "codepoints": [10902], "characters": "\u2A96" },+ "⪘": { "codepoints": [10904], "characters": "\u2A98" },+ "⪙": { "codepoints": [10905], "characters": "\u2A99" },+ "⏧": { "codepoints": [9191], "characters": "\u23E7" },+ "ℓ": { "codepoints": [8467], "characters": "\u2113" },+ "⪕": { "codepoints": [10901], "characters": "\u2A95" },+ "⪗": { "codepoints": [10903], "characters": "\u2A97" },+ "ē": { "codepoints": [275], "characters": "\u0113" },+ "∅": { "codepoints": [8709], "characters": "\u2205" },+ "∅": { "codepoints": [8709], "characters": "\u2205" },+ "∅": { "codepoints": [8709], "characters": "\u2205" },+ " ": { "codepoints": [8196], "characters": "\u2004" },+ " ": { "codepoints": [8197], "characters": "\u2005" },+ " ": { "codepoints": [8195], "characters": "\u2003" },+ "ŋ": { "codepoints": [331], "characters": "\u014B" },+ " ": { "codepoints": [8194], "characters": "\u2002" },+ "ę": { "codepoints": [281], "characters": "\u0119" },+ "𝕖": { "codepoints": [120150], "characters": "\uD835\uDD56" },+ "⋕": { "codepoints": [8917], "characters": "\u22D5" },+ "⧣": { "codepoints": [10723], "characters": "\u29E3" },+ "⩱": { "codepoints": [10865], "characters": "\u2A71" },+ "ε": { "codepoints": [949], "characters": "\u03B5" },+ "ε": { "codepoints": [949], "characters": "\u03B5" },+ "ϵ": { "codepoints": [1013], "characters": "\u03F5" },+ "≖": { "codepoints": [8790], "characters": "\u2256" },+ "≕": { "codepoints": [8789], "characters": "\u2255" },+ "≂": { "codepoints": [8770], "characters": "\u2242" },+ "⪖": { "codepoints": [10902], "characters": "\u2A96" },+ "⪕": { "codepoints": [10901], "characters": "\u2A95" },+ "=": { "codepoints": [61], "characters": "\u003D" },+ "≟": { "codepoints": [8799], "characters": "\u225F" },+ "≡": { "codepoints": [8801], "characters": "\u2261" },+ "⩸": { "codepoints": [10872], "characters": "\u2A78" },+ "⧥": { "codepoints": [10725], "characters": "\u29E5" },+ "≓": { "codepoints": [8787], "characters": "\u2253" },+ "⥱": { "codepoints": [10609], "characters": "\u2971" },+ "ℯ": { "codepoints": [8495], "characters": "\u212F" },+ "≐": { "codepoints": [8784], "characters": "\u2250" },+ "≂": { "codepoints": [8770], "characters": "\u2242" },+ "η": { "codepoints": [951], "characters": "\u03B7" },+ "ð": { "codepoints": [240], "characters": "\u00F0" },+ "ð": { "codepoints": [240], "characters": "\u00F0" },+ "ë": { "codepoints": [235], "characters": "\u00EB" },+ "ë": { "codepoints": [235], "characters": "\u00EB" },+ "€": { "codepoints": [8364], "characters": "\u20AC" },+ "!": { "codepoints": [33], "characters": "\u0021" },+ "∃": { "codepoints": [8707], "characters": "\u2203" },+ "ℰ": { "codepoints": [8496], "characters": "\u2130" },+ "ⅇ": { "codepoints": [8519], "characters": "\u2147" },+ "≒": { "codepoints": [8786], "characters": "\u2252" },+ "ф": { "codepoints": [1092], "characters": "\u0444" },+ "♀": { "codepoints": [9792], "characters": "\u2640" },+ "ffi": { "codepoints": [64259], "characters": "\uFB03" },+ "ff": { "codepoints": [64256], "characters": "\uFB00" },+ "ffl": { "codepoints": [64260], "characters": "\uFB04" },+ "𝔣": { "codepoints": [120099], "characters": "\uD835\uDD23" },+ "fi": { "codepoints": [64257], "characters": "\uFB01" },+ "fj": { "codepoints": [102, 106], "characters": "\u0066\u006A" },+ "♭": { "codepoints": [9837], "characters": "\u266D" },+ "fl": { "codepoints": [64258], "characters": "\uFB02" },+ "▱": { "codepoints": [9649], "characters": "\u25B1" },+ "ƒ": { "codepoints": [402], "characters": "\u0192" },+ "𝕗": { "codepoints": [120151], "characters": "\uD835\uDD57" },+ "∀": { "codepoints": [8704], "characters": "\u2200" },+ "⋔": { "codepoints": [8916], "characters": "\u22D4" },+ "⫙": { "codepoints": [10969], "characters": "\u2AD9" },+ "⨍": { "codepoints": [10765], "characters": "\u2A0D" },+ "½": { "codepoints": [189], "characters": "\u00BD" },+ "½": { "codepoints": [189], "characters": "\u00BD" },+ "⅓": { "codepoints": [8531], "characters": "\u2153" },+ "¼": { "codepoints": [188], "characters": "\u00BC" },+ "¼": { "codepoints": [188], "characters": "\u00BC" },+ "⅕": { "codepoints": [8533], "characters": "\u2155" },+ "⅙": { "codepoints": [8537], "characters": "\u2159" },+ "⅛": { "codepoints": [8539], "characters": "\u215B" },+ "⅔": { "codepoints": [8532], "characters": "\u2154" },+ "⅖": { "codepoints": [8534], "characters": "\u2156" },+ "¾": { "codepoints": [190], "characters": "\u00BE" },+ "¾": { "codepoints": [190], "characters": "\u00BE" },+ "⅗": { "codepoints": [8535], "characters": "\u2157" },+ "⅜": { "codepoints": [8540], "characters": "\u215C" },+ "⅘": { "codepoints": [8536], "characters": "\u2158" },+ "⅚": { "codepoints": [8538], "characters": "\u215A" },+ "⅝": { "codepoints": [8541], "characters": "\u215D" },+ "⅞": { "codepoints": [8542], "characters": "\u215E" },+ "⁄": { "codepoints": [8260], "characters": "\u2044" },+ "⌢": { "codepoints": [8994], "characters": "\u2322" },+ "𝒻": { "codepoints": [119995], "characters": "\uD835\uDCBB" },+ "≧": { "codepoints": [8807], "characters": "\u2267" },+ "⪌": { "codepoints": [10892], "characters": "\u2A8C" },+ "ǵ": { "codepoints": [501], "characters": "\u01F5" },+ "γ": { "codepoints": [947], "characters": "\u03B3" },+ "ϝ": { "codepoints": [989], "characters": "\u03DD" },+ "⪆": { "codepoints": [10886], "characters": "\u2A86" },+ "ğ": { "codepoints": [287], "characters": "\u011F" },+ "ĝ": { "codepoints": [285], "characters": "\u011D" },+ "г": { "codepoints": [1075], "characters": "\u0433" },+ "ġ": { "codepoints": [289], "characters": "\u0121" },+ "≥": { "codepoints": [8805], "characters": "\u2265" },+ "⋛": { "codepoints": [8923], "characters": "\u22DB" },+ "≥": { "codepoints": [8805], "characters": "\u2265" },+ "≧": { "codepoints": [8807], "characters": "\u2267" },+ "⩾": { "codepoints": [10878], "characters": "\u2A7E" },+ "⩾": { "codepoints": [10878], "characters": "\u2A7E" },+ "⪩": { "codepoints": [10921], "characters": "\u2AA9" },+ "⪀": { "codepoints": [10880], "characters": "\u2A80" },+ "⪂": { "codepoints": [10882], "characters": "\u2A82" },+ "⪄": { "codepoints": [10884], "characters": "\u2A84" },+ "⋛︀": { "codepoints": [8923, 65024], "characters": "\u22DB\uFE00" },+ "⪔": { "codepoints": [10900], "characters": "\u2A94" },+ "𝔤": { "codepoints": [120100], "characters": "\uD835\uDD24" },+ "≫": { "codepoints": [8811], "characters": "\u226B" },+ "⋙": { "codepoints": [8921], "characters": "\u22D9" },+ "ℷ": { "codepoints": [8503], "characters": "\u2137" },+ "ѓ": { "codepoints": [1107], "characters": "\u0453" },+ "≷": { "codepoints": [8823], "characters": "\u2277" },+ "⪒": { "codepoints": [10898], "characters": "\u2A92" },+ "⪥": { "codepoints": [10917], "characters": "\u2AA5" },+ "⪤": { "codepoints": [10916], "characters": "\u2AA4" },+ "≩": { "codepoints": [8809], "characters": "\u2269" },+ "⪊": { "codepoints": [10890], "characters": "\u2A8A" },+ "⪊": { "codepoints": [10890], "characters": "\u2A8A" },+ "⪈": { "codepoints": [10888], "characters": "\u2A88" },+ "⪈": { "codepoints": [10888], "characters": "\u2A88" },+ "≩": { "codepoints": [8809], "characters": "\u2269" },+ "⋧": { "codepoints": [8935], "characters": "\u22E7" },+ "𝕘": { "codepoints": [120152], "characters": "\uD835\uDD58" },+ "`": { "codepoints": [96], "characters": "\u0060" },+ "ℊ": { "codepoints": [8458], "characters": "\u210A" },+ "≳": { "codepoints": [8819], "characters": "\u2273" },+ "⪎": { "codepoints": [10894], "characters": "\u2A8E" },+ "⪐": { "codepoints": [10896], "characters": "\u2A90" },+ ">": { "codepoints": [62], "characters": "\u003E" },+ ">": { "codepoints": [62], "characters": "\u003E" },+ "⪧": { "codepoints": [10919], "characters": "\u2AA7" },+ "⩺": { "codepoints": [10874], "characters": "\u2A7A" },+ "⋗": { "codepoints": [8919], "characters": "\u22D7" },+ "⦕": { "codepoints": [10645], "characters": "\u2995" },+ "⩼": { "codepoints": [10876], "characters": "\u2A7C" },+ "⪆": { "codepoints": [10886], "characters": "\u2A86" },+ "⥸": { "codepoints": [10616], "characters": "\u2978" },+ "⋗": { "codepoints": [8919], "characters": "\u22D7" },+ "⋛": { "codepoints": [8923], "characters": "\u22DB" },+ "⪌": { "codepoints": [10892], "characters": "\u2A8C" },+ "≷": { "codepoints": [8823], "characters": "\u2277" },+ "≳": { "codepoints": [8819], "characters": "\u2273" },+ "≩︀": { "codepoints": [8809, 65024], "characters": "\u2269\uFE00" },+ "≩︀": { "codepoints": [8809, 65024], "characters": "\u2269\uFE00" },+ "⇔": { "codepoints": [8660], "characters": "\u21D4" },+ " ": { "codepoints": [8202], "characters": "\u200A" },+ "½": { "codepoints": [189], "characters": "\u00BD" },+ "ℋ": { "codepoints": [8459], "characters": "\u210B" },+ "ъ": { "codepoints": [1098], "characters": "\u044A" },+ "↔": { "codepoints": [8596], "characters": "\u2194" },+ "⥈": { "codepoints": [10568], "characters": "\u2948" },+ "↭": { "codepoints": [8621], "characters": "\u21AD" },+ "ℏ": { "codepoints": [8463], "characters": "\u210F" },+ "ĥ": { "codepoints": [293], "characters": "\u0125" },+ "♥": { "codepoints": [9829], "characters": "\u2665" },+ "♥": { "codepoints": [9829], "characters": "\u2665" },+ "…": { "codepoints": [8230], "characters": "\u2026" },+ "⊹": { "codepoints": [8889], "characters": "\u22B9" },+ "𝔥": { "codepoints": [120101], "characters": "\uD835\uDD25" },+ "⤥": { "codepoints": [10533], "characters": "\u2925" },+ "⤦": { "codepoints": [10534], "characters": "\u2926" },+ "⇿": { "codepoints": [8703], "characters": "\u21FF" },+ "∻": { "codepoints": [8763], "characters": "\u223B" },+ "↩": { "codepoints": [8617], "characters": "\u21A9" },+ "↪": { "codepoints": [8618], "characters": "\u21AA" },+ "𝕙": { "codepoints": [120153], "characters": "\uD835\uDD59" },+ "―": { "codepoints": [8213], "characters": "\u2015" },+ "𝒽": { "codepoints": [119997], "characters": "\uD835\uDCBD" },+ "ℏ": { "codepoints": [8463], "characters": "\u210F" },+ "ħ": { "codepoints": [295], "characters": "\u0127" },+ "⁃": { "codepoints": [8259], "characters": "\u2043" },+ "‐": { "codepoints": [8208], "characters": "\u2010" },+ "í": { "codepoints": [237], "characters": "\u00ED" },+ "í": { "codepoints": [237], "characters": "\u00ED" },+ "⁣": { "codepoints": [8291], "characters": "\u2063" },+ "î": { "codepoints": [238], "characters": "\u00EE" },+ "î": { "codepoints": [238], "characters": "\u00EE" },+ "и": { "codepoints": [1080], "characters": "\u0438" },+ "е": { "codepoints": [1077], "characters": "\u0435" },+ "¡": { "codepoints": [161], "characters": "\u00A1" },+ "¡": { "codepoints": [161], "characters": "\u00A1" },+ "⇔": { "codepoints": [8660], "characters": "\u21D4" },+ "𝔦": { "codepoints": [120102], "characters": "\uD835\uDD26" },+ "ì": { "codepoints": [236], "characters": "\u00EC" },+ "ì": { "codepoints": [236], "characters": "\u00EC" },+ "ⅈ": { "codepoints": [8520], "characters": "\u2148" },+ "⨌": { "codepoints": [10764], "characters": "\u2A0C" },+ "∭": { "codepoints": [8749], "characters": "\u222D" },+ "⧜": { "codepoints": [10716], "characters": "\u29DC" },+ "℩": { "codepoints": [8489], "characters": "\u2129" },+ "ij": { "codepoints": [307], "characters": "\u0133" },+ "ī": { "codepoints": [299], "characters": "\u012B" },+ "ℑ": { "codepoints": [8465], "characters": "\u2111" },+ "ℐ": { "codepoints": [8464], "characters": "\u2110" },+ "ℑ": { "codepoints": [8465], "characters": "\u2111" },+ "ı": { "codepoints": [305], "characters": "\u0131" },+ "⊷": { "codepoints": [8887], "characters": "\u22B7" },+ "Ƶ": { "codepoints": [437], "characters": "\u01B5" },+ "∈": { "codepoints": [8712], "characters": "\u2208" },+ "℅": { "codepoints": [8453], "characters": "\u2105" },+ "∞": { "codepoints": [8734], "characters": "\u221E" },+ "⧝": { "codepoints": [10717], "characters": "\u29DD" },+ "ı": { "codepoints": [305], "characters": "\u0131" },+ "∫": { "codepoints": [8747], "characters": "\u222B" },+ "⊺": { "codepoints": [8890], "characters": "\u22BA" },+ "ℤ": { "codepoints": [8484], "characters": "\u2124" },+ "⊺": { "codepoints": [8890], "characters": "\u22BA" },+ "⨗": { "codepoints": [10775], "characters": "\u2A17" },+ "⨼": { "codepoints": [10812], "characters": "\u2A3C" },+ "ё": { "codepoints": [1105], "characters": "\u0451" },+ "į": { "codepoints": [303], "characters": "\u012F" },+ "𝕚": { "codepoints": [120154], "characters": "\uD835\uDD5A" },+ "ι": { "codepoints": [953], "characters": "\u03B9" },+ "⨼": { "codepoints": [10812], "characters": "\u2A3C" },+ "¿": { "codepoints": [191], "characters": "\u00BF" },+ "¿": { "codepoints": [191], "characters": "\u00BF" },+ "𝒾": { "codepoints": [119998], "characters": "\uD835\uDCBE" },+ "∈": { "codepoints": [8712], "characters": "\u2208" },+ "⋹": { "codepoints": [8953], "characters": "\u22F9" },+ "⋵": { "codepoints": [8949], "characters": "\u22F5" },+ "⋴": { "codepoints": [8948], "characters": "\u22F4" },+ "⋳": { "codepoints": [8947], "characters": "\u22F3" },+ "∈": { "codepoints": [8712], "characters": "\u2208" },+ "⁢": { "codepoints": [8290], "characters": "\u2062" },+ "ĩ": { "codepoints": [297], "characters": "\u0129" },+ "і": { "codepoints": [1110], "characters": "\u0456" },+ "ï": { "codepoints": [239], "characters": "\u00EF" },+ "ï": { "codepoints": [239], "characters": "\u00EF" },+ "ĵ": { "codepoints": [309], "characters": "\u0135" },+ "й": { "codepoints": [1081], "characters": "\u0439" },+ "𝔧": { "codepoints": [120103], "characters": "\uD835\uDD27" },+ "ȷ": { "codepoints": [567], "characters": "\u0237" },+ "𝕛": { "codepoints": [120155], "characters": "\uD835\uDD5B" },+ "𝒿": { "codepoints": [119999], "characters": "\uD835\uDCBF" },+ "ј": { "codepoints": [1112], "characters": "\u0458" },+ "є": { "codepoints": [1108], "characters": "\u0454" },+ "κ": { "codepoints": [954], "characters": "\u03BA" },+ "ϰ": { "codepoints": [1008], "characters": "\u03F0" },+ "ķ": { "codepoints": [311], "characters": "\u0137" },+ "к": { "codepoints": [1082], "characters": "\u043A" },+ "𝔨": { "codepoints": [120104], "characters": "\uD835\uDD28" },+ "ĸ": { "codepoints": [312], "characters": "\u0138" },+ "х": { "codepoints": [1093], "characters": "\u0445" },+ "ќ": { "codepoints": [1116], "characters": "\u045C" },+ "𝕜": { "codepoints": [120156], "characters": "\uD835\uDD5C" },+ "𝓀": { "codepoints": [120000], "characters": "\uD835\uDCC0" },+ "⇚": { "codepoints": [8666], "characters": "\u21DA" },+ "⇐": { "codepoints": [8656], "characters": "\u21D0" },+ "⤛": { "codepoints": [10523], "characters": "\u291B" },+ "⤎": { "codepoints": [10510], "characters": "\u290E" },+ "≦": { "codepoints": [8806], "characters": "\u2266" },+ "⪋": { "codepoints": [10891], "characters": "\u2A8B" },+ "⥢": { "codepoints": [10594], "characters": "\u2962" },+ "ĺ": { "codepoints": [314], "characters": "\u013A" },+ "⦴": { "codepoints": [10676], "characters": "\u29B4" },+ "ℒ": { "codepoints": [8466], "characters": "\u2112" },+ "λ": { "codepoints": [955], "characters": "\u03BB" },+ "⟨": { "codepoints": [10216], "characters": "\u27E8" },+ "⦑": { "codepoints": [10641], "characters": "\u2991" },+ "⟨": { "codepoints": [10216], "characters": "\u27E8" },+ "⪅": { "codepoints": [10885], "characters": "\u2A85" },+ "«": { "codepoints": [171], "characters": "\u00AB" },+ "«": { "codepoints": [171], "characters": "\u00AB" },+ "←": { "codepoints": [8592], "characters": "\u2190" },+ "⇤": { "codepoints": [8676], "characters": "\u21E4" },+ "⤟": { "codepoints": [10527], "characters": "\u291F" },+ "⤝": { "codepoints": [10525], "characters": "\u291D" },+ "↩": { "codepoints": [8617], "characters": "\u21A9" },+ "↫": { "codepoints": [8619], "characters": "\u21AB" },+ "⤹": { "codepoints": [10553], "characters": "\u2939" },+ "⥳": { "codepoints": [10611], "characters": "\u2973" },+ "↢": { "codepoints": [8610], "characters": "\u21A2" },+ "⪫": { "codepoints": [10923], "characters": "\u2AAB" },+ "⤙": { "codepoints": [10521], "characters": "\u2919" },+ "⪭": { "codepoints": [10925], "characters": "\u2AAD" },+ "⪭︀": { "codepoints": [10925, 65024], "characters": "\u2AAD\uFE00" },+ "⤌": { "codepoints": [10508], "characters": "\u290C" },+ "❲": { "codepoints": [10098], "characters": "\u2772" },+ "{": { "codepoints": [123], "characters": "\u007B" },+ "[": { "codepoints": [91], "characters": "\u005B" },+ "⦋": { "codepoints": [10635], "characters": "\u298B" },+ "⦏": { "codepoints": [10639], "characters": "\u298F" },+ "⦍": { "codepoints": [10637], "characters": "\u298D" },+ "ľ": { "codepoints": [318], "characters": "\u013E" },+ "ļ": { "codepoints": [316], "characters": "\u013C" },+ "⌈": { "codepoints": [8968], "characters": "\u2308" },+ "{": { "codepoints": [123], "characters": "\u007B" },+ "л": { "codepoints": [1083], "characters": "\u043B" },+ "⤶": { "codepoints": [10550], "characters": "\u2936" },+ "“": { "codepoints": [8220], "characters": "\u201C" },+ "„": { "codepoints": [8222], "characters": "\u201E" },+ "⥧": { "codepoints": [10599], "characters": "\u2967" },+ "⥋": { "codepoints": [10571], "characters": "\u294B" },+ "↲": { "codepoints": [8626], "characters": "\u21B2" },+ "≤": { "codepoints": [8804], "characters": "\u2264" },+ "←": { "codepoints": [8592], "characters": "\u2190" },+ "↢": { "codepoints": [8610], "characters": "\u21A2" },+ "↽": { "codepoints": [8637], "characters": "\u21BD" },+ "↼": { "codepoints": [8636], "characters": "\u21BC" },+ "⇇": { "codepoints": [8647], "characters": "\u21C7" },+ "↔": { "codepoints": [8596], "characters": "\u2194" },+ "⇆": { "codepoints": [8646], "characters": "\u21C6" },+ "⇋": { "codepoints": [8651], "characters": "\u21CB" },+ "↭": { "codepoints": [8621], "characters": "\u21AD" },+ "⋋": { "codepoints": [8907], "characters": "\u22CB" },+ "⋚": { "codepoints": [8922], "characters": "\u22DA" },+ "≤": { "codepoints": [8804], "characters": "\u2264" },+ "≦": { "codepoints": [8806], "characters": "\u2266" },+ "⩽": { "codepoints": [10877], "characters": "\u2A7D" },+ "⩽": { "codepoints": [10877], "characters": "\u2A7D" },+ "⪨": { "codepoints": [10920], "characters": "\u2AA8" },+ "⩿": { "codepoints": [10879], "characters": "\u2A7F" },+ "⪁": { "codepoints": [10881], "characters": "\u2A81" },+ "⪃": { "codepoints": [10883], "characters": "\u2A83" },+ "⋚︀": { "codepoints": [8922, 65024], "characters": "\u22DA\uFE00" },+ "⪓": { "codepoints": [10899], "characters": "\u2A93" },+ "⪅": { "codepoints": [10885], "characters": "\u2A85" },+ "⋖": { "codepoints": [8918], "characters": "\u22D6" },+ "⋚": { "codepoints": [8922], "characters": "\u22DA" },+ "⪋": { "codepoints": [10891], "characters": "\u2A8B" },+ "≶": { "codepoints": [8822], "characters": "\u2276" },+ "≲": { "codepoints": [8818], "characters": "\u2272" },+ "⥼": { "codepoints": [10620], "characters": "\u297C" },+ "⌊": { "codepoints": [8970], "characters": "\u230A" },+ "𝔩": { "codepoints": [120105], "characters": "\uD835\uDD29" },+ "≶": { "codepoints": [8822], "characters": "\u2276" },+ "⪑": { "codepoints": [10897], "characters": "\u2A91" },+ "↽": { "codepoints": [8637], "characters": "\u21BD" },+ "↼": { "codepoints": [8636], "characters": "\u21BC" },+ "⥪": { "codepoints": [10602], "characters": "\u296A" },+ "▄": { "codepoints": [9604], "characters": "\u2584" },+ "љ": { "codepoints": [1113], "characters": "\u0459" },+ "≪": { "codepoints": [8810], "characters": "\u226A" },+ "⇇": { "codepoints": [8647], "characters": "\u21C7" },+ "⌞": { "codepoints": [8990], "characters": "\u231E" },+ "⥫": { "codepoints": [10603], "characters": "\u296B" },+ "◺": { "codepoints": [9722], "characters": "\u25FA" },+ "ŀ": { "codepoints": [320], "characters": "\u0140" },+ "⎰": { "codepoints": [9136], "characters": "\u23B0" },+ "⎰": { "codepoints": [9136], "characters": "\u23B0" },+ "≨": { "codepoints": [8808], "characters": "\u2268" },+ "⪉": { "codepoints": [10889], "characters": "\u2A89" },+ "⪉": { "codepoints": [10889], "characters": "\u2A89" },+ "⪇": { "codepoints": [10887], "characters": "\u2A87" },+ "⪇": { "codepoints": [10887], "characters": "\u2A87" },+ "≨": { "codepoints": [8808], "characters": "\u2268" },+ "⋦": { "codepoints": [8934], "characters": "\u22E6" },+ "⟬": { "codepoints": [10220], "characters": "\u27EC" },+ "⇽": { "codepoints": [8701], "characters": "\u21FD" },+ "⟦": { "codepoints": [10214], "characters": "\u27E6" },+ "⟵": { "codepoints": [10229], "characters": "\u27F5" },+ "⟷": { "codepoints": [10231], "characters": "\u27F7" },+ "⟼": { "codepoints": [10236], "characters": "\u27FC" },+ "⟶": { "codepoints": [10230], "characters": "\u27F6" },+ "↫": { "codepoints": [8619], "characters": "\u21AB" },+ "↬": { "codepoints": [8620], "characters": "\u21AC" },+ "⦅": { "codepoints": [10629], "characters": "\u2985" },+ "𝕝": { "codepoints": [120157], "characters": "\uD835\uDD5D" },+ "⨭": { "codepoints": [10797], "characters": "\u2A2D" },+ "⨴": { "codepoints": [10804], "characters": "\u2A34" },+ "∗": { "codepoints": [8727], "characters": "\u2217" },+ "_": { "codepoints": [95], "characters": "\u005F" },+ "◊": { "codepoints": [9674], "characters": "\u25CA" },+ "◊": { "codepoints": [9674], "characters": "\u25CA" },+ "⧫": { "codepoints": [10731], "characters": "\u29EB" },+ "(": { "codepoints": [40], "characters": "\u0028" },+ "⦓": { "codepoints": [10643], "characters": "\u2993" },+ "⇆": { "codepoints": [8646], "characters": "\u21C6" },+ "⌟": { "codepoints": [8991], "characters": "\u231F" },+ "⇋": { "codepoints": [8651], "characters": "\u21CB" },+ "⥭": { "codepoints": [10605], "characters": "\u296D" },+ "‎": { "codepoints": [8206], "characters": "\u200E" },+ "⊿": { "codepoints": [8895], "characters": "\u22BF" },+ "‹": { "codepoints": [8249], "characters": "\u2039" },+ "𝓁": { "codepoints": [120001], "characters": "\uD835\uDCC1" },+ "↰": { "codepoints": [8624], "characters": "\u21B0" },+ "≲": { "codepoints": [8818], "characters": "\u2272" },+ "⪍": { "codepoints": [10893], "characters": "\u2A8D" },+ "⪏": { "codepoints": [10895], "characters": "\u2A8F" },+ "[": { "codepoints": [91], "characters": "\u005B" },+ "‘": { "codepoints": [8216], "characters": "\u2018" },+ "‚": { "codepoints": [8218], "characters": "\u201A" },+ "ł": { "codepoints": [322], "characters": "\u0142" },+ "<": { "codepoints": [60], "characters": "\u003C" },+ "<": { "codepoints": [60], "characters": "\u003C" },+ "⪦": { "codepoints": [10918], "characters": "\u2AA6" },+ "⩹": { "codepoints": [10873], "characters": "\u2A79" },+ "⋖": { "codepoints": [8918], "characters": "\u22D6" },+ "⋋": { "codepoints": [8907], "characters": "\u22CB" },+ "⋉": { "codepoints": [8905], "characters": "\u22C9" },+ "⥶": { "codepoints": [10614], "characters": "\u2976" },+ "⩻": { "codepoints": [10875], "characters": "\u2A7B" },+ "⦖": { "codepoints": [10646], "characters": "\u2996" },+ "◃": { "codepoints": [9667], "characters": "\u25C3" },+ "⊴": { "codepoints": [8884], "characters": "\u22B4" },+ "◂": { "codepoints": [9666], "characters": "\u25C2" },+ "⥊": { "codepoints": [10570], "characters": "\u294A" },+ "⥦": { "codepoints": [10598], "characters": "\u2966" },+ "≨︀": { "codepoints": [8808, 65024], "characters": "\u2268\uFE00" },+ "≨︀": { "codepoints": [8808, 65024], "characters": "\u2268\uFE00" },+ "∺": { "codepoints": [8762], "characters": "\u223A" },+ "¯": { "codepoints": [175], "characters": "\u00AF" },+ "¯": { "codepoints": [175], "characters": "\u00AF" },+ "♂": { "codepoints": [9794], "characters": "\u2642" },+ "✠": { "codepoints": [10016], "characters": "\u2720" },+ "✠": { "codepoints": [10016], "characters": "\u2720" },+ "↦": { "codepoints": [8614], "characters": "\u21A6" },+ "↦": { "codepoints": [8614], "characters": "\u21A6" },+ "↧": { "codepoints": [8615], "characters": "\u21A7" },+ "↤": { "codepoints": [8612], "characters": "\u21A4" },+ "↥": { "codepoints": [8613], "characters": "\u21A5" },+ "▮": { "codepoints": [9646], "characters": "\u25AE" },+ "⨩": { "codepoints": [10793], "characters": "\u2A29" },+ "м": { "codepoints": [1084], "characters": "\u043C" },+ "—": { "codepoints": [8212], "characters": "\u2014" },+ "∡": { "codepoints": [8737], "characters": "\u2221" },+ "𝔪": { "codepoints": [120106], "characters": "\uD835\uDD2A" },+ "℧": { "codepoints": [8487], "characters": "\u2127" },+ "µ": { "codepoints": [181], "characters": "\u00B5" },+ "µ": { "codepoints": [181], "characters": "\u00B5" },+ "∣": { "codepoints": [8739], "characters": "\u2223" },+ "*": { "codepoints": [42], "characters": "\u002A" },+ "⫰": { "codepoints": [10992], "characters": "\u2AF0" },+ "·": { "codepoints": [183], "characters": "\u00B7" },+ "·": { "codepoints": [183], "characters": "\u00B7" },+ "−": { "codepoints": [8722], "characters": "\u2212" },+ "⊟": { "codepoints": [8863], "characters": "\u229F" },+ "∸": { "codepoints": [8760], "characters": "\u2238" },+ "⨪": { "codepoints": [10794], "characters": "\u2A2A" },+ "⫛": { "codepoints": [10971], "characters": "\u2ADB" },+ "…": { "codepoints": [8230], "characters": "\u2026" },+ "∓": { "codepoints": [8723], "characters": "\u2213" },+ "⊧": { "codepoints": [8871], "characters": "\u22A7" },+ "𝕞": { "codepoints": [120158], "characters": "\uD835\uDD5E" },+ "∓": { "codepoints": [8723], "characters": "\u2213" },+ "𝓂": { "codepoints": [120002], "characters": "\uD835\uDCC2" },+ "∾": { "codepoints": [8766], "characters": "\u223E" },+ "μ": { "codepoints": [956], "characters": "\u03BC" },+ "⊸": { "codepoints": [8888], "characters": "\u22B8" },+ "⊸": { "codepoints": [8888], "characters": "\u22B8" },+ "⋙̸": { "codepoints": [8921, 824], "characters": "\u22D9\u0338" },+ "≫⃒": { "codepoints": [8811, 8402], "characters": "\u226B\u20D2" },+ "≫̸": { "codepoints": [8811, 824], "characters": "\u226B\u0338" },+ "⇍": { "codepoints": [8653], "characters": "\u21CD" },+ "⇎": { "codepoints": [8654], "characters": "\u21CE" },+ "⋘̸": { "codepoints": [8920, 824], "characters": "\u22D8\u0338" },+ "≪⃒": { "codepoints": [8810, 8402], "characters": "\u226A\u20D2" },+ "≪̸": { "codepoints": [8810, 824], "characters": "\u226A\u0338" },+ "⇏": { "codepoints": [8655], "characters": "\u21CF" },+ "⊯": { "codepoints": [8879], "characters": "\u22AF" },+ "⊮": { "codepoints": [8878], "characters": "\u22AE" },+ "∇": { "codepoints": [8711], "characters": "\u2207" },+ "ń": { "codepoints": [324], "characters": "\u0144" },+ "∠⃒": { "codepoints": [8736, 8402], "characters": "\u2220\u20D2" },+ "≉": { "codepoints": [8777], "characters": "\u2249" },+ "⩰̸": { "codepoints": [10864, 824], "characters": "\u2A70\u0338" },+ "≋̸": { "codepoints": [8779, 824], "characters": "\u224B\u0338" },+ "ʼn": { "codepoints": [329], "characters": "\u0149" },+ "≉": { "codepoints": [8777], "characters": "\u2249" },+ "♮": { "codepoints": [9838], "characters": "\u266E" },+ "♮": { "codepoints": [9838], "characters": "\u266E" },+ "ℕ": { "codepoints": [8469], "characters": "\u2115" },+ " ": { "codepoints": [160], "characters": "\u00A0" },+ " ": { "codepoints": [160], "characters": "\u00A0" },+ "≎̸": { "codepoints": [8782, 824], "characters": "\u224E\u0338" },+ "≏̸": { "codepoints": [8783, 824], "characters": "\u224F\u0338" },+ "⩃": { "codepoints": [10819], "characters": "\u2A43" },+ "ň": { "codepoints": [328], "characters": "\u0148" },+ "ņ": { "codepoints": [326], "characters": "\u0146" },+ "≇": { "codepoints": [8775], "characters": "\u2247" },+ "⩭̸": { "codepoints": [10861, 824], "characters": "\u2A6D\u0338" },+ "⩂": { "codepoints": [10818], "characters": "\u2A42" },+ "н": { "codepoints": [1085], "characters": "\u043D" },+ "–": { "codepoints": [8211], "characters": "\u2013" },+ "≠": { "codepoints": [8800], "characters": "\u2260" },+ "⇗": { "codepoints": [8663], "characters": "\u21D7" },+ "⤤": { "codepoints": [10532], "characters": "\u2924" },+ "↗": { "codepoints": [8599], "characters": "\u2197" },+ "↗": { "codepoints": [8599], "characters": "\u2197" },+ "≐̸": { "codepoints": [8784, 824], "characters": "\u2250\u0338" },+ "≢": { "codepoints": [8802], "characters": "\u2262" },+ "⤨": { "codepoints": [10536], "characters": "\u2928" },+ "≂̸": { "codepoints": [8770, 824], "characters": "\u2242\u0338" },+ "∄": { "codepoints": [8708], "characters": "\u2204" },+ "∄": { "codepoints": [8708], "characters": "\u2204" },+ "𝔫": { "codepoints": [120107], "characters": "\uD835\uDD2B" },+ "≧̸": { "codepoints": [8807, 824], "characters": "\u2267\u0338" },+ "≱": { "codepoints": [8817], "characters": "\u2271" },+ "≱": { "codepoints": [8817], "characters": "\u2271" },+ "≧̸": { "codepoints": [8807, 824], "characters": "\u2267\u0338" },+ "⩾̸": { "codepoints": [10878, 824], "characters": "\u2A7E\u0338" },+ "⩾̸": { "codepoints": [10878, 824], "characters": "\u2A7E\u0338" },+ "≵": { "codepoints": [8821], "characters": "\u2275" },+ "≯": { "codepoints": [8815], "characters": "\u226F" },+ "≯": { "codepoints": [8815], "characters": "\u226F" },+ "⇎": { "codepoints": [8654], "characters": "\u21CE" },+ "↮": { "codepoints": [8622], "characters": "\u21AE" },+ "⫲": { "codepoints": [10994], "characters": "\u2AF2" },+ "∋": { "codepoints": [8715], "characters": "\u220B" },+ "⋼": { "codepoints": [8956], "characters": "\u22FC" },+ "⋺": { "codepoints": [8954], "characters": "\u22FA" },+ "∋": { "codepoints": [8715], "characters": "\u220B" },+ "њ": { "codepoints": [1114], "characters": "\u045A" },+ "⇍": { "codepoints": [8653], "characters": "\u21CD" },+ "≦̸": { "codepoints": [8806, 824], "characters": "\u2266\u0338" },+ "↚": { "codepoints": [8602], "characters": "\u219A" },+ "‥": { "codepoints": [8229], "characters": "\u2025" },+ "≰": { "codepoints": [8816], "characters": "\u2270" },+ "↚": { "codepoints": [8602], "characters": "\u219A" },+ "↮": { "codepoints": [8622], "characters": "\u21AE" },+ "≰": { "codepoints": [8816], "characters": "\u2270" },+ "≦̸": { "codepoints": [8806, 824], "characters": "\u2266\u0338" },+ "⩽̸": { "codepoints": [10877, 824], "characters": "\u2A7D\u0338" },+ "⩽̸": { "codepoints": [10877, 824], "characters": "\u2A7D\u0338" },+ "≮": { "codepoints": [8814], "characters": "\u226E" },+ "≴": { "codepoints": [8820], "characters": "\u2274" },+ "≮": { "codepoints": [8814], "characters": "\u226E" },+ "⋪": { "codepoints": [8938], "characters": "\u22EA" },+ "⋬": { "codepoints": [8940], "characters": "\u22EC" },+ "∤": { "codepoints": [8740], "characters": "\u2224" },+ "𝕟": { "codepoints": [120159], "characters": "\uD835\uDD5F" },+ "¬": { "codepoints": [172], "characters": "\u00AC" },+ "¬": { "codepoints": [172], "characters": "\u00AC" },+ "∉": { "codepoints": [8713], "characters": "\u2209" },+ "⋹̸": { "codepoints": [8953, 824], "characters": "\u22F9\u0338" },+ "⋵̸": { "codepoints": [8949, 824], "characters": "\u22F5\u0338" },+ "∉": { "codepoints": [8713], "characters": "\u2209" },+ "⋷": { "codepoints": [8951], "characters": "\u22F7" },+ "⋶": { "codepoints": [8950], "characters": "\u22F6" },+ "∌": { "codepoints": [8716], "characters": "\u220C" },+ "∌": { "codepoints": [8716], "characters": "\u220C" },+ "⋾": { "codepoints": [8958], "characters": "\u22FE" },+ "⋽": { "codepoints": [8957], "characters": "\u22FD" },+ "∦": { "codepoints": [8742], "characters": "\u2226" },+ "∦": { "codepoints": [8742], "characters": "\u2226" },+ "⫽⃥": { "codepoints": [11005, 8421], "characters": "\u2AFD\u20E5" },+ "∂̸": { "codepoints": [8706, 824], "characters": "\u2202\u0338" },+ "⨔": { "codepoints": [10772], "characters": "\u2A14" },+ "⊀": { "codepoints": [8832], "characters": "\u2280" },+ "⋠": { "codepoints": [8928], "characters": "\u22E0" },+ "⪯̸": { "codepoints": [10927, 824], "characters": "\u2AAF\u0338" },+ "⊀": { "codepoints": [8832], "characters": "\u2280" },+ "⪯̸": { "codepoints": [10927, 824], "characters": "\u2AAF\u0338" },+ "⇏": { "codepoints": [8655], "characters": "\u21CF" },+ "↛": { "codepoints": [8603], "characters": "\u219B" },+ "⤳̸": { "codepoints": [10547, 824], "characters": "\u2933\u0338" },+ "↝̸": { "codepoints": [8605, 824], "characters": "\u219D\u0338" },+ "↛": { "codepoints": [8603], "characters": "\u219B" },+ "⋫": { "codepoints": [8939], "characters": "\u22EB" },+ "⋭": { "codepoints": [8941], "characters": "\u22ED" },+ "⊁": { "codepoints": [8833], "characters": "\u2281" },+ "⋡": { "codepoints": [8929], "characters": "\u22E1" },+ "⪰̸": { "codepoints": [10928, 824], "characters": "\u2AB0\u0338" },+ "𝓃": { "codepoints": [120003], "characters": "\uD835\uDCC3" },+ "∤": { "codepoints": [8740], "characters": "\u2224" },+ "∦": { "codepoints": [8742], "characters": "\u2226" },+ "≁": { "codepoints": [8769], "characters": "\u2241" },+ "≄": { "codepoints": [8772], "characters": "\u2244" },+ "≄": { "codepoints": [8772], "characters": "\u2244" },+ "∤": { "codepoints": [8740], "characters": "\u2224" },+ "∦": { "codepoints": [8742], "characters": "\u2226" },+ "⋢": { "codepoints": [8930], "characters": "\u22E2" },+ "⋣": { "codepoints": [8931], "characters": "\u22E3" },+ "⊄": { "codepoints": [8836], "characters": "\u2284" },+ "⫅̸": { "codepoints": [10949, 824], "characters": "\u2AC5\u0338" },+ "⊈": { "codepoints": [8840], "characters": "\u2288" },+ "⊂⃒": { "codepoints": [8834, 8402], "characters": "\u2282\u20D2" },+ "⊈": { "codepoints": [8840], "characters": "\u2288" },+ "⫅̸": { "codepoints": [10949, 824], "characters": "\u2AC5\u0338" },+ "⊁": { "codepoints": [8833], "characters": "\u2281" },+ "⪰̸": { "codepoints": [10928, 824], "characters": "\u2AB0\u0338" },+ "⊅": { "codepoints": [8837], "characters": "\u2285" },+ "⫆̸": { "codepoints": [10950, 824], "characters": "\u2AC6\u0338" },+ "⊉": { "codepoints": [8841], "characters": "\u2289" },+ "⊃⃒": { "codepoints": [8835, 8402], "characters": "\u2283\u20D2" },+ "⊉": { "codepoints": [8841], "characters": "\u2289" },+ "⫆̸": { "codepoints": [10950, 824], "characters": "\u2AC6\u0338" },+ "≹": { "codepoints": [8825], "characters": "\u2279" },+ "ñ": { "codepoints": [241], "characters": "\u00F1" },+ "ñ": { "codepoints": [241], "characters": "\u00F1" },+ "≸": { "codepoints": [8824], "characters": "\u2278" },+ "⋪": { "codepoints": [8938], "characters": "\u22EA" },+ "⋬": { "codepoints": [8940], "characters": "\u22EC" },+ "⋫": { "codepoints": [8939], "characters": "\u22EB" },+ "⋭": { "codepoints": [8941], "characters": "\u22ED" },+ "ν": { "codepoints": [957], "characters": "\u03BD" },+ "#": { "codepoints": [35], "characters": "\u0023" },+ "№": { "codepoints": [8470], "characters": "\u2116" },+ " ": { "codepoints": [8199], "characters": "\u2007" },+ "⊭": { "codepoints": [8877], "characters": "\u22AD" },+ "⤄": { "codepoints": [10500], "characters": "\u2904" },+ "≍⃒": { "codepoints": [8781, 8402], "characters": "\u224D\u20D2" },+ "⊬": { "codepoints": [8876], "characters": "\u22AC" },+ "≥⃒": { "codepoints": [8805, 8402], "characters": "\u2265\u20D2" },+ ">⃒": { "codepoints": [62, 8402], "characters": "\u003E\u20D2" },+ "⧞": { "codepoints": [10718], "characters": "\u29DE" },+ "⤂": { "codepoints": [10498], "characters": "\u2902" },+ "≤⃒": { "codepoints": [8804, 8402], "characters": "\u2264\u20D2" },+ "<⃒": { "codepoints": [60, 8402], "characters": "\u003C\u20D2" },+ "⊴⃒": { "codepoints": [8884, 8402], "characters": "\u22B4\u20D2" },+ "⤃": { "codepoints": [10499], "characters": "\u2903" },+ "⊵⃒": { "codepoints": [8885, 8402], "characters": "\u22B5\u20D2" },+ "∼⃒": { "codepoints": [8764, 8402], "characters": "\u223C\u20D2" },+ "⇖": { "codepoints": [8662], "characters": "\u21D6" },+ "⤣": { "codepoints": [10531], "characters": "\u2923" },+ "↖": { "codepoints": [8598], "characters": "\u2196" },+ "↖": { "codepoints": [8598], "characters": "\u2196" },+ "⤧": { "codepoints": [10535], "characters": "\u2927" },+ "Ⓢ": { "codepoints": [9416], "characters": "\u24C8" },+ "ó": { "codepoints": [243], "characters": "\u00F3" },+ "ó": { "codepoints": [243], "characters": "\u00F3" },+ "⊛": { "codepoints": [8859], "characters": "\u229B" },+ "⊚": { "codepoints": [8858], "characters": "\u229A" },+ "ô": { "codepoints": [244], "characters": "\u00F4" },+ "ô": { "codepoints": [244], "characters": "\u00F4" },+ "о": { "codepoints": [1086], "characters": "\u043E" },+ "⊝": { "codepoints": [8861], "characters": "\u229D" },+ "ő": { "codepoints": [337], "characters": "\u0151" },+ "⨸": { "codepoints": [10808], "characters": "\u2A38" },+ "⊙": { "codepoints": [8857], "characters": "\u2299" },+ "⦼": { "codepoints": [10684], "characters": "\u29BC" },+ "œ": { "codepoints": [339], "characters": "\u0153" },+ "⦿": { "codepoints": [10687], "characters": "\u29BF" },+ "𝔬": { "codepoints": [120108], "characters": "\uD835\uDD2C" },+ "˛": { "codepoints": [731], "characters": "\u02DB" },+ "ò": { "codepoints": [242], "characters": "\u00F2" },+ "ò": { "codepoints": [242], "characters": "\u00F2" },+ "⧁": { "codepoints": [10689], "characters": "\u29C1" },+ "⦵": { "codepoints": [10677], "characters": "\u29B5" },+ "Ω": { "codepoints": [937], "characters": "\u03A9" },+ "∮": { "codepoints": [8750], "characters": "\u222E" },+ "↺": { "codepoints": [8634], "characters": "\u21BA" },+ "⦾": { "codepoints": [10686], "characters": "\u29BE" },+ "⦻": { "codepoints": [10683], "characters": "\u29BB" },+ "‾": { "codepoints": [8254], "characters": "\u203E" },+ "⧀": { "codepoints": [10688], "characters": "\u29C0" },+ "ō": { "codepoints": [333], "characters": "\u014D" },+ "ω": { "codepoints": [969], "characters": "\u03C9" },+ "ο": { "codepoints": [959], "characters": "\u03BF" },+ "⦶": { "codepoints": [10678], "characters": "\u29B6" },+ "⊖": { "codepoints": [8854], "characters": "\u2296" },+ "𝕠": { "codepoints": [120160], "characters": "\uD835\uDD60" },+ "⦷": { "codepoints": [10679], "characters": "\u29B7" },+ "⦹": { "codepoints": [10681], "characters": "\u29B9" },+ "⊕": { "codepoints": [8853], "characters": "\u2295" },+ "∨": { "codepoints": [8744], "characters": "\u2228" },+ "↻": { "codepoints": [8635], "characters": "\u21BB" },+ "⩝": { "codepoints": [10845], "characters": "\u2A5D" },+ "ℴ": { "codepoints": [8500], "characters": "\u2134" },+ "ℴ": { "codepoints": [8500], "characters": "\u2134" },+ "ª": { "codepoints": [170], "characters": "\u00AA" },+ "ª": { "codepoints": [170], "characters": "\u00AA" },+ "º": { "codepoints": [186], "characters": "\u00BA" },+ "º": { "codepoints": [186], "characters": "\u00BA" },+ "⊶": { "codepoints": [8886], "characters": "\u22B6" },+ "⩖": { "codepoints": [10838], "characters": "\u2A56" },+ "⩗": { "codepoints": [10839], "characters": "\u2A57" },+ "⩛": { "codepoints": [10843], "characters": "\u2A5B" },+ "ℴ": { "codepoints": [8500], "characters": "\u2134" },+ "ø": { "codepoints": [248], "characters": "\u00F8" },+ "ø": { "codepoints": [248], "characters": "\u00F8" },+ "⊘": { "codepoints": [8856], "characters": "\u2298" },+ "õ": { "codepoints": [245], "characters": "\u00F5" },+ "õ": { "codepoints": [245], "characters": "\u00F5" },+ "⊗": { "codepoints": [8855], "characters": "\u2297" },+ "⨶": { "codepoints": [10806], "characters": "\u2A36" },+ "ö": { "codepoints": [246], "characters": "\u00F6" },+ "ö": { "codepoints": [246], "characters": "\u00F6" },+ "⌽": { "codepoints": [9021], "characters": "\u233D" },+ "∥": { "codepoints": [8741], "characters": "\u2225" },+ "¶": { "codepoints": [182], "characters": "\u00B6" },+ "¶": { "codepoints": [182], "characters": "\u00B6" },+ "∥": { "codepoints": [8741], "characters": "\u2225" },+ "⫳": { "codepoints": [10995], "characters": "\u2AF3" },+ "⫽": { "codepoints": [11005], "characters": "\u2AFD" },+ "∂": { "codepoints": [8706], "characters": "\u2202" },+ "п": { "codepoints": [1087], "characters": "\u043F" },+ "%": { "codepoints": [37], "characters": "\u0025" },+ ".": { "codepoints": [46], "characters": "\u002E" },+ "‰": { "codepoints": [8240], "characters": "\u2030" },+ "⊥": { "codepoints": [8869], "characters": "\u22A5" },+ "‱": { "codepoints": [8241], "characters": "\u2031" },+ "𝔭": { "codepoints": [120109], "characters": "\uD835\uDD2D" },+ "φ": { "codepoints": [966], "characters": "\u03C6" },+ "ϕ": { "codepoints": [981], "characters": "\u03D5" },+ "ℳ": { "codepoints": [8499], "characters": "\u2133" },+ "☎": { "codepoints": [9742], "characters": "\u260E" },+ "π": { "codepoints": [960], "characters": "\u03C0" },+ "⋔": { "codepoints": [8916], "characters": "\u22D4" },+ "ϖ": { "codepoints": [982], "characters": "\u03D6" },+ "ℏ": { "codepoints": [8463], "characters": "\u210F" },+ "ℎ": { "codepoints": [8462], "characters": "\u210E" },+ "ℏ": { "codepoints": [8463], "characters": "\u210F" },+ "+": { "codepoints": [43], "characters": "\u002B" },+ "⨣": { "codepoints": [10787], "characters": "\u2A23" },+ "⊞": { "codepoints": [8862], "characters": "\u229E" },+ "⨢": { "codepoints": [10786], "characters": "\u2A22" },+ "∔": { "codepoints": [8724], "characters": "\u2214" },+ "⨥": { "codepoints": [10789], "characters": "\u2A25" },+ "⩲": { "codepoints": [10866], "characters": "\u2A72" },+ "±": { "codepoints": [177], "characters": "\u00B1" },+ "±": { "codepoints": [177], "characters": "\u00B1" },+ "⨦": { "codepoints": [10790], "characters": "\u2A26" },+ "⨧": { "codepoints": [10791], "characters": "\u2A27" },+ "±": { "codepoints": [177], "characters": "\u00B1" },+ "⨕": { "codepoints": [10773], "characters": "\u2A15" },+ "𝕡": { "codepoints": [120161], "characters": "\uD835\uDD61" },+ "£": { "codepoints": [163], "characters": "\u00A3" },+ "£": { "codepoints": [163], "characters": "\u00A3" },+ "≺": { "codepoints": [8826], "characters": "\u227A" },+ "⪳": { "codepoints": [10931], "characters": "\u2AB3" },+ "⪷": { "codepoints": [10935], "characters": "\u2AB7" },+ "≼": { "codepoints": [8828], "characters": "\u227C" },+ "⪯": { "codepoints": [10927], "characters": "\u2AAF" },+ "≺": { "codepoints": [8826], "characters": "\u227A" },+ "⪷": { "codepoints": [10935], "characters": "\u2AB7" },+ "≼": { "codepoints": [8828], "characters": "\u227C" },+ "⪯": { "codepoints": [10927], "characters": "\u2AAF" },+ "⪹": { "codepoints": [10937], "characters": "\u2AB9" },+ "⪵": { "codepoints": [10933], "characters": "\u2AB5" },+ "⋨": { "codepoints": [8936], "characters": "\u22E8" },+ "≾": { "codepoints": [8830], "characters": "\u227E" },+ "′": { "codepoints": [8242], "characters": "\u2032" },+ "ℙ": { "codepoints": [8473], "characters": "\u2119" },+ "⪵": { "codepoints": [10933], "characters": "\u2AB5" },+ "⪹": { "codepoints": [10937], "characters": "\u2AB9" },+ "⋨": { "codepoints": [8936], "characters": "\u22E8" },+ "∏": { "codepoints": [8719], "characters": "\u220F" },+ "⌮": { "codepoints": [9006], "characters": "\u232E" },+ "⌒": { "codepoints": [8978], "characters": "\u2312" },+ "⌓": { "codepoints": [8979], "characters": "\u2313" },+ "∝": { "codepoints": [8733], "characters": "\u221D" },+ "∝": { "codepoints": [8733], "characters": "\u221D" },+ "≾": { "codepoints": [8830], "characters": "\u227E" },+ "⊰": { "codepoints": [8880], "characters": "\u22B0" },+ "𝓅": { "codepoints": [120005], "characters": "\uD835\uDCC5" },+ "ψ": { "codepoints": [968], "characters": "\u03C8" },+ " ": { "codepoints": [8200], "characters": "\u2008" },+ "𝔮": { "codepoints": [120110], "characters": "\uD835\uDD2E" },+ "⨌": { "codepoints": [10764], "characters": "\u2A0C" },+ "𝕢": { "codepoints": [120162], "characters": "\uD835\uDD62" },+ "⁗": { "codepoints": [8279], "characters": "\u2057" },+ "𝓆": { "codepoints": [120006], "characters": "\uD835\uDCC6" },+ "ℍ": { "codepoints": [8461], "characters": "\u210D" },+ "⨖": { "codepoints": [10774], "characters": "\u2A16" },+ "?": { "codepoints": [63], "characters": "\u003F" },+ "≟": { "codepoints": [8799], "characters": "\u225F" },+ """: { "codepoints": [34], "characters": "\u0022" },+ """: { "codepoints": [34], "characters": "\u0022" },+ "⇛": { "codepoints": [8667], "characters": "\u21DB" },+ "⇒": { "codepoints": [8658], "characters": "\u21D2" },+ "⤜": { "codepoints": [10524], "characters": "\u291C" },+ "⤏": { "codepoints": [10511], "characters": "\u290F" },+ "⥤": { "codepoints": [10596], "characters": "\u2964" },+ "∽̱": { "codepoints": [8765, 817], "characters": "\u223D\u0331" },+ "ŕ": { "codepoints": [341], "characters": "\u0155" },+ "√": { "codepoints": [8730], "characters": "\u221A" },+ "⦳": { "codepoints": [10675], "characters": "\u29B3" },+ "⟩": { "codepoints": [10217], "characters": "\u27E9" },+ "⦒": { "codepoints": [10642], "characters": "\u2992" },+ "⦥": { "codepoints": [10661], "characters": "\u29A5" },+ "⟩": { "codepoints": [10217], "characters": "\u27E9" },+ "»": { "codepoints": [187], "characters": "\u00BB" },+ "»": { "codepoints": [187], "characters": "\u00BB" },+ "→": { "codepoints": [8594], "characters": "\u2192" },+ "⥵": { "codepoints": [10613], "characters": "\u2975" },+ "⇥": { "codepoints": [8677], "characters": "\u21E5" },+ "⤠": { "codepoints": [10528], "characters": "\u2920" },+ "⤳": { "codepoints": [10547], "characters": "\u2933" },+ "⤞": { "codepoints": [10526], "characters": "\u291E" },+ "↪": { "codepoints": [8618], "characters": "\u21AA" },+ "↬": { "codepoints": [8620], "characters": "\u21AC" },+ "⥅": { "codepoints": [10565], "characters": "\u2945" },+ "⥴": { "codepoints": [10612], "characters": "\u2974" },+ "↣": { "codepoints": [8611], "characters": "\u21A3" },+ "↝": { "codepoints": [8605], "characters": "\u219D" },+ "⤚": { "codepoints": [10522], "characters": "\u291A" },+ "∶": { "codepoints": [8758], "characters": "\u2236" },+ "ℚ": { "codepoints": [8474], "characters": "\u211A" },+ "⤍": { "codepoints": [10509], "characters": "\u290D" },+ "❳": { "codepoints": [10099], "characters": "\u2773" },+ "}": { "codepoints": [125], "characters": "\u007D" },+ "]": { "codepoints": [93], "characters": "\u005D" },+ "⦌": { "codepoints": [10636], "characters": "\u298C" },+ "⦎": { "codepoints": [10638], "characters": "\u298E" },+ "⦐": { "codepoints": [10640], "characters": "\u2990" },+ "ř": { "codepoints": [345], "characters": "\u0159" },+ "ŗ": { "codepoints": [343], "characters": "\u0157" },+ "⌉": { "codepoints": [8969], "characters": "\u2309" },+ "}": { "codepoints": [125], "characters": "\u007D" },+ "р": { "codepoints": [1088], "characters": "\u0440" },+ "⤷": { "codepoints": [10551], "characters": "\u2937" },+ "⥩": { "codepoints": [10601], "characters": "\u2969" },+ "”": { "codepoints": [8221], "characters": "\u201D" },+ "”": { "codepoints": [8221], "characters": "\u201D" },+ "↳": { "codepoints": [8627], "characters": "\u21B3" },+ "ℜ": { "codepoints": [8476], "characters": "\u211C" },+ "ℛ": { "codepoints": [8475], "characters": "\u211B" },+ "ℜ": { "codepoints": [8476], "characters": "\u211C" },+ "ℝ": { "codepoints": [8477], "characters": "\u211D" },+ "▭": { "codepoints": [9645], "characters": "\u25AD" },+ "®": { "codepoints": [174], "characters": "\u00AE" },+ "®": { "codepoints": [174], "characters": "\u00AE" },+ "⥽": { "codepoints": [10621], "characters": "\u297D" },+ "⌋": { "codepoints": [8971], "characters": "\u230B" },+ "𝔯": { "codepoints": [120111], "characters": "\uD835\uDD2F" },+ "⇁": { "codepoints": [8641], "characters": "\u21C1" },+ "⇀": { "codepoints": [8640], "characters": "\u21C0" },+ "⥬": { "codepoints": [10604], "characters": "\u296C" },+ "ρ": { "codepoints": [961], "characters": "\u03C1" },+ "ϱ": { "codepoints": [1009], "characters": "\u03F1" },+ "→": { "codepoints": [8594], "characters": "\u2192" },+ "↣": { "codepoints": [8611], "characters": "\u21A3" },+ "⇁": { "codepoints": [8641], "characters": "\u21C1" },+ "⇀": { "codepoints": [8640], "characters": "\u21C0" },+ "⇄": { "codepoints": [8644], "characters": "\u21C4" },+ "⇌": { "codepoints": [8652], "characters": "\u21CC" },+ "⇉": { "codepoints": [8649], "characters": "\u21C9" },+ "↝": { "codepoints": [8605], "characters": "\u219D" },+ "⋌": { "codepoints": [8908], "characters": "\u22CC" },+ "˚": { "codepoints": [730], "characters": "\u02DA" },+ "≓": { "codepoints": [8787], "characters": "\u2253" },+ "⇄": { "codepoints": [8644], "characters": "\u21C4" },+ "⇌": { "codepoints": [8652], "characters": "\u21CC" },+ "‏": { "codepoints": [8207], "characters": "\u200F" },+ "⎱": { "codepoints": [9137], "characters": "\u23B1" },+ "⎱": { "codepoints": [9137], "characters": "\u23B1" },+ "⫮": { "codepoints": [10990], "characters": "\u2AEE" },+ "⟭": { "codepoints": [10221], "characters": "\u27ED" },+ "⇾": { "codepoints": [8702], "characters": "\u21FE" },+ "⟧": { "codepoints": [10215], "characters": "\u27E7" },+ "⦆": { "codepoints": [10630], "characters": "\u2986" },+ "𝕣": { "codepoints": [120163], "characters": "\uD835\uDD63" },+ "⨮": { "codepoints": [10798], "characters": "\u2A2E" },+ "⨵": { "codepoints": [10805], "characters": "\u2A35" },+ ")": { "codepoints": [41], "characters": "\u0029" },+ "⦔": { "codepoints": [10644], "characters": "\u2994" },+ "⨒": { "codepoints": [10770], "characters": "\u2A12" },+ "⇉": { "codepoints": [8649], "characters": "\u21C9" },+ "›": { "codepoints": [8250], "characters": "\u203A" },+ "𝓇": { "codepoints": [120007], "characters": "\uD835\uDCC7" },+ "↱": { "codepoints": [8625], "characters": "\u21B1" },+ "]": { "codepoints": [93], "characters": "\u005D" },+ "’": { "codepoints": [8217], "characters": "\u2019" },+ "’": { "codepoints": [8217], "characters": "\u2019" },+ "⋌": { "codepoints": [8908], "characters": "\u22CC" },+ "⋊": { "codepoints": [8906], "characters": "\u22CA" },+ "▹": { "codepoints": [9657], "characters": "\u25B9" },+ "⊵": { "codepoints": [8885], "characters": "\u22B5" },+ "▸": { "codepoints": [9656], "characters": "\u25B8" },+ "⧎": { "codepoints": [10702], "characters": "\u29CE" },+ "⥨": { "codepoints": [10600], "characters": "\u2968" },+ "℞": { "codepoints": [8478], "characters": "\u211E" },+ "ś": { "codepoints": [347], "characters": "\u015B" },+ "‚": { "codepoints": [8218], "characters": "\u201A" },+ "≻": { "codepoints": [8827], "characters": "\u227B" },+ "⪴": { "codepoints": [10932], "characters": "\u2AB4" },+ "⪸": { "codepoints": [10936], "characters": "\u2AB8" },+ "š": { "codepoints": [353], "characters": "\u0161" },+ "≽": { "codepoints": [8829], "characters": "\u227D" },+ "⪰": { "codepoints": [10928], "characters": "\u2AB0" },+ "ş": { "codepoints": [351], "characters": "\u015F" },+ "ŝ": { "codepoints": [349], "characters": "\u015D" },+ "⪶": { "codepoints": [10934], "characters": "\u2AB6" },+ "⪺": { "codepoints": [10938], "characters": "\u2ABA" },+ "⋩": { "codepoints": [8937], "characters": "\u22E9" },+ "⨓": { "codepoints": [10771], "characters": "\u2A13" },+ "≿": { "codepoints": [8831], "characters": "\u227F" },+ "с": { "codepoints": [1089], "characters": "\u0441" },+ "⋅": { "codepoints": [8901], "characters": "\u22C5" },+ "⊡": { "codepoints": [8865], "characters": "\u22A1" },+ "⩦": { "codepoints": [10854], "characters": "\u2A66" },+ "⇘": { "codepoints": [8664], "characters": "\u21D8" },+ "⤥": { "codepoints": [10533], "characters": "\u2925" },+ "↘": { "codepoints": [8600], "characters": "\u2198" },+ "↘": { "codepoints": [8600], "characters": "\u2198" },+ "§": { "codepoints": [167], "characters": "\u00A7" },+ "§": { "codepoints": [167], "characters": "\u00A7" },+ ";": { "codepoints": [59], "characters": "\u003B" },+ "⤩": { "codepoints": [10537], "characters": "\u2929" },+ "∖": { "codepoints": [8726], "characters": "\u2216" },+ "∖": { "codepoints": [8726], "characters": "\u2216" },+ "✶": { "codepoints": [10038], "characters": "\u2736" },+ "𝔰": { "codepoints": [120112], "characters": "\uD835\uDD30" },+ "⌢": { "codepoints": [8994], "characters": "\u2322" },+ "♯": { "codepoints": [9839], "characters": "\u266F" },+ "щ": { "codepoints": [1097], "characters": "\u0449" },+ "ш": { "codepoints": [1096], "characters": "\u0448" },+ "∣": { "codepoints": [8739], "characters": "\u2223" },+ "∥": { "codepoints": [8741], "characters": "\u2225" },+ "­": { "codepoints": [173], "characters": "\u00AD" },+ "­": { "codepoints": [173], "characters": "\u00AD" },+ "σ": { "codepoints": [963], "characters": "\u03C3" },+ "ς": { "codepoints": [962], "characters": "\u03C2" },+ "ς": { "codepoints": [962], "characters": "\u03C2" },+ "∼": { "codepoints": [8764], "characters": "\u223C" },+ "⩪": { "codepoints": [10858], "characters": "\u2A6A" },+ "≃": { "codepoints": [8771], "characters": "\u2243" },+ "≃": { "codepoints": [8771], "characters": "\u2243" },+ "⪞": { "codepoints": [10910], "characters": "\u2A9E" },+ "⪠": { "codepoints": [10912], "characters": "\u2AA0" },+ "⪝": { "codepoints": [10909], "characters": "\u2A9D" },+ "⪟": { "codepoints": [10911], "characters": "\u2A9F" },+ "≆": { "codepoints": [8774], "characters": "\u2246" },+ "⨤": { "codepoints": [10788], "characters": "\u2A24" },+ "⥲": { "codepoints": [10610], "characters": "\u2972" },+ "←": { "codepoints": [8592], "characters": "\u2190" },+ "∖": { "codepoints": [8726], "characters": "\u2216" },+ "⨳": { "codepoints": [10803], "characters": "\u2A33" },+ "⧤": { "codepoints": [10724], "characters": "\u29E4" },+ "∣": { "codepoints": [8739], "characters": "\u2223" },+ "⌣": { "codepoints": [8995], "characters": "\u2323" },+ "⪪": { "codepoints": [10922], "characters": "\u2AAA" },+ "⪬": { "codepoints": [10924], "characters": "\u2AAC" },+ "⪬︀": { "codepoints": [10924, 65024], "characters": "\u2AAC\uFE00" },+ "ь": { "codepoints": [1100], "characters": "\u044C" },+ "/": { "codepoints": [47], "characters": "\u002F" },+ "⧄": { "codepoints": [10692], "characters": "\u29C4" },+ "⌿": { "codepoints": [9023], "characters": "\u233F" },+ "𝕤": { "codepoints": [120164], "characters": "\uD835\uDD64" },+ "♠": { "codepoints": [9824], "characters": "\u2660" },+ "♠": { "codepoints": [9824], "characters": "\u2660" },+ "∥": { "codepoints": [8741], "characters": "\u2225" },+ "⊓": { "codepoints": [8851], "characters": "\u2293" },+ "⊓︀": { "codepoints": [8851, 65024], "characters": "\u2293\uFE00" },+ "⊔": { "codepoints": [8852], "characters": "\u2294" },+ "⊔︀": { "codepoints": [8852, 65024], "characters": "\u2294\uFE00" },+ "⊏": { "codepoints": [8847], "characters": "\u228F" },+ "⊑": { "codepoints": [8849], "characters": "\u2291" },+ "⊏": { "codepoints": [8847], "characters": "\u228F" },+ "⊑": { "codepoints": [8849], "characters": "\u2291" },+ "⊐": { "codepoints": [8848], "characters": "\u2290" },+ "⊒": { "codepoints": [8850], "characters": "\u2292" },+ "⊐": { "codepoints": [8848], "characters": "\u2290" },+ "⊒": { "codepoints": [8850], "characters": "\u2292" },+ "□": { "codepoints": [9633], "characters": "\u25A1" },+ "□": { "codepoints": [9633], "characters": "\u25A1" },+ "▪": { "codepoints": [9642], "characters": "\u25AA" },+ "▪": { "codepoints": [9642], "characters": "\u25AA" },+ "→": { "codepoints": [8594], "characters": "\u2192" },+ "𝓈": { "codepoints": [120008], "characters": "\uD835\uDCC8" },+ "∖": { "codepoints": [8726], "characters": "\u2216" },+ "⌣": { "codepoints": [8995], "characters": "\u2323" },+ "⋆": { "codepoints": [8902], "characters": "\u22C6" },+ "☆": { "codepoints": [9734], "characters": "\u2606" },+ "★": { "codepoints": [9733], "characters": "\u2605" },+ "ϵ": { "codepoints": [1013], "characters": "\u03F5" },+ "ϕ": { "codepoints": [981], "characters": "\u03D5" },+ "¯": { "codepoints": [175], "characters": "\u00AF" },+ "⊂": { "codepoints": [8834], "characters": "\u2282" },+ "⫅": { "codepoints": [10949], "characters": "\u2AC5" },+ "⪽": { "codepoints": [10941], "characters": "\u2ABD" },+ "⊆": { "codepoints": [8838], "characters": "\u2286" },+ "⫃": { "codepoints": [10947], "characters": "\u2AC3" },+ "⫁": { "codepoints": [10945], "characters": "\u2AC1" },+ "⫋": { "codepoints": [10955], "characters": "\u2ACB" },+ "⊊": { "codepoints": [8842], "characters": "\u228A" },+ "⪿": { "codepoints": [10943], "characters": "\u2ABF" },+ "⥹": { "codepoints": [10617], "characters": "\u2979" },+ "⊂": { "codepoints": [8834], "characters": "\u2282" },+ "⊆": { "codepoints": [8838], "characters": "\u2286" },+ "⫅": { "codepoints": [10949], "characters": "\u2AC5" },+ "⊊": { "codepoints": [8842], "characters": "\u228A" },+ "⫋": { "codepoints": [10955], "characters": "\u2ACB" },+ "⫇": { "codepoints": [10951], "characters": "\u2AC7" },+ "⫕": { "codepoints": [10965], "characters": "\u2AD5" },+ "⫓": { "codepoints": [10963], "characters": "\u2AD3" },+ "≻": { "codepoints": [8827], "characters": "\u227B" },+ "⪸": { "codepoints": [10936], "characters": "\u2AB8" },+ "≽": { "codepoints": [8829], "characters": "\u227D" },+ "⪰": { "codepoints": [10928], "characters": "\u2AB0" },+ "⪺": { "codepoints": [10938], "characters": "\u2ABA" },+ "⪶": { "codepoints": [10934], "characters": "\u2AB6" },+ "⋩": { "codepoints": [8937], "characters": "\u22E9" },+ "≿": { "codepoints": [8831], "characters": "\u227F" },+ "∑": { "codepoints": [8721], "characters": "\u2211" },+ "♪": { "codepoints": [9834], "characters": "\u266A" },+ "¹": { "codepoints": [185], "characters": "\u00B9" },+ "¹": { "codepoints": [185], "characters": "\u00B9" },+ "²": { "codepoints": [178], "characters": "\u00B2" },+ "²": { "codepoints": [178], "characters": "\u00B2" },+ "³": { "codepoints": [179], "characters": "\u00B3" },+ "³": { "codepoints": [179], "characters": "\u00B3" },+ "⊃": { "codepoints": [8835], "characters": "\u2283" },+ "⫆": { "codepoints": [10950], "characters": "\u2AC6" },+ "⪾": { "codepoints": [10942], "characters": "\u2ABE" },+ "⫘": { "codepoints": [10968], "characters": "\u2AD8" },+ "⊇": { "codepoints": [8839], "characters": "\u2287" },+ "⫄": { "codepoints": [10948], "characters": "\u2AC4" },+ "⟉": { "codepoints": [10185], "characters": "\u27C9" },+ "⫗": { "codepoints": [10967], "characters": "\u2AD7" },+ "⥻": { "codepoints": [10619], "characters": "\u297B" },+ "⫂": { "codepoints": [10946], "characters": "\u2AC2" },+ "⫌": { "codepoints": [10956], "characters": "\u2ACC" },+ "⊋": { "codepoints": [8843], "characters": "\u228B" },+ "⫀": { "codepoints": [10944], "characters": "\u2AC0" },+ "⊃": { "codepoints": [8835], "characters": "\u2283" },+ "⊇": { "codepoints": [8839], "characters": "\u2287" },+ "⫆": { "codepoints": [10950], "characters": "\u2AC6" },+ "⊋": { "codepoints": [8843], "characters": "\u228B" },+ "⫌": { "codepoints": [10956], "characters": "\u2ACC" },+ "⫈": { "codepoints": [10952], "characters": "\u2AC8" },+ "⫔": { "codepoints": [10964], "characters": "\u2AD4" },+ "⫖": { "codepoints": [10966], "characters": "\u2AD6" },+ "⇙": { "codepoints": [8665], "characters": "\u21D9" },+ "⤦": { "codepoints": [10534], "characters": "\u2926" },+ "↙": { "codepoints": [8601], "characters": "\u2199" },+ "↙": { "codepoints": [8601], "characters": "\u2199" },+ "⤪": { "codepoints": [10538], "characters": "\u292A" },+ "ß": { "codepoints": [223], "characters": "\u00DF" },+ "ß": { "codepoints": [223], "characters": "\u00DF" },+ "⌖": { "codepoints": [8982], "characters": "\u2316" },+ "τ": { "codepoints": [964], "characters": "\u03C4" },+ "⎴": { "codepoints": [9140], "characters": "\u23B4" },+ "ť": { "codepoints": [357], "characters": "\u0165" },+ "ţ": { "codepoints": [355], "characters": "\u0163" },+ "т": { "codepoints": [1090], "characters": "\u0442" },+ "⃛": { "codepoints": [8411], "characters": "\u20DB" },+ "⌕": { "codepoints": [8981], "characters": "\u2315" },+ "𝔱": { "codepoints": [120113], "characters": "\uD835\uDD31" },+ "∴": { "codepoints": [8756], "characters": "\u2234" },+ "∴": { "codepoints": [8756], "characters": "\u2234" },+ "θ": { "codepoints": [952], "characters": "\u03B8" },+ "ϑ": { "codepoints": [977], "characters": "\u03D1" },+ "ϑ": { "codepoints": [977], "characters": "\u03D1" },+ "≈": { "codepoints": [8776], "characters": "\u2248" },+ "∼": { "codepoints": [8764], "characters": "\u223C" },+ " ": { "codepoints": [8201], "characters": "\u2009" },+ "≈": { "codepoints": [8776], "characters": "\u2248" },+ "∼": { "codepoints": [8764], "characters": "\u223C" },+ "þ": { "codepoints": [254], "characters": "\u00FE" },+ "þ": { "codepoints": [254], "characters": "\u00FE" },+ "˜": { "codepoints": [732], "characters": "\u02DC" },+ "×": { "codepoints": [215], "characters": "\u00D7" },+ "×": { "codepoints": [215], "characters": "\u00D7" },+ "⊠": { "codepoints": [8864], "characters": "\u22A0" },+ "⨱": { "codepoints": [10801], "characters": "\u2A31" },+ "⨰": { "codepoints": [10800], "characters": "\u2A30" },+ "∭": { "codepoints": [8749], "characters": "\u222D" },+ "⤨": { "codepoints": [10536], "characters": "\u2928" },+ "⊤": { "codepoints": [8868], "characters": "\u22A4" },+ "⌶": { "codepoints": [9014], "characters": "\u2336" },+ "⫱": { "codepoints": [10993], "characters": "\u2AF1" },+ "𝕥": { "codepoints": [120165], "characters": "\uD835\uDD65" },+ "⫚": { "codepoints": [10970], "characters": "\u2ADA" },+ "⤩": { "codepoints": [10537], "characters": "\u2929" },+ "‴": { "codepoints": [8244], "characters": "\u2034" },+ "™": { "codepoints": [8482], "characters": "\u2122" },+ "▵": { "codepoints": [9653], "characters": "\u25B5" },+ "▿": { "codepoints": [9663], "characters": "\u25BF" },+ "◃": { "codepoints": [9667], "characters": "\u25C3" },+ "⊴": { "codepoints": [8884], "characters": "\u22B4" },+ "≜": { "codepoints": [8796], "characters": "\u225C" },+ "▹": { "codepoints": [9657], "characters": "\u25B9" },+ "⊵": { "codepoints": [8885], "characters": "\u22B5" },+ "◬": { "codepoints": [9708], "characters": "\u25EC" },+ "≜": { "codepoints": [8796], "characters": "\u225C" },+ "⨺": { "codepoints": [10810], "characters": "\u2A3A" },+ "⨹": { "codepoints": [10809], "characters": "\u2A39" },+ "⧍": { "codepoints": [10701], "characters": "\u29CD" },+ "⨻": { "codepoints": [10811], "characters": "\u2A3B" },+ "⏢": { "codepoints": [9186], "characters": "\u23E2" },+ "𝓉": { "codepoints": [120009], "characters": "\uD835\uDCC9" },+ "ц": { "codepoints": [1094], "characters": "\u0446" },+ "ћ": { "codepoints": [1115], "characters": "\u045B" },+ "ŧ": { "codepoints": [359], "characters": "\u0167" },+ "≬": { "codepoints": [8812], "characters": "\u226C" },+ "↞": { "codepoints": [8606], "characters": "\u219E" },+ "↠": { "codepoints": [8608], "characters": "\u21A0" },+ "⇑": { "codepoints": [8657], "characters": "\u21D1" },+ "⥣": { "codepoints": [10595], "characters": "\u2963" },+ "ú": { "codepoints": [250], "characters": "\u00FA" },+ "ú": { "codepoints": [250], "characters": "\u00FA" },+ "↑": { "codepoints": [8593], "characters": "\u2191" },+ "ў": { "codepoints": [1118], "characters": "\u045E" },+ "ŭ": { "codepoints": [365], "characters": "\u016D" },+ "û": { "codepoints": [251], "characters": "\u00FB" },+ "û": { "codepoints": [251], "characters": "\u00FB" },+ "у": { "codepoints": [1091], "characters": "\u0443" },+ "⇅": { "codepoints": [8645], "characters": "\u21C5" },+ "ű": { "codepoints": [369], "characters": "\u0171" },+ "⥮": { "codepoints": [10606], "characters": "\u296E" },+ "⥾": { "codepoints": [10622], "characters": "\u297E" },+ "𝔲": { "codepoints": [120114], "characters": "\uD835\uDD32" },+ "ù": { "codepoints": [249], "characters": "\u00F9" },+ "ù": { "codepoints": [249], "characters": "\u00F9" },+ "↿": { "codepoints": [8639], "characters": "\u21BF" },+ "↾": { "codepoints": [8638], "characters": "\u21BE" },+ "▀": { "codepoints": [9600], "characters": "\u2580" },+ "⌜": { "codepoints": [8988], "characters": "\u231C" },+ "⌜": { "codepoints": [8988], "characters": "\u231C" },+ "⌏": { "codepoints": [8975], "characters": "\u230F" },+ "◸": { "codepoints": [9720], "characters": "\u25F8" },+ "ū": { "codepoints": [363], "characters": "\u016B" },+ "¨": { "codepoints": [168], "characters": "\u00A8" },+ "¨": { "codepoints": [168], "characters": "\u00A8" },+ "ų": { "codepoints": [371], "characters": "\u0173" },+ "𝕦": { "codepoints": [120166], "characters": "\uD835\uDD66" },+ "↑": { "codepoints": [8593], "characters": "\u2191" },+ "↕": { "codepoints": [8597], "characters": "\u2195" },+ "↿": { "codepoints": [8639], "characters": "\u21BF" },+ "↾": { "codepoints": [8638], "characters": "\u21BE" },+ "⊎": { "codepoints": [8846], "characters": "\u228E" },+ "υ": { "codepoints": [965], "characters": "\u03C5" },+ "ϒ": { "codepoints": [978], "characters": "\u03D2" },+ "υ": { "codepoints": [965], "characters": "\u03C5" },+ "⇈": { "codepoints": [8648], "characters": "\u21C8" },+ "⌝": { "codepoints": [8989], "characters": "\u231D" },+ "⌝": { "codepoints": [8989], "characters": "\u231D" },+ "⌎": { "codepoints": [8974], "characters": "\u230E" },+ "ů": { "codepoints": [367], "characters": "\u016F" },+ "◹": { "codepoints": [9721], "characters": "\u25F9" },+ "𝓊": { "codepoints": [120010], "characters": "\uD835\uDCCA" },+ "⋰": { "codepoints": [8944], "characters": "\u22F0" },+ "ũ": { "codepoints": [361], "characters": "\u0169" },+ "▵": { "codepoints": [9653], "characters": "\u25B5" },+ "▴": { "codepoints": [9652], "characters": "\u25B4" },+ "⇈": { "codepoints": [8648], "characters": "\u21C8" },+ "ü": { "codepoints": [252], "characters": "\u00FC" },+ "ü": { "codepoints": [252], "characters": "\u00FC" },+ "⦧": { "codepoints": [10663], "characters": "\u29A7" },+ "⇕": { "codepoints": [8661], "characters": "\u21D5" },+ "⫨": { "codepoints": [10984], "characters": "\u2AE8" },+ "⫩": { "codepoints": [10985], "characters": "\u2AE9" },+ "⊨": { "codepoints": [8872], "characters": "\u22A8" },+ "⦜": { "codepoints": [10652], "characters": "\u299C" },+ "ϵ": { "codepoints": [1013], "characters": "\u03F5" },+ "ϰ": { "codepoints": [1008], "characters": "\u03F0" },+ "∅": { "codepoints": [8709], "characters": "\u2205" },+ "ϕ": { "codepoints": [981], "characters": "\u03D5" },+ "ϖ": { "codepoints": [982], "characters": "\u03D6" },+ "∝": { "codepoints": [8733], "characters": "\u221D" },+ "↕": { "codepoints": [8597], "characters": "\u2195" },+ "ϱ": { "codepoints": [1009], "characters": "\u03F1" },+ "ς": { "codepoints": [962], "characters": "\u03C2" },+ "⊊︀": { "codepoints": [8842, 65024], "characters": "\u228A\uFE00" },+ "⫋︀": { "codepoints": [10955, 65024], "characters": "\u2ACB\uFE00" },+ "⊋︀": { "codepoints": [8843, 65024], "characters": "\u228B\uFE00" },+ "⫌︀": { "codepoints": [10956, 65024], "characters": "\u2ACC\uFE00" },+ "ϑ": { "codepoints": [977], "characters": "\u03D1" },+ "⊲": { "codepoints": [8882], "characters": "\u22B2" },+ "⊳": { "codepoints": [8883], "characters": "\u22B3" },+ "в": { "codepoints": [1074], "characters": "\u0432" },+ "⊢": { "codepoints": [8866], "characters": "\u22A2" },+ "∨": { "codepoints": [8744], "characters": "\u2228" },+ "⊻": { "codepoints": [8891], "characters": "\u22BB" },+ "≚": { "codepoints": [8794], "characters": "\u225A" },+ "⋮": { "codepoints": [8942], "characters": "\u22EE" },+ "|": { "codepoints": [124], "characters": "\u007C" },+ "|": { "codepoints": [124], "characters": "\u007C" },+ "𝔳": { "codepoints": [120115], "characters": "\uD835\uDD33" },+ "⊲": { "codepoints": [8882], "characters": "\u22B2" },+ "⊂⃒": { "codepoints": [8834, 8402], "characters": "\u2282\u20D2" },+ "⊃⃒": { "codepoints": [8835, 8402], "characters": "\u2283\u20D2" },+ "𝕧": { "codepoints": [120167], "characters": "\uD835\uDD67" },+ "∝": { "codepoints": [8733], "characters": "\u221D" },+ "⊳": { "codepoints": [8883], "characters": "\u22B3" },+ "𝓋": { "codepoints": [120011], "characters": "\uD835\uDCCB" },+ "⫋︀": { "codepoints": [10955, 65024], "characters": "\u2ACB\uFE00" },+ "⊊︀": { "codepoints": [8842, 65024], "characters": "\u228A\uFE00" },+ "⫌︀": { "codepoints": [10956, 65024], "characters": "\u2ACC\uFE00" },+ "⊋︀": { "codepoints": [8843, 65024], "characters": "\u228B\uFE00" },+ "⦚": { "codepoints": [10650], "characters": "\u299A" },+ "ŵ": { "codepoints": [373], "characters": "\u0175" },+ "⩟": { "codepoints": [10847], "characters": "\u2A5F" },+ "∧": { "codepoints": [8743], "characters": "\u2227" },+ "≙": { "codepoints": [8793], "characters": "\u2259" },+ "℘": { "codepoints": [8472], "characters": "\u2118" },+ "𝔴": { "codepoints": [120116], "characters": "\uD835\uDD34" },+ "𝕨": { "codepoints": [120168], "characters": "\uD835\uDD68" },+ "℘": { "codepoints": [8472], "characters": "\u2118" },+ "≀": { "codepoints": [8768], "characters": "\u2240" },+ "≀": { "codepoints": [8768], "characters": "\u2240" },+ "𝓌": { "codepoints": [120012], "characters": "\uD835\uDCCC" },+ "⋂": { "codepoints": [8898], "characters": "\u22C2" },+ "◯": { "codepoints": [9711], "characters": "\u25EF" },+ "⋃": { "codepoints": [8899], "characters": "\u22C3" },+ "▽": { "codepoints": [9661], "characters": "\u25BD" },+ "𝔵": { "codepoints": [120117], "characters": "\uD835\uDD35" },+ "⟺": { "codepoints": [10234], "characters": "\u27FA" },+ "⟷": { "codepoints": [10231], "characters": "\u27F7" },+ "ξ": { "codepoints": [958], "characters": "\u03BE" },+ "⟸": { "codepoints": [10232], "characters": "\u27F8" },+ "⟵": { "codepoints": [10229], "characters": "\u27F5" },+ "⟼": { "codepoints": [10236], "characters": "\u27FC" },+ "⋻": { "codepoints": [8955], "characters": "\u22FB" },+ "⨀": { "codepoints": [10752], "characters": "\u2A00" },+ "𝕩": { "codepoints": [120169], "characters": "\uD835\uDD69" },+ "⨁": { "codepoints": [10753], "characters": "\u2A01" },+ "⨂": { "codepoints": [10754], "characters": "\u2A02" },+ "⟹": { "codepoints": [10233], "characters": "\u27F9" },+ "⟶": { "codepoints": [10230], "characters": "\u27F6" },+ "𝓍": { "codepoints": [120013], "characters": "\uD835\uDCCD" },+ "⨆": { "codepoints": [10758], "characters": "\u2A06" },+ "⨄": { "codepoints": [10756], "characters": "\u2A04" },+ "△": { "codepoints": [9651], "characters": "\u25B3" },+ "⋁": { "codepoints": [8897], "characters": "\u22C1" },+ "⋀": { "codepoints": [8896], "characters": "\u22C0" },+ "ý": { "codepoints": [253], "characters": "\u00FD" },+ "ý": { "codepoints": [253], "characters": "\u00FD" },+ "я": { "codepoints": [1103], "characters": "\u044F" },+ "ŷ": { "codepoints": [375], "characters": "\u0177" },+ "ы": { "codepoints": [1099], "characters": "\u044B" },+ "¥": { "codepoints": [165], "characters": "\u00A5" },+ "¥": { "codepoints": [165], "characters": "\u00A5" },+ "𝔶": { "codepoints": [120118], "characters": "\uD835\uDD36" },+ "ї": { "codepoints": [1111], "characters": "\u0457" },+ "𝕪": { "codepoints": [120170], "characters": "\uD835\uDD6A" },+ "𝓎": { "codepoints": [120014], "characters": "\uD835\uDCCE" },+ "ю": { "codepoints": [1102], "characters": "\u044E" },+ "ÿ": { "codepoints": [255], "characters": "\u00FF" },+ "ÿ": { "codepoints": [255], "characters": "\u00FF" },+ "ź": { "codepoints": [378], "characters": "\u017A" },+ "ž": { "codepoints": [382], "characters": "\u017E" },+ "з": { "codepoints": [1079], "characters": "\u0437" },+ "ż": { "codepoints": [380], "characters": "\u017C" },+ "ℨ": { "codepoints": [8488], "characters": "\u2128" },+ "ζ": { "codepoints": [950], "characters": "\u03B6" },+ "𝔷": { "codepoints": [120119], "characters": "\uD835\uDD37" },+ "ж": { "codepoints": [1078], "characters": "\u0436" },+ "⇝": { "codepoints": [8669], "characters": "\u21DD" },+ "𝕫": { "codepoints": [120171], "characters": "\uD835\uDD6B" },+ "𝓏": { "codepoints": [120015], "characters": "\uD835\uDCCF" },+ "‍": { "codepoints": [8205], "characters": "\u200D" },+ "‌": { "codepoints": [8204], "characters": "\u200C" }+}
+ mangrove.cabal view
@@ -0,0 +1,223 @@+cabal-version: 2.2++name: mangrove+author: Sam May+category: Web+synopsis: A parser for web documents according to the HTML5 specification.++version: 0.1.0.0+stability: provisional+maintainer: sam@eitilt.life++copyright: © 2020-2021 Sam May+license: MPL-2.0+license-file: LICENSE++extra-source-files: CHANGELOG.md+ , README.md+ , README.project.md+data-dir: data+data-files: entities.json++homepage: https://ag.eitilt.life/willow+bug-reports: mailto:ag@eitilt.life++description: `mangrove` provides HTML parsing for the Willow web+ browser suite. As such, it has not necessarily been+ written with a broader audience in mind, but the resulting+ data structures should still be generic enough to serve as+ a general parsing library should you need HTML5+ compatibility (most likely, its codified error recovery+ algorithms); if you do use this for other projects, please+ do share any issues —or even just discomforts— that+ broader usage reveals. Notably, however, `mangrove` makes+ no attempt to parse CSS, JavaScript, or to access linked+ files, leaving those tasks to other parts of the suite and+ merely generates a simple document tree from the markup.++build-type: Simple+tested-with: GHC ==8.2.2, GHC ==8.4.4, GHC ==8.6.5, GHC ==8.8.4, GHC ==8.10.3+++source-repository head+ type: darcs+ location: https://darcs.eitilt.life/willow+ subdir: mangrove++source-repository this+ type: darcs+ location: https://darcs.eitilt.life/willow+ subdir: mangrove+ tag: v/html/0.1.0.0.+++source-repository head+ type: darcs+ location: https://hub.darcs.net/ag.eitilt/willow+ subdir: mangrove++source-repository this+ type: darcs+ location: https://hub.darcs.net/ag.eitilt/willow+ subdir: mangrove+ tag: v/html/0.1.0.0.+++flag dev+ description: Trigger stricter behaviour for development+ default: False+ manual: True++flag html5lib+ default: False+ description: Enable the html5lib tests, which require manually+ downloading the test data.++flag wpt+ default: False+ description: Enable integration with web-platform-tests suite, greatly+ expanding the test surface at the expense of requiring the+ manual download of the (large) test data repository.+++common flags-ghc+ if flag(dev)+ ghc-options: -Weverything+ -Wno-unrecognised-pragmas+ -Wno-implicit-prelude -Wno-missing-import-lists+ -Wno-missing-exported-signatures -Wno-missing-local-signatures+ -Wno-missed-specialisations -Wno-all-missed-specialisations+ -Wno-safe -Wno-unsafe+ -Wno-monomorphism-restriction+ if impl(ghc >= 8.4.1)+ ghc-options: -Wno-missing-export-lists+ if impl(ghc >= 8.8.1)+ ghc-options: -Wno-missing-deriving-strategies+ if impl(ghc >= 8.10.1)+ ghc-options: -Wno-missing-safe-haskell-mode+ -Wno-prepositive-qualified-module+ else+ ghc-options: -Wno-unrecognised-pragmas+++library+ import: flags-ghc+ hs-source-dirs: src+ exposed-modules: Web.Mangrove.Parse.Common.Error+ , Web.Mangrove.Parse.Encoding.Preprocess+ , Web.Mangrove.Parse.Tokenize+ , Web.Mangrove.Parse.Tree+ other-modules: Paths_mangrove+ , Web.Mangrove.Parse.Common.Character+ , Web.Mangrove.Parse.Tokenize.CData+ , Web.Mangrove.Parse.Tokenize.Character+ , Web.Mangrove.Parse.Tokenize.Common+ , Web.Mangrove.Parse.Tokenize.Comment+ , Web.Mangrove.Parse.Tokenize.Data+ , Web.Mangrove.Parse.Tokenize.Dispatcher+ , Web.Mangrove.Parse.Tokenize.Doctype+ , Web.Mangrove.Parse.Tokenize.RawText+ , Web.Mangrove.Parse.Tokenize.RCData+ , Web.Mangrove.Parse.Tokenize.PlainText+ , Web.Mangrove.Parse.Tokenize.ScriptData+ , Web.Mangrove.Parse.Tokenize.ScriptDataEscaped+ , Web.Mangrove.Parse.Tokenize.ScriptDataDoubleEscaped+ , Web.Mangrove.Parse.Tokenize.Tag+ , Web.Mangrove.Parse.Tree.AfterAfterBody+ , Web.Mangrove.Parse.Tree.AfterAfterFrameset+ , Web.Mangrove.Parse.Tree.AfterBody+ , Web.Mangrove.Parse.Tree.AfterFrameset+ , Web.Mangrove.Parse.Tree.AfterHead+ , Web.Mangrove.Parse.Tree.BeforeHead+ , Web.Mangrove.Parse.Tree.BeforeHtml+ , Web.Mangrove.Parse.Tree.Common+ , Web.Mangrove.Parse.Tree.Dispatcher+ , Web.Mangrove.Parse.Tree.Foreign+ , Web.Mangrove.Parse.Tree.InBody+ , Web.Mangrove.Parse.Tree.InCaption+ , Web.Mangrove.Parse.Tree.InCell+ , Web.Mangrove.Parse.Tree.InColumnGroup+ , Web.Mangrove.Parse.Tree.InFrameset+ , Web.Mangrove.Parse.Tree.InHead+ , Web.Mangrove.Parse.Tree.InHeadNoscript+ , Web.Mangrove.Parse.Tree.Initial+ , Web.Mangrove.Parse.Tree.InRow+ , Web.Mangrove.Parse.Tree.InSelect+ , Web.Mangrove.Parse.Tree.InSelectInTable+ , Web.Mangrove.Parse.Tree.InTable+ , Web.Mangrove.Parse.Tree.InTableBody+ , Web.Mangrove.Parse.Tree.InTableText+ , Web.Mangrove.Parse.Tree.InTemplate+ , Web.Mangrove.Parse.Tree.InText+ , Web.Mangrove.Parse.Tree.Patch+ , Web.Mangrove.Parse.Tree.Patch.Fold+ autogen-modules: Paths_mangrove+ build-depends: base >=4.10 && <4.15+ , willow+ , aeson <1.6+ , bytestring <0.11+ , containers <0.7+ , filepath <1.5+ , text <1.3+ , transformers <0.6+ , unordered-containers <0.3+ , utility-ht <0.1+ , vector <0.13+ default-language: Haskell98+ other-extensions: ApplicativeDo+ , CPP+ , FlexibleInstances+ , FunctionalDependencies+ , KindSignatures+ , MultiParamTypeClasses+ , OverloadedStrings+ , Trustworthy+ , UndecidableInstances++test-suite html5lib+ import: flags-ghc+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ if !flag(html5lib)+ buildable: False+ main-is: Test/Mangrove/Html5Lib.hs+ other-modules: Paths_mangrove+ , Test.Mangrove.Html5Lib.Common+ , Test.Mangrove.Html5Lib.Tokenizer+ , Test.Mangrove.Html5Lib.Tokenizer.JSON+ , Test.Mangrove.Html5Lib.TreeConstruction+ , Test.Mangrove.Html5Lib.TreeConstruction.Parser+ autogen-modules: Paths_mangrove+ build-depends: base+ , mangrove+ , willow+ , aeson+ , bytestring+ , filepath+ , text+ , unordered-containers+ , vector+ , HUnit <1.7+ default-language: Haskell98+ other-extensions: CPP+ , OverloadedStrings++test-suite unit+ import: flags-ghc+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Test/Mangrove/Unit.hs+ other-modules: Paths_mangrove+ Test.Mangrove.Unit.Common+ , Test.Mangrove.Unit.Tokenize+ , Test.Mangrove.Unit.Tokenize.Tag+ autogen-modules: Paths_mangrove+ build-depends: base+ , mangrove+ , bytestring+ , filepath+ , text+ , utility-ht+ , HUnit <1.7+ default-language: Haskell98+ other-extensions: OverloadedStrings
+ src/Web/Mangrove/Parse/Common/Character.hs view
@@ -0,0 +1,202 @@+{-# LANGUAGE Trustworthy #-}++{-|+Description: HTML named character reference definitions and lookup.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: provisional+Portability: portable++The __[HTML](https://html.spec.whatwg.org/)__ standard defines a large number+of iconic character names for accessing commonly-used characters outside of the+file encoding, or just the capabilities of the keyboard used. A simple+implementation would be a @'M.HashMap' 'String' 'String'@ (as a few names map+to multiple Unicode characters), but unfortunately the compatibility+restrictions of the parsing algorithm mean that type would be+less-than-performant; as the parser needs to check for a valid reference on+every character, the full reference pool would need to be searched each time+for @O(n*log m)@ with a large @m@. (While @m@ can be reduced by filtering the+map at each step, amortized for @O(n*m*log m)@, there's no guarantee the+smaller @m@ is ultimately an improvement.)++This module instead organizes the character references into a search tree+indexed by 'Char', allowing each test step to operate over a much smaller+search space for @O(n*log 62)@, at the expense of slightly greater space+overhead.+-}+module Web.Mangrove.Parse.Common.Character+ ( CharacterReferenceTree ( .. )+ , ReferenceValue ( .. )+ , lookupCharacterReference+ , characterReferences+ ) where+++import qualified Data.Aeson as J+import qualified Data.Bifunctor as F.B+import qualified Data.Either as E+import qualified Data.HashMap.Strict as M+import qualified Data.Text as T++import qualified System.IO.Unsafe as IO.Unsafe++import Paths_mangrove++import Control.Applicative ( (<|>) )+import Data.Aeson ( (.:) )+import System.FilePath ( (<.>) )+++-- | __HTML:__+-- @[named character references]+-- (https://html.spec.whatwg.org/multipage/named-characters.html#named-character-references)@+-- +-- The decomposition of the HTML named character reference table into a+-- search-optimized form. The value type consists of the character reference+-- 'String' whose name terminates with the key 'Char', if one exists, alongside+-- any character references whose names are prefixed appropriately.+-- +-- The ampersand and semicolon delimiting the character reference are not+-- considered part of the name for storage; the former is silently dropped,+-- while the latter is indicated by the value of 'isSemicolonOptional'.+-- +-- For example, a minimal tree defining only the reference names @"¢"@,+-- @"¢"@, and @"·"@ would have the structure:+-- +-- > [ ( 'c'+-- > , Nothing+-- > , [ ( 'e'+-- > , Nothing+-- > , [ ( 'n'+-- > , Nothing+-- > , [ ( 't'+-- > , Just $ ReferenceValue True '\xA2'+-- > , [ ( 'e'+-- > , Nothing+-- > , [ ( 'r'+-- > , Nothing+-- > , [ ( 'd'+-- > , Nothing+-- > , [ ( 'o'+-- > , Nothing+-- > , [ ( 't'+-- > , Just $ ReferenceValue False '\xB7'+-- > , []+-- > ) ] ) ] ) ] ) ] ) ] ) ] ) ] ) ] ) ]+newtype CharacterReferenceTree =+ CharacterReferenceTree (M.HashMap Char (Maybe ReferenceValue, CharacterReferenceTree))+ deriving ( Eq, Show, Read )+++-- | A collection of data describing how to replace some named character+-- reference with a Unicode character sequence.+data ReferenceValue = ReferenceValue+ { isSemicolonOptional :: Bool+ -- ^ Whether the reference allows a compatibility form without a+ -- terminating semicolon.+ , referenceValue :: String+ -- ^ The 'Char'(s) to insert into the document in place of the+ -- reference.+ }+ deriving ( Eq, Show, Read )+++-- | User-friendly access into 'characterReferences', if the full name of the+-- potential character reference is already known. Note that the underlying+-- map isn't structured as a traditional 'M.HashMap', and so lookup is @O(n)@+-- over the length of the name rather than @O(log m)@ over the size of the map.+-- +-- This doesn't perform the longest-match calculations described by the HTML+-- standard, just a simple "does this string match a reference name" as if the+-- underlying structure were a flat @'M.HashMap' 'String' ref@. The leading+-- ampersand and trailing semicolon may be present, but neither is required.+lookupCharacterReference :: String -> Maybe ReferenceValue+lookupCharacterReference = lookupCharacterReference' characterReferences . dropAmpersand+ where dropAmpersand ('&':cs) = cs+ dropAmpersand cs = cs++-- | Iterate through the reference tree according to the remainder of the+-- reference name.+lookupCharacterReference' :: CharacterReferenceTree -> String -> Maybe ReferenceValue+lookupCharacterReference' _ [] = Nothing+lookupCharacterReference' (CharacterReferenceTree refs) [c] = M.lookup c refs >>= fst+lookupCharacterReference' (CharacterReferenceTree refs) [c, ';'] = M.lookup c refs >>= fst+lookupCharacterReference' (CharacterReferenceTree refs) (c:cs) = do+ (_, refs') <- M.lookup c refs+ lookupCharacterReference' refs' cs+++-- | The full set of named character references defined by the HTML standard,+-- in a search-optimized form. Unless the potential reference name isn't+-- completely and unambiguously known (e.g., during the resolution algorithm+-- described by the HTML standard), 'lookupCharacterReference' is the better+-- interface to use.+-- +-- Uses 'IO.Unsafe.unsafePerformIO' internally, as the underlying file should+-- never change at runtime, and so every evaluation would be pure.+characterReferences :: CharacterReferenceTree+characterReferences = IO.Unsafe.unsafePerformIO $ do+ entities <- getDataFileName $ "entities" <.> "json"+ maybe (CharacterReferenceTree M.empty) repackReferences <$> J.decodeFileStrict entities+{-# NOINLINE characterReferences #-}+++-- | Given a naïve map of character reference names to Unicode values, optimize+-- it for searching character-by-character.+repackReferences :: M.HashMap String CharacterData -> CharacterReferenceTree+repackReferences = foldr (repackReferences' . F.B.bimap (drop 1) (map toEnum . codepoints))+ (CharacterReferenceTree M.empty) . M.toList++-- | Add a single key-value character reference pair to the growing search tree.+repackReferences' :: (String, String) -> CharacterReferenceTree -> CharacterReferenceTree+repackReferences' ref (CharacterReferenceTree refs) = CharacterReferenceTree $+ M.unionWith joinTree (singletonReferenceTree ref) refs++-- | 'M.unionWith' any character references sharing a prefix. Note that if+-- multiple references have the same name (modulo semicolons) but different+-- values, the resulting value isn't necessarily predictable, nor guaranteed to+-- be stable between even minor library versions.+joinTree+ :: (Maybe ReferenceValue, CharacterReferenceTree)+ -- ^ The reference value to be added at some character.+ -> (Maybe ReferenceValue, CharacterReferenceTree)+ -- ^ The reference value already existing in the map.+ -> (Maybe ReferenceValue, CharacterReferenceTree)+joinTree (l, CharacterReferenceTree ls) (r, CharacterReferenceTree rs) =+ (fmap checkOptional $ l <|> r, CharacterReferenceTree $ M.unionWith joinTree ls rs)+ where checkOptional ref = ref+ { isSemicolonOptional =+ maybe False isSemicolonOptional l || maybe False isSemicolonOptional r+ }++-- | Generate a search tree containing the single key-value character reference+-- pair, to be merged into a larger accumulation.+singletonReferenceTree+ :: (String, String)+ -> M.HashMap Char (Maybe ReferenceValue, CharacterReferenceTree)+singletonReferenceTree (key, ref) = E.fromRight M.empty $+ foldr singletonReferenceTree' (Left True) key+ where singletonReferenceTree' ';' _ = Left False+ singletonReferenceTree' c (Left semicolon) = Right $+ M.singleton c (Just $ ReferenceValue semicolon ref, CharacterReferenceTree M.empty)+ singletonReferenceTree' c (Right ref') = Right $+ M.singleton c (Nothing, CharacterReferenceTree ref')+++-- | Internal representation of the character reference definitions in the+-- @entities.json@ file; the names are provided by JSON dictionary keys.+newtype CharacterData = CharacterData+ { codepoints :: [Int]+ -- ^ The Unicode code points represented by a given name.+ -- , characters :: String+ -- Uses surrogate character points rather than high-Unicode characters,+ -- and so isn't as desirable to build from.+ }+ deriving ( Eq, Show, Read )+instance J.FromJSON CharacterData where+ parseJSON = J.withObject "reference" $ \v -> CharacterData+ <$> v .: T.pack "codepoints"+ -- <*> v .: T.pack "characters"
+ src/Web/Mangrove/Parse/Common/Error.hs view
@@ -0,0 +1,128 @@+{-|+Description: Well-typed errors which may be raised by the HTML parsing algorithm.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: experimental+Portability: portable+-}+module Web.Mangrove.Parse.Common.Error+ ( ParseError ( .. )+ ) where+++import qualified Data.ByteString.Short as BS.SH+import qualified Data.Text as T++import qualified Numeric.Natural as Z++import Web.Willow.DOM+++-- | __HTML:__+-- @[parse errors]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parse-errors)@+-- (constructor names directly reflect the identifiers in the spec)+-- +-- Indicators that something syntactically unexpected was found in an HTML+-- file. Given the focus on recovery in the parse algorithm, these are more+-- properly warnings than errors, but the name follows that in the+-- specification.+-- +-- This datatype consists of the union of the errors raised by any of the+-- various stages of the algorithm; no distinction is made between an "encoding+-- error" and a "tree-building error".+-- +-- Several constructors wrap a payload which would otherwise be unrecoverable+-- from the DOM, to enable richer error reporting.+data ParseError+ = AbruptTemplateFragment+ | AbruptClosingOfEmptyComment+ | AbruptDoctypePublicIdentifier+ | AbruptDoctypeSystemIdentifier+ | AbsenceOfDigitsInNumericCharacterReference+ | BREndTag+ | CDataInHtmlContent+ | CharacterReferenceOutsideUnicodeRange Z.Natural+ | ControlCharacterInInputStream+ | ControlCharacterReference+ | DuplicateAttribute BasicAttribute+ | DuplicateSingletonElement ElementParams+ | EndTagWithAttributes+ | EndTagWithTrailingSolidus+ | EOFBeforeTagName+ | EOFInCData+ | EOFInComment+ | EOFInDoctype+ | EOFInScriptHtmlCommentLikeText+ | EOFInTag -- Payload?+ | EOFInTemplate+ | EOFInText+ | FramesetInBody ElementParams+ | IncompletelyClosedFormattingElement+ | IncorrectlyClosedComment+ | IncorrectlyOpenedComment+ | InvalidByteSequence BS.SH.ShortByteString+ | InvalidCharacterSequenceAfterDoctypeName+ | InvalidFirstCharacterOfTagName+ | LegacyDoctype+ | MalformedTableStructure ElementParams+ | MissingAttributeValue+ | MissingDoctype+ | MissingDoctypeName+ | MissingDoctypePublicIdentifier+ | MissingDoctypeSystemIdentifier+ | MissingEndTagName+ | MissingQuoteBeforeDoctypePublicIdentifier+ | MissingQuoteBeforeDoctypeSystemIdentifier+ | MissingSemicolonAfterCharacterReference+ | MissingWhitespaceAfterDoctypePublicKeyword+ | MissingWhitespaceAfterDoctypeSystemKeyword+ | MissingWhitespaceBeforeDoctypeName+ | MissingWhitespaceBetweenAttributes+ | MissingWhitespaceBetweenDoctypePublicAndSystemIdentifiers+ | NestedComment+ | NestedElementForAdoptionAgency+ | NestedNonRecursiveElement+ | NestedSingletonElement+ | NoncharacterCharacterReference Char+ | NoncharacterInInputStream+ | NonVoidHtmlElementStartTagWithTrailingSolidus+ | NullCharacterReference+ | ObsoleteTagName T.Text+ | OverlappingFormattingElements+ | OverlappingHeaderElements+ | OverlappingInputElements+ | SurrogateCharacterReference Char+ | SurrogateInInputStream+ | UnexpectedCharacterAfterDoctypeSystemIdentifier Char+ | UnexpectedCharacterInAttributeName+ | UnexpectedCharacterInUnquotedAttributeValue+ | UnexpectedContentAfterBody+ | UnexpectedDescendantElement ElementParams+ | UnexpectedDoctype DocumentTypeParams+ | UnexpectedElementInTableStructure+ | UnexpectedElementWithImpliedEndTag+ | UnexpectedEqualsSignBeforeAttributeName+ | UnexpectedEndColInColumnGroup+ | UnexpectedEndOfBody+ | UnexpectedEndTag ElementParams+ | UnexpectedFormattingElementOutOfScope+ | UnexpectedHtmlElementInForeignContent+ | UnexpectedMetadataOutsideOfHead+ | UnexpectedNodeAfterFrameset -- Payload?+ | UnexpectedNodeInFrameset -- Payload?+ | UnexpectedNodeInHeadNoscript+ | UnexpectedNodeInTableStructure+ | UnexpectedNullCharacter+ | UnexpectedQuestionMarkInsteadOfTagName+ | UnexpectedSolidusInTag+ | UnexpectedTableCellOutsideOfRow+ | UnexpectedTextInTableStructure+ | UnknownNamedCharacterReference+ | UnmatchedEndTag ElementParams+ | UnmatchedEndPTag+ | XmlnsAttribtuteWithMismatchedValue+ deriving ( Eq, Show, Read )
+ src/Web/Mangrove/Parse/Encoding/Preprocess.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE CPP #-}++{-|+Description: Clean a 'Char' stream by normalizing newlines and warning about control characters.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: experimental+Portability: portable++To simplify the tokenization parsers, the many representations of line breaks+are unified into a single, Unix-style @\\n@. While we're iterating over the+input, and before some of the special characters are replaced, it's also a good+time to trigger the warnings for unexpected characters+('ControlCharacterInInputStream', 'SurrogateInInputStream', and+'NoncharacterInInputStream').+-}+module Web.Mangrove.Parse.Encoding.Preprocess+ ( preprocess+ , preprocessStep+ -- * Initialization+ , Encoding ( .. )+ , DecoderState+ , initialDecoderState+ ) where+++import qualified Data.Bifunctor as F.B+import qualified Data.ByteString as BS+import qualified Data.ByteString.Short as BS.SH+import qualified Data.Tuple.HT as U.HT++#if MIN_VERSION_base(4,11,0)+#else+import Data.Semigroup ( (<>) )+#endif++import Web.Mangrove.Parse.Common.Error+import Web.Willow.Common.Encoding+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Parser.Util+++-- | __Encoding:__+-- @[preprocessing the input stream]+-- (https://encoding.spec.whatwg.org/#preprocessing-the-input-stream)@+-- +-- Given a character encoding scheme, transform a dependant 'BS.ByteString'+-- into portable 'Char's. If any byte sequences are meaningless or illegal,+-- they are replaced with the Unicode replacement character @\\xFFFD@. All+-- newlines are normallized to a single @\\n@ 'Char', and Unicode control+-- characters, surrogate characters, and non-characters are marked with the+-- proper errors.+-- +-- See 'preprocessStep' to operate over only a minimal section.+preprocess :: DecoderState -> BS.ByteString -> ([([ParseError], Char)], DecoderState)+preprocess state = F.B.first (normalize . map charError . concatMap flatten) . decode state+ where normalize ((err1, '\r'):(err2, '\n'):cs) = (err1 <> err2, '\n') : normalize cs+ normalize ((err, '\r'):cs) = (err, '\n') : normalize cs+ normalize (c:cs) = c : normalize cs+ normalize [] = []++-- | __Encoding:__+-- @[preprocessing the input stream]+-- (https://encoding.spec.whatwg.org/#preprocessing-the-input-stream)@+-- +-- Read the smallest number of bytes from the head of the 'BS.ByteString'+-- which would leave the decoder in a re-enterable state. Any byte+-- sequences which are meaningless or illegal are replaced with the Unicode+-- replacement character @\\xFFFD@. All newlines are normallized to a single+-- @\\n@ 'Char', and Unicode control characters, surrogate characters, and+-- non-characters are marked with the proper errors.+-- +-- See 'preprocess' to operate over the entire string at once.+preprocessStep+ :: DecoderState+ -> BS.ByteString+ -> ([([ParseError], Char)], DecoderState, BS.ByteString)+preprocessStep state stream = normalize' . flatten' $ decodeStep state stream+ where flatten' = U.HT.mapFst3 (maybe [] $ map charError . flatten)+ normalize' c'@([], _, _) = c'+ normalize' ([(errs, '\r')], state', stream') = case flatten' $ decodeStep state' stream' of+ ((errs', '\n'):cs, state'', stream'') ->+ normalize' ((errs ++ errs', '\n') : cs, state'', stream'')+ _ -> ([(errs, '\n')], state', stream')+ normalize' ((errs, '\r'):(errs', '\n'):cs, state', stream') =+ U.HT.mapFst3 ((errs ++ errs', '\n') :) $ normalize' (cs, state', stream')+ normalize' ((errs, '\r'):cs, state', stream') =+ U.HT.mapFst3 ((errs, '\n') :) $ normalize' (cs, state', stream')+ normalize' (c:cs, state', stream') =+ U.HT.mapFst3 (c :) $ normalize' (cs, state', stream')+++-- | Add a 'ControlCharacterInInputStream', 'SurrogateInInputStream', or+-- 'NoncharacterInInputStream' error to the relevant characters.+charError :: ([ParseError], Char) -> ([ParseError], Char)+charError c'@(_, c)+ | range '\xD800' '\xDFFF' c = addErr SurrogateInInputStream+ | range '\xFDD0' '\xFDEF' c = addErr NoncharacterInInputStream+ | noncharacter = addErr NoncharacterInInputStream+ | range '\SOH' '\US' c && notElem c "\t\n\f\r" = addErr ControlCharacterInInputStream+ | range '\DEL' '\x9F' c = addErr ControlCharacterInInputStream+ | otherwise = c'+ where noncharacter = case mod (fromEnum c) 0x10000 of+ 0xFFFE -> True+ 0xFFFF -> True+ _ -> False+ -- If this function could ever be applied twice, any errors would be+ -- duplicated. As it's not exported from the module, that shouldn't be+ -- an issue.+ addErr err = F.B.first (err :) c'++-- | Rewrap the split type containing a list into a list at the top level. Any+-- 'Left' errors are replaced by @[('InvalidByteSequence' /bytes/, '\\xFFFD')]@.+flatten :: Either BS.SH.ShortByteString String -> [([ParseError], Char)]+flatten = either (\err -> [([InvalidByteSequence err], replacementChar)]) (map $ \c -> ([], c))
+ src/Web/Mangrove/Parse/Tokenize.hs view
@@ -0,0 +1,189 @@+{-|+Description: Extract basic semantic categories from a simple textual stream.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: provisional+Portability: portable++This module and the internal branch it heads implement the "Tokenization"+section of the+__[HTML](https://html.spec.whatwg.org/multipage/parsing.html#tokenization)__+document parsing specification, processing a stream of text to add information+on, or group it by, semantic category. This then allows the following stage to+base its logic on such higher-level concepts as "markup tag" or "comment"+without worrying about the (sometimes complex) escaping behaviour required to+parse them.+-}+module Web.Mangrove.Parse.Tokenize+ ( -- * Types+ -- ** Final+ Token ( .. )+ , BasicAttribute+ , TagParams ( .. )+ , emptyTagParams+ , DoctypeParams ( .. )+ , emptyDoctypeParams+ -- ** Intermediate+ , TokenizerState+ , CurrentTokenizerState ( .. )+ , Encoding ( .. )+ -- * Initialization+ , defaultTokenizerState+ , tokenizerMode+ , tokenizerStartTag+ , tokenizerEncoding+ -- * Transformations+ , tokenize+ , tokenizeStep+ , finalizeTokenizer+ ) where+++import qualified Control.Applicative as A+import qualified Control.Monad.Trans.State as N.S++import qualified Data.Bifunctor as F.B+import qualified Data.ByteString as BS+import qualified Data.ByteString.Short as BS.SH+import qualified Data.Maybe as Y++import Web.Willow.DOM++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Encoding.Preprocess+import Web.Mangrove.Parse.Tokenize.Common hiding ( setRemainder )+import Web.Mangrove.Parse.Tokenize.Dispatcher+import Web.Willow.Common.Encoding+import Web.Willow.Common.Encoding.Sniffer+import Web.Willow.Common.Parser+++-- | __HTML:__+-- @[tokenization]+-- (https://html.spec.whatwg.org/multipage/parsing.html#tokenization)@+-- +-- Given a starting environment, transform a binary document stream into a+-- stream of semantic atoms. If the parse fails, returns all tokens before the+-- one which caused the error, but any trailing bytes are silently dropped.+tokenize+ :: TokenizerState+ -> BS.ByteString+ -> ([([ParseError], Token)], TokenizerState)+tokenize state stream = loop $ tokenizeStep state stream+ where loop (ts, state', output)+ | BS.null output = (ts, state')+ | otherwise = F.B.first (ts ++) . loop $ tokenizeStep state' output+++-- | Parse a minimal number of bytes from an input stream, into a sequence of+-- semantic tokens. Returns all data required to seamlessly resume parsing.+tokenizeStep+ :: TokenizerState+ -> BS.ByteString+ -> ([([ParseError], Token)], TokenizerState, BS.ByteString)+tokenizeStep state stream = case runParserT (N.S.runStateT dispatcher' state) stream of+ Just ((out, state'), stream') -> (out, state', stream')+ Nothing -> ([], stateEof, BS.empty)+ where stateEof = state+ { decoderState_ = Right $ setRemainder (BS.SH.toShort stream) <$>+ decoderDefaultState state stream+ }++-- | Explicitly indicate that the input stream will not contain any further+-- bytes, and perform any finalization processing based on that.+finalizeTokenizer :: TokenizerState -> [([ParseError], Token)]+finalizeTokenizer state = fst $ tokenize state' BS.empty+ where state' = state+ { tokenParserState = (tokenParserState state)+ { atEndOfStream = True+ }+ }+++-- | Given a string as emitted by the decoder and the final state of that+-- parser, repack it into a single list with that final state encapsulated in a+-- 'Just'; the 'init' of the string is given 'Nothing's.+repackStream :: ([([ParseError], Char)], DecoderState, BS.ByteString) -> [TokenizerInput]+repackStream ([], _, _) = []+repackStream ([(errs, c)], dState, bs) =+ [TokenizerInput errs c $ Just (Just dState, bs)]+repackStream ((errs, c):cs, dState, bs) =+ TokenizerInput errs c Nothing : repackStream (cs, dState, bs)+++-- | Wrap the standard dispatcher to operate over a raw 'BS.ByteString'+-- rather than the "Web.Mangrove.Parse.Encoding" output.+dispatcher' :: StateParser TokenizerState BS.ByteString [([ParseError], Token)]+dispatcher' = do+ state <- N.S.get+ stream <- abridge+ let dState = Y.fromMaybe (initialDecoderState Utf8) $ decoderDefaultState state stream+ recurse state $ preprocessStep' dState stream+ where preprocessStep' dState input = case preprocessStep dState input of+ ([], _, _) -> []+ cs'@(_, dState', input') -> repackStream cs' ++ preprocessStep' dState' input'++-- | Loop the tokenization dispatcher until it returns a set of tokens which+-- happens to coincide with a decoder breakpoint. Relies on lazy evaluation in+-- the stream generation to avoid forcing the entire thing at once, while still+-- retaining the capability to consume as much input as necessary to get the+-- parsers to line up.+recurse+ :: TokenizerState+ -> [TokenizerInput]+ -> StateParser TokenizerState BS.ByteString [([ParseError], Token)]+recurse state stream = case runParserT (N.S.runStateT dispatcher $ tokenParserState state) stream of+ Nothing -> A.empty+ Just ((out, tokState'), stream') -> case Y.listToMaybe (reverse out) >>= tokenizedState of+ Nothing -> do+ let state' = state+ { tokenParserState = tokState'+ }+ N.S.put state'+ out' <- recurse state' stream'+ return $ map repackOut out ++ out'+ Just (dState, dStream) -> do+ pushChunk dStream+ N.S.put $ state+ { decoderState_ = Right dState+ , tokenParserState = tokState'+ }+ return $ map repackOut out+ where repackOut t' = (tokenizedErrs t', tokenizedOut t')+++-- | Specify which section of the finite state machine describing the+-- tokenization algorithm should be active.+tokenizerMode :: CurrentTokenizerState -> TokenizerState -> TokenizerState+tokenizerMode mode state = state+ { tokenParserState = (tokenParserState state)+ { currentState = mode+ }+ }++-- | Specify the data to use as the previous tag which had been emitted by the+-- tokenizer. This only has to be called when required for external algorithms+-- or constructions; the parser automatically updates as required for generated+-- 'StartTag' tokens.+tokenizerStartTag :: Maybe Namespace -> ElementName -> TokenizerState -> TokenizerState+tokenizerStartTag ns name state = state+ { tokenParserState = (tokenParserState state)+ { prevStartTag = Just name+ , currentNodeNamespace = ns+ }+ }++-- | Specify the encoding scheme used by a given parse environment to read from+-- the binary input stream. Note that this will always use the initial state+-- for the respective decoder; intermediate states as returned by 'decodeStep'+-- are not supported.+tokenizerEncoding :: Either SnifferEnvironment (Maybe Encoding) -> TokenizerState -> TokenizerState+tokenizerEncoding enc' state = state+ { decoderState_ = case enc' of+ Right Nothing -> Right Nothing+ Right (Just enc) -> Left $ Right enc+ Left env -> Left $ Left env+ }
+ src/Web/Mangrove/Parse/Tokenize/CData.hs view
@@ -0,0 +1,65 @@+{-|+Description: Tokenization rules for characters within @\<[CDATA[@ ... @]]\>@ sections.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.CData+ ( tokenCDataSection+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+++-- | __HTML:__+-- @[CDATA section state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-state)@+-- +-- The parsing instructions rooted in the 'CDataState' section of the state+-- machine.+tokenCDataSection :: Tokenizer [TokenizerOutput Token]+tokenCDataSection = tokenizers (Just [([EOFInCData], EndOfStream)])+ [ ifs_ (== ']') tokenCDataSectionBracket+ , elsesChar $ \c -> emit' ([], Character c)+ ]++-- | __HTML:__+-- @[CDATA section bracket state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-bracket-state)@+-- +-- The parsing instructions for after reading @"]"@ in the 'CDataState' section+-- of the state machine.+tokenCDataSectionBracket :: Tokenizer [TokenizerOutput Token]+tokenCDataSectionBracket = tokenizers (Just [([], Character ']'), ([EOFInCData], EndOfStream)])+ [ ifs_ (== ']') tokenCDataSectionEnd+ , elsePush_ $ emit' ([], Character ']')+ ]++-- | __HTML:__+-- @[CDATA section end state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-end-state)@+-- +-- The parsing instructions for after reading @"]]"@ in the 'CDataState'+-- section of the state machine.+tokenCDataSectionEnd :: Tokenizer [TokenizerOutput Token]+tokenCDataSectionEnd = tokenCDataSectionEnd' >>= \ts -> case tokenizedOut ts of+ [] -> return []+ (t':ts') -> emits (tokenizedState ts) $+ (tokenizedErrs ts, Character t') : [([], Character t) | t <- ts']+ where emits state ts = finalStateList state <$> mapM emit ts++-- | Loop within the __HTML__ @[CDATA section end state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-end-state)@+-- to read and re-emit any number of @']'@ 'Char's before the closing @"]]\>"@.+tokenCDataSectionEnd' :: Tokenizer (TokenizerOutput String)+tokenCDataSectionEnd' = tokenizer (Just ([EOFInCData], "]]"))+ [ if_ (== ']') $ consOut ']' <$> tokenCDataSectionEnd'+ , if_ (== '>') $ changeState DataState *> packToken ([], "")+ , elsePush_ $ packToken ([], "]]")+ ]
+ src/Web/Mangrove/Parse/Tokenize/Character.hs view
@@ -0,0 +1,396 @@+{-# LANGUAGE Trustworthy #-}++{-|+Description: Tokenization rules for characters comprising character references.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable++The monadic parser construction and prefix-preferring 'String' concatenation+mean that character references are resolved opposite the algorithm described in+the __[HTML](https://html.spec.whatwg.org/multipage/parsing.html)__+specification. That is most notable in the numeric references, where the spec+recommends multiplying an accumulator by the numeric base before adding each+digit, while this implementation multiplies each digit by the positional value+before adding the accumulator. The reversal in named references is more+conceptual and subtle, but the spec is worded to read a name from the input+while the implementation reads input 'Char's according to the list of reference+names; this is likely how most implementations accomplish it.+-}+module Web.Mangrove.Parse.Tokenize.Character+ ( flushCharRef+ , tokenCharacterReference+ ) where+++import qualified Control.Applicative as A+import qualified Control.Monad as N++import qualified Data.Bifunctor as F.B+import qualified Data.Char as C+import qualified Data.HashMap.Strict as M+import qualified Data.Maybe as Y+import qualified Data.Vector as V+import qualified Data.Word as W++import qualified Numeric.Natural as Z++import Data.Functor ( ($>) )+import Data.Vector ( (!?) )++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Common.Character+import Web.Mangrove.Parse.Tokenize.Common+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Util++import {-# SOURCE #-} Web.Mangrove.Parse.Tokenize.Dispatcher+++-- | __HTML:__+-- @[flush code points consumed as a character reference]+-- (https://html.spec.whatwg.org/multipage/parsing.html#flush-code-points-consumed-as-a-character-reference)@+-- +-- Transform a wrapped 'Char' sequence into a sequence of wrapped 'Character'+-- tokens.+flushCharRef :: TokenizerOutput String -> Tokenizer [TokenizerOutput Token]+flushCharRef out = case tokenizedOut out of+ "" -> case tokenizedErrs out of+ [] -> return []+ errs -> consTokenErrorsList errs . Y.fromMaybe [] <$> A.optional dispatcher+ [c] -> return [out { tokenizedOut = Character c }]+ (c:_) -> do+ let c' = out+ { tokenizedOut = Character c+ , tokenizedState = Nothing+ }+ cs' <- flushCharRef out'+ return $ c' : cs'+ where out' = out+ { tokenizedErrs = []+ , tokenizedOut = drop 1 $ tokenizedOut out+ }+++-- | __HTML:__+-- @[character reference state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#character-reference-state)@+-- +-- The parsing instructions for after reading @"&"@ in a section of the state+-- machine which allows character references.+tokenCharacterReference+ :: Bool+ -- ^ Whether this parser was called from within a markup tag's attribute.+ -> Tokenizer (TokenizerOutput String)+tokenCharacterReference inAttribute = tokenizer (Just ([], "&"))+ [ ifPush_ isAsciiAlphaNum $ do+ ref' <- tokenNamedCharacterReference inAttribute characterReferences+ case tokenizedOut ref' of+ NotFound -> consTokenErrors (tokenizedErrs ref') . consOut '&' <$> tokenAmbiguousAmpersand+ Flush name -> return ref'+ { tokenizedOut = '&' : name+ }+ Found ref -> return ref'+ { tokenizedOut = ref+ }+ , if_ (== '#') tokenNumericCharacterReference+ , elsePush_ $ packToken ([], "&")+ ]++-- | The result of looking up a named character reference.+data CharacterReference+ = NotFound+ -- ^ No matching name found.+ | Flush String+ -- ^ A named reference was found, but historical reasons require+ -- emitting the name as characters anyway.+ | Found String+ -- ^ A named reference was found and successfully resolved.++-- | __HTML:__+-- @[named character reference state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state)@+-- +-- The parsing instructions for after reading @"&"@ followed by letters and/or+-- numbers in a section of the state machine which allows character references.+tokenNamedCharacterReference+ :: Bool+ -- ^ Whether this parser was called from within a markup tag's attribute.+ -> CharacterReferenceTree+ -- ^ The list of reference names, filtered according to what prefix has+ -- been already encountered.+ -> Tokenizer (TokenizerOutput CharacterReference)+tokenNamedCharacterReference inAttribute (CharacterReferenceTree refs) = do+ cm' <- A.optional next+ let state1 = endState+ state2 = do+ cm <- cm'+ decodedState cm+ case cm' of+ Nothing -> packState ([], NotFound) state1+ Just c -> case M.lookup (decodedOut c) refs of+ -- No reference found, and no further references possible.+ Nothing -> push c *> packState ([], NotFound) state2+ -- No reference found, but longer potential reference names exist.+ Just (Nothing, refs') -> do+ ref' <- tokenNamedCharacterReference inAttribute refs'+ let errs' = decodedErrs c ++ tokenizedErrs ref'+ case tokenizedOut ref' of+ -- None of those longer names match.+ NotFound -> push c *> packState ([], NotFound) state2+ -- Fall back on the longer name.+ Flush name -> return ref'+ { tokenizedErrs = errs'+ , tokenizedOut = Flush $ decodedOut c : name+ }+ Found ref -> return ref'+ { tokenizedErrs = errs'+ , tokenizedOut = Found ref+ }+ -- Reference found at the current name.+ Just (Just found, refs') -> do+ semicolon <- A.optional $ next >>= satisfying (\d -> decodedOut d == ';')+ let state3 = maybe state2 decodedState semicolon+ if not (isSemicolonOptional found) && Y.isNothing semicolon+ then tokenNamedCharacterReference inAttribute refs' >>= \ref ->+ case tokenizedOut ref of+ NotFound -> push c $> ref+ _ -> return ref+ else tokenNamedCharacterReference inAttribute refs' >>=+ foundNamedCharacterReference+ inAttribute+ (Y.isJust semicolon)+ (decodedOut c)+ state3+ (referenceValue found)++-- | Perform the logistics around determining what 'Character'(s) should be+-- emitted, based on the current environment and any longer match.+foundNamedCharacterReference+ :: Bool+ -- ^ Whether the reference is being evaluated as part of an attribute value.+ -> Bool+ -- ^ Whether the last character matched is a semicolon.+ -> Char+ -- ^ The last character of the reference, /excluding/ the semicolon.+ -> DecoderOutputState+ -- ^ Remainder of the binary document stream after the last character matched.+ -> String+ -- ^ The evaluated sequence represented by the name.+ -> TokenizerOutput CharacterReference+ -- ^ The value returned by continuing the lookup on a longer string.+ -> Tokenizer (TokenizerOutput CharacterReference)+foundNamedCharacterReference attribute semicolon char state found ref = case tokenizedOut ref of+ NotFound -> if attribute && not semicolon+ then do+ c' <- A.optional $ next >>= satisfying+ (\c -> decodedOut c == '=' || isAsciiAlphaNum (decodedOut c))+ case c' of+ Nothing -> packState ([MissingSemicolonAfterCharacterReference], Found found) state+ Just c -> push c *> packState ([], Flush [char]) state+ else packState (errs', Found found) state+ Flush name -> return $ ref+ { tokenizedOut = Flush $ char : name+ }+ _ -> return ref+ where errs'+ | semicolon = []+ | otherwise = [MissingSemicolonAfterCharacterReference]++-- | __HTML:__+-- @[ambiguous ampersand state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#ambiguous-ampersand-state)@+-- +-- The parsing instructions for after reading @"&"@ followed by a string which+-- does not correspond to any known reference name, in a section of the state+-- machine which allows character references.+tokenAmbiguousAmpersand :: Tokenizer (TokenizerOutput String)+tokenAmbiguousAmpersand = tokenizer (Just ([], ""))+ [ ifChar isAsciiAlphaNum $ \c -> consOut c <$> tokenAmbiguousAmpersand+ , ifPush_ (== ';') $ packToken ([UnknownNamedCharacterReference], "")+ , elsePush_ $ packToken ([], "")+ ]++-- | __HTML:__+-- @[numeric character reference state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-state)@+-- +-- The parsing instructions for after reading @"&#"@ in a section of the state+-- machine which allows character references.+tokenNumericCharacterReference :: Tokenizer (TokenizerOutput String)+tokenNumericCharacterReference = fmap (either ("&#" ++) id) <$> tokenizer+ (Just ([AbsenceOfDigitsInNumericCharacterReference], Left ""))+ [ if_ (== 'x') $ flatten . fmap (F.B.bimap ('x' :) tokenNumericCharacterReferenceEnd) <$>+ tokenHexadecimalCharacterReferenceStart+ , if_ (== 'X') $ flatten . fmap (F.B.bimap ('X' :) tokenNumericCharacterReferenceEnd) <$>+ tokenHexadecimalCharacterReferenceStart+ , elsePush_ $ flatten . fmap (F.B.second tokenNumericCharacterReferenceEnd) <$>+ tokenDecimalCharacterReferenceStart+ ]+ where flatten tok = case tokenizedOut tok of+ Left str -> tok { tokenizedOut = Left str }+ Right (errs, c) -> consTokenErrors errs $ tok { tokenizedOut = Right [c] }++-- | __HTML:__+-- @[hexadecimal character reference start state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-start-state)@+-- +-- The parsing instructions for after reading @"&#x"@ or @"&#X"@ in a section+-- of the state machine which allows character references.+tokenHexadecimalCharacterReferenceStart+ :: Tokenizer (TokenizerOutput (Either String Z.Natural))+ -- ^ The inner data contains a theoretically-Unicode code point+ -- ('Right', though it may exceed the upper bound) or the characters+ -- consumed in reading something invalid ('Left'), as relevant.+tokenHexadecimalCharacterReferenceStart = tokenizer+ (Just ([AbsenceOfDigitsInNumericCharacterReference], Left ""))+ [ ifPush_ C.isHexDigit $ fmap packReference tokenHexadecimalCharacterReference+ , elsePush_ $ packToken ([AbsenceOfDigitsInNumericCharacterReference], Left "")+ ]++-- | __HTML:__+-- @[decimal character reference start state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-start-state)@+-- +-- The parsing instructions for after reading @"&#"@ followed by digit from @0@+-- to @9@ in a section of the state machine which allows character references.+tokenDecimalCharacterReferenceStart+ :: Tokenizer (TokenizerOutput (Either String Z.Natural))+ -- ^ The inner data contains a theoretically-Unicode code point+ -- ('Right', though it may exceed the upper bound) or the characters+ -- consumed in reading something invalid ('Left'), as relevant.+tokenDecimalCharacterReferenceStart = tokenizer+ (Just ([AbsenceOfDigitsInNumericCharacterReference], Left ""))+ [ ifPush_ C.isDigit $ fmap packReference tokenDecimalCharacterReference+ , elsePush_ $ packToken ([AbsenceOfDigitsInNumericCharacterReference], Left "")+ ]++-- | Extract the Unicode-ish code point calculated by the numeric character+-- reference loops, and repack it as output by the respective initializer.+packReference :: TokenizerOutput (Z.Natural, Z.Natural) -> TokenizerOutput (Either String Z.Natural)+packReference = fmap $ Right . snd++-- | __HTML:__+-- @[hexadecimal character reference state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-state)@+-- +-- The parsing instructions for after reading @"&#x"@ or @"&#X"@ followed by a+-- hexadecimal digit in a section of the state machine which allows character+-- references.+tokenHexadecimalCharacterReference+ :: Tokenizer (TokenizerOutput (Z.Natural, Z.Natural))+ -- ^ The inner data contains the number of valid numeric digits to the+ -- right ('fst') and the number composed from those digits ('snd').+ -- + -- The datatype has been chosen so that even extremely long references+ -- (over a gigabyte of digits /at minimum/ with 'Word') have no chance+ -- of causing system-dependant behaviour; @ @ and @ ..0;@ being+ -- equivalent when the second has 2^30 @0@s (among other alignments)+ -- could potentially allow some obscure attack.+tokenHexadecimalCharacterReference = tokenizer+ (Just ([MissingSemicolonAfterCharacterReference], (0, 0)))+ [ ifChar C.isDigit $ \c -> increment 0x30 c <$> tokenHexadecimalCharacterReference+ , ifChar (range 'A' 'F') $ \c -> increment 0x37 c <$> tokenHexadecimalCharacterReference+ , ifChar (range 'a' 'f') $ \c -> increment 0x57 c <$> tokenHexadecimalCharacterReference+ , if_ (== ';') $ packToken ([], (0, 0))+ , elsePush_ $ packToken ([MissingSemicolonAfterCharacterReference], (0, 0))+ ]+ where increment offset c = fmap $ \(pos, accum) ->+ (pos + 1, fromIntegral (fromEnum c - offset) * (16 ^ pos) + accum)++-- | __HTML:__+-- @[decimal character reference state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-state)@+-- +-- The parsing instructions for after reading @"&#"@ followed by a digit from+-- @0@ through @9@ in a section of the state machine which allows character+-- references.+tokenDecimalCharacterReference+ :: Tokenizer (TokenizerOutput (Z.Natural, Z.Natural))+ -- ^ The inner data contains the number of valid numeric digits to the+ -- right ('fst') and the number composed from those digits ('snd').+ -- + -- The datatype has been chosen so that even extremely long references+ -- (over a gigabyte of digits /at minimum/ with 'Word') have no chance+ -- of causing system-dependant behaviour; @ @ and @
..0;@ being+ -- equivalent when the second has 2^30 @0@s (among other alignments)+ -- could potentially allow some obscure attack.+tokenDecimalCharacterReference = tokenizer+ (Just ([MissingSemicolonAfterCharacterReference], (0, 0)))+ [ ifChar C.isDigit $ \c -> increment c <$> tokenDecimalCharacterReference+ , if_ (== ';') $ packToken ([], (0, 0))+ , elsePush_ $ packToken ([MissingSemicolonAfterCharacterReference], (0, 0))+ ]+ where increment c = fmap $ \(pos, accum) ->+ (pos + 1, fromIntegral (fromEnum c - 0x30) * (10 ^ pos) + accum)++-- | __HTML:__+-- @[numeric character reference end state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state)@+-- +-- The instructions for processing a theoretically-Unicode code point into an+-- actually-legal 'Char', after reading a non-digit character following a+-- @"&#"@ in a section of the state machine which allows character references.+tokenNumericCharacterReferenceEnd :: Z.Natural -> ([ParseError], Char)+tokenNumericCharacterReferenceEnd 0x00 = ([NullCharacterReference], replacementChar)+tokenNumericCharacterReferenceEnd code+ | code > 0x10FFFF =+ ([CharacterReferenceOutsideUnicodeRange code], replacementChar)+ | range 0xD800 0xDFFF code =+ ([SurrogateCharacterReference . toEnum $ fromIntegral code], replacementChar)+ | range 0xFDD0 0xFDEF code =+ ([NoncharacterCharacterReference . toEnum $ fromIntegral code], toEnum $ fromIntegral code)+ | cMod == 0xFFFE || cMod == 0xFFFF =+ ([NoncharacterCharacterReference . toEnum $ fromIntegral code], toEnum $ fromIntegral code)+ | range 0x00 0x1F code && notElem code [0x09, 0x0A, 0x0C] =+ ([ControlCharacterReference], toEnum $ fromIntegral code)+ | range 0x7F 0x9F code = ([ControlCharacterReference],+ Y.fromMaybe (toEnum $ fromIntegral code) . N.join $+ controlReplacement !? (fromIntegral code - 0x7F))+ | otherwise = ([], toEnum $ fromIntegral code)+ where cMod = fromIntegral code :: W.Word16++-- | The specification-defined replacements for the C1 control characters, plus+-- @0x7F@ (DEL). 'Nothing' placeholders are used for controls without a+-- replacement, to allow indexing by the code point minus 0x7F.+controlReplacement :: V.Vector (Maybe Char)+controlReplacement = V.fromList+ [ Nothing -- 0x7F+ , Just '\x20AC' -- 0x80+ , Nothing -- 0x81+ , Just '\x201A' -- 0x82+ , Just '\x0192' -- 0x83+ , Just '\x201E' -- 0x84+ , Just '\x2026' -- 0x85+ , Just '\x2020' -- 0x86+ , Just '\x2021' -- 0x87+ , Just '\x02C6' -- 0x88+ , Just '\x2030' -- 0x89+ , Just '\x0160' -- 0x8A+ , Just '\x2039' -- 0x8B+ , Just '\x0152' -- 0x8C+ , Nothing -- 0x8D+ , Just '\x017D' -- 0x8E+ , Nothing -- 0x8F+ , Nothing -- 0x90+ , Just '\x2018' -- 0x91+ , Just '\x2019' -- 0x92+ , Just '\x201C' -- 0x93+ , Just '\x201D' -- 0x94+ , Just '\x2022' -- 0x95+ , Just '\x2013' -- 0x96+ , Just '\x2014' -- 0x97+ , Just '\x02DC' -- 0x98+ , Just '\x2122' -- 0x99+ , Just '\x0161' -- 0x9A+ , Just '\x203A' -- 0x9B+ , Just '\x0153' -- 0x9C+ , Nothing -- 0x9D+ , Just '\x017E' -- 0x9E+ , Just '\x0178' -- 0x9F+ ]
+ src/Web/Mangrove/Parse/Tokenize/Comment.hs view
@@ -0,0 +1,155 @@+{-|+Description: Tokenization rules for characters within @\<!--@ ... @--\>@ sections.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.Comment+ ( tokenCommentStart+ , packCommentToken+ ) where+++import qualified Data.Text as T++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Willow.Common.Encoding.Character+++-- | Wrap a given span of 'Char's into the payload of a 'Comment'.+packCommentToken :: TokenizerOutput String -> TokenizerOutput Token+packCommentToken = fmap $ Comment . T.pack+++-- | __HTML:__+-- @[comment start state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-start-state)@+-- +-- The parsing instructions for after reading @"\<!--"@ in a section of the+-- state machine which allows markup declarations.+tokenCommentStart :: Tokenizer (TokenizerOutput String)+tokenCommentStart = tokenizer (Just ([EOFInComment], ""))+ [ if_ (== '-') tokenCommentStartDash+ , if_ (== '>') $ changeState DataState *> packToken ([AbruptClosingOfEmptyComment], "")+ , elsePush_ tokenComment+ ]++-- | __HTML:__+-- @[comment start dash state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-start-state)@+-- +-- The parsing instructions for after reading @"\<!---"@ in a section of the+-- state machine which allows markup declarations.+tokenCommentStartDash :: Tokenizer (TokenizerOutput String)+tokenCommentStartDash = tokenizer (Just ([EOFInComment], ""))+ [ if_ (== '-') tokenCommentEnd+ , if_ (== '>') $ changeState DataState *> packToken ([AbruptClosingOfEmptyComment], "")+ , elsePush_ $ consOut '-' <$> tokenComment+ ]++-- | __HTML:__+-- @[comment state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-state)@+-- +-- The parsing instructions rooted in the HTML comment section of the state+-- machine.+tokenComment :: Tokenizer (TokenizerOutput String)+tokenComment = tokenizer (Just ([EOFInComment], ""))+ [ ifChar (== '<') $ \c -> consOut c <$> tokenCommentLessThanSign+ , if_ (== '-') tokenCommentEndDash+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consOut replacementChar <$> tokenComment+ , elseChar $ \c -> consOut c <$> tokenComment+ ]++-- | __HTML:__+-- @[comment less-than sign state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-state)@+-- +-- The parsing instructions for after reading @"\<"@ in the HTML comment+-- section of the state machine.+tokenCommentLessThanSign :: Tokenizer (TokenizerOutput String)+tokenCommentLessThanSign = tokenizer (Just ([EOFInComment], ""))+ [ ifChar (== '!') $ \c -> consOut c <$> tokenCommentLessThanSignBang+ , ifChar (== '<') $ \c -> consOut c <$> tokenCommentLessThanSign+ , elsePush_ tokenComment+ ]++-- | __HTML:__+-- @[comment less-than sign bang state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-state)@+-- +-- The parsing instructions for after reading @"\<!"@ in the HTML comment+-- section of the state machine.+tokenCommentLessThanSignBang :: Tokenizer (TokenizerOutput String)+tokenCommentLessThanSignBang = tokenizer (Just ([EOFInComment], ""))+ [ if_ (== '-') tokenCommentLessThanSignBangDash+ , elsePush_ tokenComment+ ]++-- | __HTML:__+-- @[comment less-than sign bang dash state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-dash-state)@+-- +-- The parsing instructions for after reading @"\<!-"@ in the HTML comment+-- section of the state machine.+tokenCommentLessThanSignBangDash :: Tokenizer (TokenizerOutput String)+tokenCommentLessThanSignBangDash = tokenizer (Just ([EOFInComment], ""))+ [ if_ (== '-') tokenCommentLessThanSignBangDashDash+ , elsePush_ tokenCommentEndDash+ ]++-- | __HTML:__+-- @[comment less-than sign bang dash dash state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-dash-dash-state)@+-- +-- The parsing instructions for after reading @"\<!--"@ in the HTML comment+-- section of the state machine.+tokenCommentLessThanSignBangDashDash :: Tokenizer (TokenizerOutput String)+tokenCommentLessThanSignBangDashDash = tokenizer (Just ([EOFInComment], ""))+ [ ifPush_ (== '>') tokenCommentEnd+ , elsePush_ $ consTokenError NestedComment <$> tokenCommentEnd+ ]++-- | __HTML:__+-- @[comment end dash state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-end-dash-state)@+-- +-- The parsing instructions for after reading @"-"@ in the HTML comment section+-- of the state machine.+tokenCommentEndDash :: Tokenizer (TokenizerOutput String)+tokenCommentEndDash = tokenizer (Just ([EOFInComment], ""))+ [ if_ (== '-') tokenCommentEnd+ , elsePush_ $ consOut '-' <$> tokenComment+ ]++-- | __HTML:__+-- @[comment end state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-end-state)@+-- +-- The parsing instructions for after reading @"--"@ in the HTML comment+-- section of the state machine.+tokenCommentEnd :: Tokenizer (TokenizerOutput String)+tokenCommentEnd = tokenizer (Just ([EOFInComment], ""))+ [ if_ (== '>') $ changeState DataState *> packToken ([], "")+ , if_ (== '!') tokenCommentEndBang+ , ifChar (== '-') $ \c -> consOut c <$> tokenCommentEnd+ , elsePush_ $ consOuts "--" <$> tokenComment+ ]++-- | __HTML:__+-- @[comment end bang state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#comment-end-bang-state)@+-- +-- The parsing instructions for after reading @"--!"@ in the HTML comment+-- section of the state machine.+tokenCommentEndBang :: Tokenizer (TokenizerOutput String)+tokenCommentEndBang = tokenizer (Just ([EOFInComment], ""))+ [ if_ (== '-') $ consOuts "--!" <$> tokenCommentEndDash+ , if_ (== '>') $ packToken ([IncorrectlyClosedComment], "")+ , elsePush_ $ consOuts "--!" <$> tokenComment+ ]
+ src/Web/Mangrove/Parse/Tokenize/Common.hs view
@@ -0,0 +1,778 @@+{-|+Description: Functions and objects used to build the tokenizer.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: provisional+Portability: portable++The tokenizer stage is in a uniquely challenging position where the standard's+instructions are given as 'switch' statements (unlike+'Web.Mangrove.Parse.Encoding.Preprocess'), it operates over the output+---including errors and re-entrant state--- of a previous stage (unlike+'Web.Mangrove.Parse.Encoding'), and the conceptual input is of a type, 'Char',+with many constructors and a lot of standard library support (unlike+'Web.Mangrove.Parse.Tree'). That intersection means that it has proven best to+abstract the parser constructors themselves, rather than to abstract the+predicate tests and/or the combinator functions as in the other stages.+-}+module Web.Mangrove.Parse.Tokenize.Common+ ( -- * Types+ -- ** Parser+ Tokenizer+ , TokenizerState ( .. )+ , TokenParserState ( .. )+ , decoderState+ , decoderDefaultState+ , CurrentTokenizerState ( .. )+ , defaultTokenizerState+ -- *** Output+ -- **** Tokenizer+ , TokenizerOutput ( .. )+ , mapErrs+ , continueState+ , endState+ , finalStateList+ , Wrapped+ , WrappedOutput+ , WrappedOutputs+ -- **** Decoder+ , TokenizerInput ( .. )+ , DecoderOutputState+ , decodedRemainder+ , setRemainder+ -- ** Data+ , Token ( .. )+ , DoctypeParams ( .. )+ , emptyDoctypeParams+ , TagParams ( .. )+ , emptyTagParams+ , BasicAttribute+ -- * Parser building+ -- ** Input+ -- *** Single token+ , tokenizer+ , if_+ , ifChar+ , else_+ , elseChar+ -- *** Token list+ , tokenizers+ , ifs_+ , ifsChar+ , elses_+ , elsesChar+ -- *** Reconsume input+ , ifPush_+ , ifPushChar+ , elsePush_+ , elsePushChar+ -- ** Output+ , packToken+ , packState+ , emit+ , emit'+ , consEmit+ , consTokenError+ , consTokenErrors+ , consTokenErrorsList+ , consOut+ , consOuts+ -- ** Combinators+ , appropriateEndTag+ , changeState+ , chunk'+ ) where+++import qualified Control.Applicative as A+import qualified Control.Monad.Trans.State as N.S++import qualified Data.ByteString as BS+import qualified Data.ByteString.Short as BS.SH+import qualified Data.Either as E+import qualified Data.HashMap.Strict as M+import qualified Data.Maybe as Y+import qualified Data.Text as T++import Web.Willow.DOM++import Web.Mangrove.Parse.Common.Error+import Web.Willow.Common.Encoding hiding ( setRemainder )+import Web.Willow.Common.Encoding.Sniffer+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch++import qualified Web.Willow.Common.Encoding as Willow++import Control.Applicative ( (<|>) )+++-- | The smallest segment of data which carries semantic meaning.+data Token+ = Doctype DoctypeParams+ -- ^ __HTML:__+ -- @DOCTYPE token@+ -- + -- 'Web.Mangrove.DOM.DocumentType', describing the language used in the+ -- document.+ | StartTag TagParams+ -- ^ __HTML:__+ -- @start tag token@+ -- + -- 'Web.Mangrove.DOM.Element', marking the start of a markup section,+ -- or a point of markup which (per the specification) doesn't contain+ -- any content.+ | EndTag TagParams+ -- ^ __HTML:__+ -- @end tag token@+ -- + -- 'Web.Mangrove.DOM.Element' with a @'/'@ character before the name,+ -- marking the end of a section opened by 'StartTag'.+ | Comment T.Text+ -- ^ __HTML:__+ -- @comment token@+ -- + -- 'Web.Mangrove.DOM.Comment', marking author's notes or other text+ -- about the document itself, rather than being part of the content.+ | Character Char+ -- ^ __HTML:__+ -- @character token@+ -- + -- 'Web.Mangrove.DOM.Character', usually containing (a small portion+ -- of) text which should rendered for the user or included in the+ -- header metadata, but occasionally subject to further processing+ -- (i.e. the content of @\<script\>@ or @\<style\>@ sections).+ | EndOfStream+ -- ^ __HTML:__+ -- @end-of-file token@+ -- + -- Represents both an explicit mark of the end of the stream, when a+ -- simple @[]@ doesn't suffice, and provides a seat to carry+ -- 'ParseError's if no other token is emitted at the same time.+ -- + -- Note: the former role doesn't have any guarantees; a stream can end+ -- without an 'EndOfStream' termination, and 'EndOfStream' tokens can occur in+ -- places other than the end of the file.+ deriving ( Eq, Show, Read )+++-- | __HTML:__+-- the data associated with a @doctype token@+-- +-- All data comprising a document type declaration which may be obtained+-- directly from the raw document stream. Values may be easily instantiated as+-- updates to 'emptyDoctypeParams'.+data DoctypeParams = DoctypeParams+ { doctypeName :: Maybe T.Text+ -- ^ The root element of the document, which may also identify the+ -- primary language used.+ , doctypePublicId :: Maybe T.Text+ -- ^ A globally-unique reference to the definition of the language.+ , doctypeSystemId :: Maybe T.Text+ -- ^ A system-dependant (but perhaps easier to access) reference to the+ -- definition of the language.+ , doctypeQuirks :: Bool+ -- ^ Whether the document should be read and rendered in a+ -- backwards-compatible manner, even if the other data in the token+ -- would match that expected by the specification. Note that 'False'+ -- value is still subject to those expectations; this just provides an+ -- override in the case of, for example, a malformed declaration.+ }+ deriving ( Eq, Show, Read )++-- | A sane default collection for easy record initialization; namely,+-- 'Nothing's and 'False'.+emptyDoctypeParams :: DoctypeParams+emptyDoctypeParams = DoctypeParams+ { doctypeName = Nothing+ , doctypePublicId = Nothing+ , doctypeSystemId = Nothing+ , doctypeQuirks = False+ }+++-- | __HTML:__+-- the data associated with a @start tag@ or an @end tag token@+-- +-- All data comprising a markup tag which may be obtained directly from the raw+-- document stream. Values may be easily instantiated as updates to+-- 'emptyTagParams'.+data TagParams = TagParams+ { tagName :: ElementName+ -- ^ The primary identifier of the markup tag, defining its behaviour+ -- during rendering, and providing a means of matching opening tags+ -- with closing ones.+ , tagIsSelfClosing :: Bool+ -- ^ Whether the tag was closed at the same point it was opened,+ -- according to the XML-style "@/>@" syntax. HTML null elements are+ -- handled in the tree construction stage instead.+ , tagAttributes :: M.HashMap T.Text T.Text+ -- ^ Finer-grained metadata attached to the markup tag.+ }+ deriving ( Eq, Show, Read )++-- | A sane default collection for easy record initialization.+emptyTagParams :: TagParams+emptyTagParams = TagParams+ { tagName = T.empty+ , tagIsSelfClosing = False+ , tagAttributes = M.empty+ }+++-- | Parser combinators written over the output of the+-- 'Web.Mangrove.Parse.Encoding.decoder' stage, segmenting the raw 'Char'+-- strings into semantic atoms.+type Tokenizer = StateParser TokenParserState [TokenizerInput]+++-- | The collection of data required to extract a list of semantic atoms from a+-- binary document stream. Values may be easily instantiated as updates to+-- 'defaultTokenizerState'.+data TokenizerState = TokenizerState+ { tokenParserState :: TokenParserState+ -- ^ The state of the current 'Web.Mangrove.Parse.Tokenize.tokenize'+ -- stage.+ , decoderState_ :: Either (Either SnifferEnvironment Encoding) (Maybe DecoderState)+ -- ^ The state of the previous 'Web.Mangrove.Parse.Encoding.decoder'+ -- stage, or the data used to initialize it. For easy access to the+ -- 'DecoderState' itself, see 'decoderState'.+ }+ deriving ( Eq, Show, Read )++-- | All the data which needs to be tracked for correct behaviour in the+-- tokenization stage.+data TokenParserState = TokenParserState+ { prevStartTag :: Maybe ElementName+ -- ^ __HTML:__+ -- @[appropriate end tag token]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#appropriate-end-tag-token)@+ -- + -- Certain states in the parser, which only emit 'Character' tokens,+ -- are able to collapse multiple tags which result in that behaviour by+ -- comparing a potential closing markup tag to the name on the+ -- 'StartTag' token which triggered the state.+ , currentState :: CurrentTokenizerState+ -- ^ The set of rules currently active in the state machine.+ , currentNodeNamespace :: Maybe Namespace+ -- ^ Certain states in the parser change behaviour if the @adjusted+ -- current node@ is not an HTML element. Given the direction of+ -- visibility in the parser stack, this stage can't directly access+ -- that (tree construction level) datum, and so that needs to be+ -- tracked redundently.+ , atEndOfStream :: Bool+ -- ^ Whether the current input stream is known to be the final part of+ -- the document stream ('True') or whether additional input may still+ -- follow ('False') and thus any finalization should not be performed.+ }+ deriving ( Eq, Show, Read )++-- | The various fixed points in the tokenization algorithm, where the parser+-- may break and re-enter seamlessly.+data CurrentTokenizerState+ = DataState+ -- ^ __HTML:__+ -- @[data state]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#data-state)@+ -- + -- The core rules, providing the most common tokenization behaviour.+ | RCDataState+ -- ^ __HTML:__+ -- @[RCDATA state]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state)@+ -- + -- 'Character'-focused production while, unlike 'RawTextState',+ -- resolving character reference values.+ | RawTextState+ -- ^ __HTML:__+ -- @[RAWTEXT state]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#rawtext-state)@+ -- + -- 'Character'-focused production which, unlike 'RCDataState', passes+ -- character reference sequences unchanged.+ | PlainTextState+ -- ^ __HTML:__+ -- @[PLAINTEXT state]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#plaintext-state)@+ -- + -- Blind conversion of the entire document stream into 'Character'+ -- tokens.+ | ScriptDataState+ -- ^ __HTML:__+ -- @[script data state]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-state)@+ -- + -- 'Character'-focused production according to the (occasionally+ -- complex) rules governing the handling of @\<script\>@ contents.+ | ScriptDataEscapedState+ -- ^ __HTML:__+ -- @[script data escaped state]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-state)@+ -- + -- 'Character'-focused production for data within a @\<!--@ / @--\>@+ -- section within 'ScriptDataState'.+ | ScriptDataDoubleEscapedState+ -- ^ __HTML:__+ -- @[script data double escaped state]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-state)@+ -- + -- 'Character'-focused production for data within a @\<script\>@+ -- section within 'ScriptDataEscapedState'.+ | CDataState+ -- ^ __HTML:__+ -- @[CDATA section state]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-state)@+ -- + -- 'Character'-focused production for data within a foreign @\<[CDATA[@+ -- / @]]\>@ escape section.+ deriving ( Eq, Ord, Bounded, Enum, Show, Read )++-- | A sane default collection for easy record initialization; namely,+-- interpret the binary stream as 'Utf8' in the primary 'DataState'.+defaultTokenizerState :: TokenizerState+defaultTokenizerState = TokenizerState+ { decoderState_ = Left $ Right Utf8+ , tokenParserState = TokenParserState+ { prevStartTag = Nothing+ , currentState = DataState+ , currentNodeNamespace = Nothing+ , atEndOfStream = False+ }+ }+++-- | The state of the previous 'Web.Mangrove.Parse.Encoding.decoder' stage.+-- Note that the high-level conceptual view of the parser stack is of each+-- stage moving along the 'BS.ByteString' as a (more or less) unified front,+-- rather than each stage independently running over the output of the+-- previous.+decoderState :: TokenizerState -> Maybe DecoderState+decoderState = E.fromRight Nothing . decoderState_++-- | As 'decoderState', but generating a default initial state if it doesn't+-- yet exist.+decoderDefaultState :: TokenizerState -> BS.ByteString -> Maybe DecoderState+decoderDefaultState state stream = case decoderState_ state of+ Right dState -> dState+ Left initialize -> Just $ either (flip sniffDecoderState stream) initialDecoderState initialize+++-- | __HTML:__+-- @[appropriate end tag token]+-- (https://html.spec.whatwg.org/multipage/parsing.html#appropriate-end-tag-token)@+-- +-- Check whether the last 'StartTag' token emitted by the tokenizer has a name+-- that matches the given 'String'.+appropriateEndTag :: String -> Tokenizer Bool+appropriateEndTag testName = do+ prevName <- prevStartTag <$> N.S.get+ return $ prevName == Just (T.pack testName)+++-- | Prepare the tokenizer to process the /next/ input 'Char' according to the+-- given set of instructions.+changeState :: CurrentTokenizerState -> Tokenizer ()+changeState newState = do+ state <- N.S.get+ N.S.put $ state+ { currentState = newState+ }+++-- | All data with which to initialize the decoder, to resume as if the state+-- machine transition hadn't been interrupted. If 'Nothing', the associated+-- 'Char' was emitted in the 'init' of several at once; in this case, the+-- decoder can't be re-entered in exactly the same place with the data wrapped+-- in this type, and so the stream must continue to be processed until the+-- first 'Just' value.+type DecoderOutputState = Maybe (Maybe DecoderState, BS.ByteString)+++-- | A smart wrapper over 'switch' to provide an end-of-stream recovery. See+-- 'tokenizers' if multiple objects may be returned at once.+tokenizer+ :: Maybe ([ParseError], out)+ -- ^ The return value to use when the stream is empty.+ -> [SwitchCase TokenizerInput Tokenizer (WrappedOutput out)]+ -- ^ If the return value is paired with 'True', the final state of the+ -- input stream is allowed as a fallback; if 'False' (i.e., something+ -- has been 'push'ed back for reconsumption) the returned state is used+ -- as-is, even if that doesn't result in a valid fixed breakpoint.+ -> Tokenizer (TokenizerOutput out)+tokenizer Nothing cases = next >>= switch' cases+tokenizer (Just (errs, out)) cases = (next >>= switch' cases) <|> do+ eos <- atEndOfStream <$> N.S.get+ if eos+ then end *> return recovery+ else A.empty+ where recovery = TokenizerOutput+ { tokenizedErrs = errs+ , tokenizedOut = out+ , tokenizedState = endState+ }++-- | As 'switch', but with an internal marker to indicate whether the final+-- state of the input stream may be retrieved from the given value, or whether+-- it must only reflect the generated output. See @switches'@ if multiple+-- objects may be returned at once.+switch'+ :: [SwitchCase TokenizerInput Tokenizer (WrappedOutput out)]+ -> TokenizerInput+ -> Tokenizer (TokenizerOutput out)+switch' cases t' = mapState' <$> switch cases t'+ where mapState' (True, out) = mapState (<|> decodedState t') out+ mapState' (False, out) = out+++-- | A smart wrapper over 'switch' to provide an end-of-stream recovery. See+-- 'tokenizer' if only a single object may be returned.+tokenizers+ :: Maybe [([ParseError], out)]+ -- ^ The return values to use when the stream is empty.+ -> [SwitchCase TokenizerInput Tokenizer (WrappedOutputs out)]+ -- ^ If the return value is paired with 'True', the final state of the+ -- input stream is allowed as a fallback; if 'False' (i.e., something+ -- has been 'push'ed back for reconsumption) the returned state is used+ -- as-is, even if that doesn't result in a valid fixed breakpoint.+ -> Tokenizer [TokenizerOutput out]+tokenizers Nothing cases = next >>= switches' cases+tokenizers (Just recovery) cases = (next >>= switches' cases) <|> do+ eos <- atEndOfStream <$> N.S.get+ if eos+ then end *> return recovery'+ else A.empty+ where recovery' = finalStateList endState . flip map recovery $ \(errs, out) ->+ TokenizerOutput errs out Nothing++-- | As 'switch', but with an internal marker to indicate whether the final+-- state of the input stream may be retrieved from the given value, or whether+-- it must only reflect the generated output. See @switch'@ if only a single+-- object may be returned.+switches'+ :: [SwitchCase TokenizerInput Tokenizer (WrappedOutputs out)]+ -> TokenizerInput+ -> Tokenizer [TokenizerOutput out]+switches' cases t' = repack <$> switch cases t'+ where repack (_, []) = []+ repack (True, [out']) = [mapState (<|> decodedState t') out']+ repack (False, [out']) = [out']+ repack (useState, t:ts) = t : repack (useState, ts)+++-- | Perform some operation on the list of errors associated with a wrapped+-- output value.+mapErrs :: ([ParseError] -> [ParseError]) -> TokenizerOutput out -> TokenizerOutput out+mapErrs f out = out { tokenizedErrs = f $ tokenizedErrs out }++-- | Perform some operation on the stream state associated with a wrapped+-- output value.+mapState+ :: (DecoderOutputState -> DecoderOutputState)+ -> TokenizerOutput out+ -> TokenizerOutput out+mapState f out = out { tokenizedState = f $ tokenizedState out }++-- | Indicate that the given wrapped value should never be used as a breakpoint+-- for future stages of the parser.+continueState :: TokenizerOutput out -> TokenizerOutput out+continueState = mapState $ const Nothing++-- | Mark the list of wrapped values such that any in the 'init' may not be+-- used as a breakpoint for future stages of the parser, and the 'last' has the+-- given 'tokenizedState'.+finalStateList :: DecoderOutputState -> [TokenizerOutput out] -> [TokenizerOutput out]+finalStateList _ [] = []+finalStateList state [t'] = [t' { tokenizedState = state }]+finalStateList state (t':ts') = continueState t' : finalStateList state ts'+++-- | Wrap an output value with a placeholder state indicating that the value+-- may not be used as a tokenizer re-entry point. If the value is a 'Token',+-- use 'emit' instead.+packToken :: ([ParseError], out) -> Tokenizer (TokenizerOutput out)+packToken = flip packState Nothing++-- | Wrap an output value such that parsing will continue with the given binary+-- document remainder if 'Just', or such that it can not serve as a breakpoint+-- with 'Nothing'.+packState :: ([ParseError], out) -> DecoderOutputState -> Tokenizer (TokenizerOutput out)+packState (errs, out) dState = return $ TokenizerOutput+ { tokenizedErrs = errs+ , tokenizedOut = out+ , tokenizedState = dState+ }+++-- | Wrap a semantic atom and associated errors into the form the tokenizing+-- parsers expect to output. See 'packToken' if the 'snd' item in the tuple is+-- not a 'Token', or 'emit'' to easily wrap the output as a singleton list.+emit :: ([ParseError], Token) -> Tokenizer (TokenizerOutput Token)+emit t'@(_, StartTag d) = do+ state <- N.S.get+ N.S.put $ state+ { prevStartTag = Just $ tagName d+ }+ packToken t'+emit t'@(_, EndTag d) = consTokenErrors errs <$> packToken t'+ where errs = Y.catMaybes [attrError, closeError]+ attrError+ | null (tagAttributes d) = Nothing+ | otherwise = Just EndTagWithAttributes+ closeError+ | tagIsSelfClosing d = Just EndTagWithTrailingSolidus+ | otherwise = Nothing+emit t' = packToken t'++-- | Wrap a semantic atom and associated errors into the form the tokenizing+-- parsers expect to output, and pack it as a singleton list. See 'packToken'+-- if the 'snd' item in the tuple is not a 'Token', or 'emit' if the outer list+-- is unnecessary.+emit' :: ([ParseError], Token) -> Tokenizer [TokenizerOutput Token]+emit' = fmap (: []) . emit++-- | Wrap a semantic atom and associated errors into the form the tokenizing+-- parsers expect to output, and add it at the head of a list of the same+-- generated by a separate parser function.+consEmit+ :: ([ParseError], Token)+ -> Tokenizer [TokenizerOutput Token]+ -> Tokenizer [TokenizerOutput Token]+consEmit tok p = do+ t <- emit tok+ ts <- p+ return $ t : ts+++-- | The collection of data returned by the "Web.Mangrove.Parse.Decode"+-- stage, and so comprising the input to the tokenizer.+data TokenizerInput = TokenizerInput+ { decodedErrs :: [ParseError]+ -- ^ Any authoring errors detected during decoding.+ , decodedOut :: Char+ -- ^ The decoded character itself.+ , decodedState :: DecoderOutputState+ -- ^ The data required to resume decoding immediately following the+ -- value, if possible. See also 'decodedRemainder'.+ }+ deriving ( Eq, Show, Read )++-- | The unparsed portion of the binary stream, /after/ parsing the associated+-- 'Char'. See also 'decodedState'.+decodedRemainder :: TokenizerInput -> Maybe BS.ByteString+decodedRemainder = fmap snd . decodedState+++-- | Store the given binary sequence as unparsable without further input, to be+-- prepended to the beginning of stream on the next call to+-- 'Web.Mangrove.Parse.Tokenize.tokenize'.+setRemainder :: BS.SH.ShortByteString -> TokenizerState -> TokenizerState+setRemainder bs state = state+ { decoderState_ = Right $ Willow.setRemainder bs <$> decoderDefaultState state BS.empty+ }+++-- | The standard output of parsers used in the tokenization stage.+-- Specifically, it contains the final state of the decoder stage in addition+-- to the generated value, to enable the recursion loop to detect the end of+-- multi-'Char' outputs and properly update the resume state. May be easily+-- instantiated through calls to 'packToken' and 'packState'.+data TokenizerOutput out = TokenizerOutput+ { tokenizedErrs :: [ParseError]+ -- ^ Any authoring errors detected during decoding or tokenization.+ , tokenizedOut :: out+ -- ^ The point of data specifically generated by the parser.+ , tokenizedState :: DecoderOutputState+ -- ^ The data required to resume tokenization immediately following the+ -- value, if possible.+ }+ deriving ( Eq, Show, Read )++instance Functor TokenizerOutput where+ fmap f out = out { tokenizedOut = f $ tokenizedOut out }+++-- | Type-level abstraction for values output by any tokenize-stage parser,+-- along with the flag required to determine the proper remainder of the input+-- stream to return.+type Wrapped out = (Bool, out)++-- | Type-level abstraction for single values output by a tokenizer, along with+-- the flag required to determine the proper remainder of the input stream to+-- return.+type WrappedOutput out = Wrapped (TokenizerOutput out)++-- | Type-level abstraction for multiple values output by a tokenizer, along+-- with the flag required to determine the proper remainder of the input stream+-- to return.+type WrappedOutputs out = Wrapped [TokenizerOutput out]+++-- | Wrap an 'If_' clause returning a single value, with a more user-friendly+-- predicate type than would otherwise be required by a raw 'SwitchCase' in the+-- tokenizer stage.+if_+ :: (Char -> Bool)+ -> Tokenizer (TokenizerOutput out)+ -> SwitchCase TokenizerInput Tokenizer (WrappedOutput out)+if_ f t = If (f . decodedOut) $ \c' ->+ (,) True . mapErrs (decodedErrs c' ++) <$> t++-- | Wrap an 'If_' clause returning a list of semantic atoms, with a more+-- user-friendly predicate type than would otherwise be required by a raw+-- 'SwitchCase' in the tokenizer stage.+ifs_+ :: (Char -> Bool)+ -> Tokenizer [TokenizerOutput Token]+ -> SwitchCase TokenizerInput Tokenizer (WrappedOutputs Token)+ifs_ f t = If (f . decodedOut) $ \c' ->+ (,) True . consTokenErrorsList (decodedErrs c') <$> t+++-- | Wrap an 'If' clause returning a single value, with more user-friendly+-- predicate and monad types than would otherwise be required by a raw+-- 'SwitchCase' in the tokenizer stage.+ifChar+ :: (Char -> Bool)+ -> (Char -> Tokenizer (TokenizerOutput out))+ -> SwitchCase TokenizerInput Tokenizer (WrappedOutput out)+ifChar f t = If (f . decodedOut) $ \c' ->+ (,) True . mapErrs (decodedErrs c' ++) <$> t (decodedOut c')++-- | Wrap an 'If' clause returning a list of semantic atoms, with more+-- user-friendly predicate and monad types than would otherwise be required by+-- a raw 'SwitchCase' in the tokenizer stage.+ifsChar+ :: (Char -> Bool)+ -> (Char -> Tokenizer [TokenizerOutput Token])+ -> SwitchCase TokenizerInput Tokenizer (WrappedOutputs Token)+ifsChar f t = If (f . decodedOut) $ \c' ->+ (,) True . consTokenErrorsList (decodedErrs c') <$> t (decodedOut c')+++-- | Wrap an 'If_' clause returning a single value, with a more user-friendly+-- predicate type, and return the input character to the stream for+-- reconsumption.+ifPush_+ :: (Char -> Bool)+ -> Tokenizer out+ -> SwitchCase TokenizerInput Tokenizer (Wrapped out)+ifPush_ f t = If (f . decodedOut) $ \c' ->+ push c' *> fmap ((,) False) t++-- | Wrap an 'If' clause returning a single value, with more user-friendly+-- predicate and monad types, and return the input character to the stream for+-- reconsumption.+ifPushChar+ :: (Char -> Bool)+ -> (Char -> Tokenizer out)+ -> SwitchCase TokenizerInput Tokenizer (Wrapped out)+ifPushChar f t = If (f . decodedOut) $ \c' ->+ push c' *> fmap ((,) False) (t $ decodedOut c')+++-- | Wrap an 'Else_' clause returning a single value, performing any relevant+-- back-end processing.+else_+ :: Tokenizer (TokenizerOutput out)+ -> SwitchCase TokenizerInput Tokenizer (WrappedOutput out)+else_ t = Else $ \c' ->+ (,) True . mapErrs (decodedErrs c' ++) <$> t++-- | Wrap an 'Else_' clause returning a list of semantic atoms, performing any+-- relevant back-end processing.+elses_+ :: Tokenizer [TokenizerOutput Token]+ -> SwitchCase TokenizerInput Tokenizer (WrappedOutputs Token)+elses_ t = Else $ \c' ->+ (,) True . consTokenErrorsList (decodedErrs c') <$> t+++-- | Wrap 'Else' clause returning a single value, with a more user-friendly+-- monad input type than would otherwise be required by a raw 'SwitchCase' in+-- the tokenizer stage.+elseChar+ :: (Char -> Tokenizer (TokenizerOutput out))+ -> SwitchCase TokenizerInput Tokenizer (WrappedOutput out)+elseChar t = Else $ \c' ->+ (,) True . mapErrs (decodedErrs c' ++) <$> t (decodedOut c')++-- | Wrap an 'Else' clause returning a list of semantic atoms, with a more+-- user-friendly monad input type than would otherwise be required by a raw+-- 'SwitchCase' in the tokenizer stage.+elsesChar+ :: (Char -> Tokenizer [TokenizerOutput Token])+ -> SwitchCase TokenizerInput Tokenizer (WrappedOutputs Token)+elsesChar t = Else $ \c' ->+ (,) True . consTokenErrorsList (decodedErrs c') <$> t (decodedOut c')+++-- | Wrap an 'Else_' clause returning a single value, performing any relevant+-- back-end processing, and return the input character to the stream for+-- reconsumption.+elsePush_+ :: Tokenizer out+ -> SwitchCase TokenizerInput Tokenizer (Wrapped out)+elsePush_ t = Else $ \c' ->+ push c' *> fmap ((,) False) t++-- | Wrap an 'Else' clause returning a single value, with a more user-friendly+-- monad type, and return the input character to the stream for reconsumption.+elsePushChar+ :: (Char -> Tokenizer out)+ -> SwitchCase TokenizerInput Tokenizer (Wrapped out)+elsePushChar t = Else $ \c' ->+ push c' *> fmap ((,) False) (t $ decodedOut c')+++-- | Add the given 'ParseError' to the collection of warnings associated with+-- the wrapped value.+consTokenError :: ParseError -> TokenizerOutput out -> TokenizerOutput out+consTokenError err = mapErrs (err :)++-- | Add the given 'ParseError's to the collection of warnings associated with+-- the wrapped value.+consTokenErrors :: [ParseError] -> TokenizerOutput out -> TokenizerOutput out+consTokenErrors errs = mapErrs (errs ++)++-- | Add the given 'ParseError's to the collection of warnings associated with+-- the first wrapped value in the list, or create a placeholder if it does not+-- yet contain any 'Token's.+consTokenErrorsList :: [ParseError] -> [TokenizerOutput Token] -> [TokenizerOutput Token]+consTokenErrorsList [] [] = []+consTokenErrorsList errs [] = [TokenizerOutput errs EndOfStream Nothing]+consTokenErrorsList errs (t:ts) = consTokenErrors errs t : ts+++-- | Push the given value to the head of the wrapped collection.+-- +-- @+-- consOut x == fmap (x :)+-- @+consOut :: out -> TokenizerOutput [out] -> TokenizerOutput [out]+consOut x = fmap (x :)++-- | Push the given values to the head of the wrapped collection.+-- +-- @+-- consOut xs == fmap (xs ++)+-- @+consOuts :: [out] -> TokenizerOutput [out] -> TokenizerOutput [out]+consOuts xs = fmap (xs ++)+++-- | Lift a call to 'chunk' over the wrapped 'Char's which make up the input to+-- the tokenization stage. As an additional quality-of-life, perform some+-- function on the input 'String' before comparing it to the desired value; for+-- behaviour according to the base 'chunk' function, use @'chunk'' 'id'@.+chunk' :: (Char -> Char) -> String -> Tokenizer [TokenizerInput]+chunk' f test = nextChunk (fromIntegral $ length test) >>=+ satisfying (\str -> map (f . decodedOut) str == test)+++-- | Produce a placeholder value for use in generating a 'TokenizerOutput'+-- value located at the end of the byte stream.+endState :: DecoderOutputState+endState = Just (Nothing, BS.empty)
+ src/Web/Mangrove/Parse/Tokenize/Data.hs view
@@ -0,0 +1,34 @@+{-|+Description: Basic tokenization rules for the character stream.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.Data+ ( tokenData+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tokenize.Character+import Web.Mangrove.Parse.Tokenize.Tag+++-- | __HTML:__+-- @[data state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#data-state)@+-- +-- The parsing instructions rooted in the 'DataState' section of the state+-- machine.+tokenData :: Tokenizer [TokenizerOutput Token]+tokenData = tokenizers (Just [([], EndOfStream)])+ [ ifs_ (== '&') $ tokenCharacterReference False >>= flushCharRef+ , ifs_ (== '<') tokenTagOpen+ , ifs_ (== '\NUL') $ emit' ([UnexpectedNullCharacter], Character '\NUL')+ , elsesChar $ \c -> emit' ([], Character c)+ ]
+ src/Web/Mangrove/Parse/Tokenize/Dispatcher.hs view
@@ -0,0 +1,42 @@+{-|+Description: Central directory for tokenization rules.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.Dispatcher+ ( dispatcher+ ) where+++import qualified Control.Monad.Trans.State as N.S++import Web.Mangrove.Parse.Tokenize.CData+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tokenize.Data+import Web.Mangrove.Parse.Tokenize.RawText+import Web.Mangrove.Parse.Tokenize.RCData+import Web.Mangrove.Parse.Tokenize.PlainText+import Web.Mangrove.Parse.Tokenize.ScriptData+import Web.Mangrove.Parse.Tokenize.ScriptDataEscaped+import Web.Mangrove.Parse.Tokenize.ScriptDataDoubleEscaped+++-- | Delegate parsing the binary stream to the appropriate part of the finite+-- state machine, according to the top-level 'CurrentTokenizerState'.+dispatcher :: Tokenizer [TokenizerOutput Token]+dispatcher = do+ state <- N.S.get+ case currentState state of+ DataState -> tokenData+ RCDataState -> tokenRCData+ RawTextState -> tokenRawText+ PlainTextState -> fmap (: []) tokenPlainText+ ScriptDataState -> tokenScriptData+ ScriptDataEscapedState -> tokenScriptDataEscaped+ ScriptDataDoubleEscapedState -> tokenScriptDataDoubleEscaped+ CDataState -> tokenCDataSection
+ src/Web/Mangrove/Parse/Tokenize/Dispatcher.hs-boot view
@@ -0,0 +1,14 @@+{-|+Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com+-}+module Web.Mangrove.Parse.Tokenize.Dispatcher+ ( dispatcher+ ) where+++import Web.Mangrove.Parse.Tokenize.Common+++dispatcher :: Tokenizer [TokenizerOutput Token]
+ src/Web/Mangrove/Parse/Tokenize/Doctype.hs view
@@ -0,0 +1,383 @@+{-|+Description: Tokenization rules for characters comprising a @\<!DOCTYPE\>@ declaration.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.Doctype+ ( tokenDoctype+ ) where+++import qualified Data.Char as C+import qualified Data.Text as T++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Parser.Util++import Control.Applicative ( (<|>) )+++-- | An intermediate form of 'DoctypeParams' with unpacked parameter types to+-- facilitate construction. Specifically, 'Char'-by-'Char' construction of a+-- 'String' followed by a single 'T.pack' is much faster than repeated @O(n)@+-- calls to 'T.cons'. Values may be easily instantiated as updates to+-- @emptyDoctypeData@ or @quirksDoctype@.+data DoctypeParams' = DoctypeParams'+ { doctypeName' :: Maybe String+ -- ^ See 'doctypeName'.+ , doctypePublicId' :: Maybe String+ -- ^ See 'doctypePublicId'.+ , doctypeSystemId' :: Maybe String+ -- ^ See 'doctypeSystemId'.+ , doctypeQuirks' :: Bool+ -- ^ See 'doctypeQuirks'.+ }+ deriving ( Eq, Show, Read )++-- | A sane default collection for easy record initialization; namely,+-- 'Nothing's and 'False'. See also @quirksDoctype@.+emptyDoctypeData :: DoctypeParams'+emptyDoctypeData = DoctypeParams'+ { doctypeName' = Nothing+ , doctypePublicId' = Nothing+ , doctypeSystemId' = Nothing+ , doctypeQuirks' = False+ }++-- | As like @emptyDoctypeData@, but indicating that the document should be+-- forced to render in the quirks mode.+quirksDoctype :: DoctypeParams'+quirksDoctype = emptyDoctypeData+ { doctypeQuirks' = True+ }+++-- | Wrap a given collection of data into the payload of a 'Doctype'.+packDoctypeToken :: TokenizerOutput DoctypeParams' -> TokenizerOutput Token+packDoctypeToken d' = flip fmap d' $ \d -> Doctype $ emptyDoctypeParams+ { doctypeName = T.pack <$> doctypeName' d+ , doctypePublicId = T.pack <$> doctypePublicId' d+ , doctypeSystemId = T.pack <$> doctypeSystemId' d+ , doctypeQuirks = doctypeQuirks' d+ }+++-- | Mark a collection of data as containing a doctype public identifier,+-- without overwriting any data which may already be set.+ensurePublicId :: TokenizerOutput DoctypeParams' -> TokenizerOutput DoctypeParams'+ensurePublicId = fmap $ \d -> d+ { doctypePublicId' = doctypePublicId' d <|> Just ""+ }++-- | Mark a collection of data as containing a doctype system identifier,+-- without overwriting any data which may already be set.+ensureSystemId :: TokenizerOutput DoctypeParams' -> TokenizerOutput DoctypeParams'+ensureSystemId = fmap $ \d -> d+ { doctypeSystemId' = doctypeSystemId' d <|> Just ""+ }++-- | Mark a collection of data as forcing a document to render in quirks mode.+ensureQuirks :: TokenizerOutput DoctypeParams' -> TokenizerOutput DoctypeParams'+ensureQuirks = fmap $ \d -> d+ { doctypeQuirks' = True+ }+++-- | Prepend a character to the unpacked name of the root element -- or, in+-- other words, the markup language used.+consDoctypeName :: Char -> TokenizerOutput DoctypeParams' -> TokenizerOutput DoctypeParams'+consDoctypeName c = fmap $ \d -> d+ { doctypeName' = fmap (c :) $ doctypeName' d <|> Just []+ }++-- | Prepend a character to the unpacked public identifier of a doctype.+consPublicId :: Char -> TokenizerOutput DoctypeParams' -> TokenizerOutput DoctypeParams'+consPublicId c = fmap $ \d -> d+ { doctypePublicId' = fmap (c :) $ doctypePublicId' d <|> Just []+ }++-- | Prepend a character to the unpacked system identifier of a doctype.+consSystemId :: Char -> TokenizerOutput DoctypeParams' -> TokenizerOutput DoctypeParams'+consSystemId c = fmap $ \d -> d+ { doctypeSystemId' = fmap (c :) $ doctypeSystemId' d <|> Just []+ }+++-- | __HTML:__+-- @[DOCTYPE state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#doctype-state)@+-- +-- The parsing instructions for after reading @"\<!DOCTYPE"@ in a section of the+-- state machine which allows markup declarations.+tokenDoctype :: Tokenizer (TokenizerOutput Token)+tokenDoctype = tokenizer (Just ([EOFInDoctype], quirksDoctype'))+ [ if_ isAsciiWhitespace $ fmap packDoctypeToken tokenBeforeDoctypeName+ , ifPush_ (== '>') $ fmap packDoctypeToken tokenBeforeDoctypeName+ , elsePush_ $ fmap (packDoctypeToken . consTokenError MissingWhitespaceBeforeDoctypeName)+ tokenBeforeDoctypeName+ ]+ where quirksDoctype' = Doctype $ emptyDoctypeParams+ { doctypeQuirks = True+ }++-- | __HTML:__+-- @[before DOCTYPE name state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-name-state)@+-- +-- The parsing instructions for after reading @"\<!DOCTYPE "@ in the doctype+-- declaration section of the state machine.+tokenBeforeDoctypeName :: Tokenizer (TokenizerOutput DoctypeParams')+tokenBeforeDoctypeName = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenBeforeDoctypeName+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consDoctypeName replacementChar <$>+ tokenDoctypeName+ , if_ (== '>') $ packToken ([MissingDoctypeName], quirksDoctype)+ , elseChar $ \c -> consDoctypeName (toAsciiLower c) <$> tokenDoctypeName+ ]++-- | __HTML:__+-- @[DOCTYPE name state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#doctype-name-state)@+-- +-- The parsing instructions at the initial segment of the doctype declaration+-- section of the state machine.+tokenDoctypeName :: Tokenizer (TokenizerOutput DoctypeParams')+tokenDoctypeName = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenAfterDoctypeName+ , if_ (== '>') $ changeState DataState *> packToken ([], emptyDoctypeData)+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consDoctypeName replacementChar <$>+ tokenDoctypeName+ , elseChar $ \c -> consDoctypeName (toAsciiLower c) <$> tokenDoctypeName+ ]++-- | __HTML:__+-- @[after DOCTYPE name state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-name-state)@+-- +-- The parsing instructions for after reading the root element in the doctype+-- declaration section of the state machine.+tokenAfterDoctypeName :: Tokenizer (TokenizerOutput DoctypeParams')+tokenAfterDoctypeName = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenAfterDoctypeName+ , if_ (== '>') $ changeState DataState *> packToken ([], emptyDoctypeData)+ , elsePush_ tokenAfterDoctypeName'+ ]++-- | Dispatch the parser according to a @"PUBLIC"@ or @"SYSTEM"@ keyword in the+-- doctype declaration.+tokenAfterDoctypeName' :: Tokenizer (TokenizerOutput DoctypeParams')+tokenAfterDoctypeName' = choice+ [ chunk' C.toUpper "PUBLIC" *> tokenAfterDoctypePublicKeyword+ , chunk' C.toUpper "SYSTEM" *> tokenAfterDoctypeSystemKeyword+ , consTokenError InvalidCharacterSequenceAfterDoctypeName . ensureQuirks <$> tokenBogusDoctype+ ]++-- | __HTML:__+-- @[after DOCTYPE public keyword state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-public-keyword-state)@+-- +-- The parsing instructions for after reading @"PUBLIC"@ in the doctype+-- declaration section of the state machine.+tokenAfterDoctypePublicKeyword :: Tokenizer (TokenizerOutput DoctypeParams')+tokenAfterDoctypePublicKeyword = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenBeforeDoctypePublicIdentifier+ , if_ (== '"') $ consTokenError MissingWhitespaceAfterDoctypePublicKeyword . ensurePublicId <$>+ tokenDoctypePublicIdentifierDoubleQuoted+ , if_ (== '\'') $ consTokenError MissingWhitespaceAfterDoctypePublicKeyword . ensurePublicId <$>+ tokenDoctypePublicIdentifierSingleQuoted+ , if_ (== '>') $ changeState DataState *>+ packToken ([MissingDoctypePublicIdentifier], quirksDoctype)+ , elsePush_ $ consTokenError MissingQuoteBeforeDoctypePublicIdentifier . ensureQuirks <$>+ tokenBogusDoctype+ ]++-- | __HTML:__+-- @[before DOCTYPE public identifier state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-public-identifier-state)@+-- +-- The parsing instructions for after reading @"PUBLIC "@ in the doctype+-- declaration section of the state machine.+tokenBeforeDoctypePublicIdentifier :: Tokenizer (TokenizerOutput DoctypeParams')+tokenBeforeDoctypePublicIdentifier = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenBeforeDoctypePublicIdentifier+ , if_ (== '"') $ fmap ensurePublicId tokenDoctypePublicIdentifierDoubleQuoted+ , if_ (== '\'') $ fmap ensurePublicId tokenDoctypePublicIdentifierSingleQuoted+ , if_ (== '>') $ changeState DataState *>+ packToken ([MissingDoctypePublicIdentifier], quirksDoctype)+ , elsePush_ $ consTokenError MissingQuoteBeforeDoctypePublicIdentifier . ensureQuirks <$>+ tokenBogusDoctype+ ]++-- | __HTML:__+-- @[DOCTYPE public identifier (double-quoted) state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#doctype-public-identifier-(double-quoted\)-state)@+-- +-- The parsing instructions for after reading @"PUBLIC \\""@ in the doctype+-- declaration section of the state machine.+tokenDoctypePublicIdentifierDoubleQuoted :: Tokenizer (TokenizerOutput DoctypeParams')+tokenDoctypePublicIdentifierDoubleQuoted = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ (== '"') tokenAfterDoctypePublicIdentifier+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consPublicId replacementChar <$>+ tokenDoctypePublicIdentifierDoubleQuoted+ , if_ (== '>') $ changeState DataState *> + packToken ([AbruptDoctypePublicIdentifier], quirksDoctype)+ , elseChar $ \c -> consPublicId c <$> tokenDoctypePublicIdentifierDoubleQuoted+ ]++-- | __HTML:__+-- @[DOCTYPE public identifier (single-quoted) state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#doctype-public-identifier-(single-quoted\)-state)@+-- +-- The parsing instructions for after reading @"PUBLIC '"@ in the doctype+-- declaration section of the state machine.+tokenDoctypePublicIdentifierSingleQuoted :: Tokenizer (TokenizerOutput DoctypeParams')+tokenDoctypePublicIdentifierSingleQuoted = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ (== '\'') tokenAfterDoctypePublicIdentifier+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consPublicId replacementChar <$>+ tokenDoctypePublicIdentifierSingleQuoted+ , if_ (== '>') $ changeState DataState *> + packToken ([AbruptDoctypePublicIdentifier], quirksDoctype)+ , elseChar $ \c -> consPublicId c <$> tokenDoctypePublicIdentifierSingleQuoted+ ]++-- | __HTML:__+-- @[after DOCTYPE public identifier state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-public-identifier-state)@+-- +-- The parsing instructions for after reading the closing quote of a public+-- identifier in the doctype declaration section of the state machine.+tokenAfterDoctypePublicIdentifier :: Tokenizer (TokenizerOutput DoctypeParams')+tokenAfterDoctypePublicIdentifier = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenBetweenDoctypePublicAndSystemIdentifiers+ , if_ (== '>') $ changeState DataState *> packToken ([], emptyDoctypeData)+ , if_ (== '"') $+ consTokenError MissingWhitespaceBetweenDoctypePublicAndSystemIdentifiers . ensureSystemId <$>+ tokenDoctypeSystemIdentifierDoubleQuoted+ , if_ (== '\'') $+ consTokenError MissingWhitespaceBetweenDoctypePublicAndSystemIdentifiers . ensureSystemId <$>+ tokenDoctypeSystemIdentifierSingleQuoted+ , elsePush_ $+ consTokenError MissingQuoteBeforeDoctypeSystemIdentifier . ensureQuirks <$>+ tokenBogusDoctype+ ]++-- | __HTML:__+-- @[between DOCTYPE public and system identifier state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#between-doctype-public-and-system-identifiers-state)@+-- +-- The parsing instructions for after reading a whitespace character after the+-- closing quote of a public identifier in the doctype declaration section of+-- the state machine.+tokenBetweenDoctypePublicAndSystemIdentifiers :: Tokenizer (TokenizerOutput DoctypeParams')+tokenBetweenDoctypePublicAndSystemIdentifiers = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenBetweenDoctypePublicAndSystemIdentifiers+ , if_ (== '>') $ changeState DataState *> packToken ([], emptyDoctypeData)+ , if_ (== '"') $ fmap ensureSystemId tokenDoctypeSystemIdentifierDoubleQuoted+ , if_ (== '\'') $ fmap ensureSystemId tokenDoctypeSystemIdentifierSingleQuoted+ , elsePush_ $ consTokenError MissingQuoteBeforeDoctypeSystemIdentifier . ensureQuirks <$>+ tokenBogusDoctype+ ]++-- | __HTML:__+-- @[after DOCTYPE system keyword state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-system-keyword-state)@+-- +-- The parsing instructions for after reading @"SYSTEM"@ in the doctype+-- declaration section of the state machine.+tokenAfterDoctypeSystemKeyword :: Tokenizer (TokenizerOutput DoctypeParams')+tokenAfterDoctypeSystemKeyword = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenBeforeDoctypeSystemIdentifier+ , if_ (== '"') $ consTokenError MissingWhitespaceAfterDoctypeSystemKeyword . ensureSystemId <$>+ tokenDoctypeSystemIdentifierDoubleQuoted+ , if_ (== '\'') $ consTokenError MissingWhitespaceAfterDoctypeSystemKeyword . ensureSystemId <$>+ tokenDoctypeSystemIdentifierSingleQuoted+ , if_ (== '>') $ changeState DataState *>+ packToken ([MissingDoctypeSystemIdentifier], quirksDoctype)+ , elsePush_ $ consTokenError MissingQuoteBeforeDoctypeSystemIdentifier . ensureQuirks <$>+ tokenBogusDoctype+ ]++-- | __HTML:__+-- @[before DOCTYPE system identifier state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-system-identifier-state)@+-- +-- The parsing instructions for after reading @"SYSTEM "@ in the doctype+-- declaration section of the state machine.+tokenBeforeDoctypeSystemIdentifier :: Tokenizer (TokenizerOutput DoctypeParams')+tokenBeforeDoctypeSystemIdentifier = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenBeforeDoctypeSystemIdentifier+ , if_ (== '"') $ fmap ensureSystemId tokenDoctypeSystemIdentifierDoubleQuoted+ , if_ (== '\'') $ fmap ensureSystemId tokenDoctypeSystemIdentifierSingleQuoted+ , if_ (== '>') $ changeState DataState *>+ packToken ([MissingDoctypeSystemIdentifier], quirksDoctype)+ , elsePush_ $ consTokenError MissingQuoteBeforeDoctypeSystemIdentifier . ensureQuirks <$>+ tokenBogusDoctype+ ]++-- | __HTML:__+-- @[DOCTYPE system identifier (double-quoted) state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#doctype-system-identifier-(double-quoted\)-state)@+-- +-- The parsing instructions for after reading @"SYSTEM \\""@, or a @'"'@ after+-- a public identifier, in the doctype declaration section of the state+-- machine.+tokenDoctypeSystemIdentifierDoubleQuoted :: Tokenizer (TokenizerOutput DoctypeParams')+tokenDoctypeSystemIdentifierDoubleQuoted = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ (== '"') tokenAfterDoctypeSystemIdentifier+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consSystemId replacementChar <$>+ tokenDoctypeSystemIdentifierDoubleQuoted+ , if_ (== '>') $ changeState DataState *> + packToken ([AbruptDoctypeSystemIdentifier], quirksDoctype)+ , elseChar $ \c -> consSystemId c <$> tokenDoctypeSystemIdentifierDoubleQuoted+ ]++-- | __HTML:__+-- @[DOCTYPE system identifier (single-quoted) state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#doctype-system-identifier-(single-quoted\)-state)@+-- +-- The parsing instructions for after reading @"SYSTEM '"@, or a @'\''@ after+-- a public identifier, in the doctype declaration section of the state+-- machine.+tokenDoctypeSystemIdentifierSingleQuoted :: Tokenizer (TokenizerOutput DoctypeParams')+tokenDoctypeSystemIdentifierSingleQuoted = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ (== '\'') tokenAfterDoctypeSystemIdentifier+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consSystemId replacementChar <$>+ tokenDoctypeSystemIdentifierSingleQuoted+ , if_ (== '>') $ changeState DataState *> + packToken ([AbruptDoctypeSystemIdentifier], quirksDoctype)+ , elseChar $ \c -> consSystemId c <$> tokenDoctypeSystemIdentifierSingleQuoted+ ]++-- | __HTML:__+-- @[after DOCTYPE system identifier state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-system-identifier-state)@+-- +-- The parsing instructions for after reading the closing quote of a system+-- identifier in the doctype declaration section of the state machine.+tokenAfterDoctypeSystemIdentifier :: Tokenizer (TokenizerOutput DoctypeParams')+tokenAfterDoctypeSystemIdentifier = tokenizer (Just ([EOFInDoctype], quirksDoctype))+ [ if_ isAsciiWhitespace tokenAfterDoctypeSystemIdentifier+ , if_ (== '>') $ changeState DataState *> packToken ([], emptyDoctypeData)+ , elsePushChar $ \c -> consTokenError (UnexpectedCharacterAfterDoctypeSystemIdentifier c) <$>+ tokenBogusDoctype+ ]++-- | __HTML:__+-- @[bogus DOCTYPE state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#bogus-doctype-state)@+-- +-- The parsing instructions for after reading a disallowed character sequence+-- in the doctype declaration section of the state machine.+tokenBogusDoctype :: Tokenizer (TokenizerOutput DoctypeParams')+tokenBogusDoctype = tokenizer (Just ([], emptyDoctypeData))+ [ if_ (== '>') $ changeState DataState *> packToken ([], emptyDoctypeData)+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter <$> tokenBogusDoctype+ , else_ tokenBogusDoctype+ ]
+ src/Web/Mangrove/Parse/Tokenize/PlainText.hs view
@@ -0,0 +1,31 @@+{-|+Description: Tokenization rules for characters which should not be processed further, without an exit condition.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.PlainText+ ( tokenPlainText+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Willow.Common.Encoding.Character+++-- | __HTML:__+-- @[PLAINTEXT state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#plaintext-state)@+-- +-- The parsing instructions rooted in the 'PlainTextState' section of the state+-- machine.+tokenPlainText :: Tokenizer (TokenizerOutput Token)+tokenPlainText = tokenizer Nothing+ [ if_ (== '\NUL') $ emit ([UnexpectedNullCharacter], Character replacementChar)+ , elseChar $ \c -> emit ([], Character c)+ ]
+ src/Web/Mangrove/Parse/Tokenize/RCData.hs view
@@ -0,0 +1,35 @@+{-|+Description: Tokenization rules for characters which should be minimally processed.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.RCData+ ( tokenRCData+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tokenize.Character+import Web.Mangrove.Parse.Tokenize.Tag+import Web.Willow.Common.Encoding.Character+++-- | __HTML:__+-- @[RCDATA state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state)@+-- +-- The parsing instructions rooted in the 'RCDataState' section of the state+-- machine.+tokenRCData :: Tokenizer [TokenizerOutput Token]+tokenRCData = tokenizers Nothing+ [ ifs_ (== '&') $ tokenCharacterReference False >>= flushCharRef+ , ifs_ (== '<') tokenAppropriateEndTagLessThanSign+ , ifs_ (== '\NUL') $ emit' ([UnexpectedNullCharacter], Character replacementChar)+ , elsesChar $ \c -> emit' ([], Character c)+ ]
+ src/Web/Mangrove/Parse/Tokenize/RawText.hs view
@@ -0,0 +1,33 @@+{-|+Description: Tokenization rules for characters which should not be processed further.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.RawText+ ( tokenRawText+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tokenize.Tag+import Web.Willow.Common.Encoding.Character+++-- | __HTML:__+-- @[RAWTEXT state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#rawtext-state)@+-- +-- The parsing instructions rooted in the 'RawTextState' section of the state+-- machine.+tokenRawText :: Tokenizer [TokenizerOutput Token]+tokenRawText = tokenizers Nothing+ [ ifs_ (== '<') tokenAppropriateEndTagLessThanSign+ , ifs_ (== '\NUL') $ emit' ([UnexpectedNullCharacter], Character replacementChar)+ , elsesChar $ \c -> emit' ([], Character c)+ ]
+ src/Web/Mangrove/Parse/Tokenize/ScriptData.hs view
@@ -0,0 +1,51 @@+{-# HLINT ignore "Use ." #-}++{-|+Description: Tokenization rules for characters within @\<script\>@ ... @\</script\>@+ sections.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.ScriptData+ ( tokenScriptData+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tokenize.ScriptDataEscaped+import Web.Mangrove.Parse.Tokenize.Tag+import Web.Willow.Common.Encoding.Character+++-- | __HTML:__+-- @[script data state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-state)@+-- +-- The parsing instructions rooted in the 'ScriptDataState' section of the+-- state machine.+tokenScriptData :: Tokenizer [TokenizerOutput Token]+tokenScriptData = tokenizers Nothing+ [ ifs_ (== '<') tokenScriptDataLessThanSign+ , ifs_ (== '\NUL') $ emit' ([UnexpectedNullCharacter], Character replacementChar)+ , elsesChar $ \c -> emit' ([], Character c)+ ]++-- | __HTML:__+-- @[script data less-than sign state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-less-than-sign-state)@+-- +-- The parsing instructions for after reading @"\<"@ in the 'ScriptDataState'+-- section of the state machine.+tokenScriptDataLessThanSign :: Tokenizer [TokenizerOutput Token]+tokenScriptDataLessThanSign = tokenizers (Just [([], Character '<')])+ [ ifs_ (== '/') tokenAppropriateEndTagOpen+ , ifs_ (== '!') $ consEmit ([], Character '<') . consEmit ([], Character '!') $+ tokenScriptDataEscapeStart+ , elsePush_ $ emit' ([], Character '<')+ ]
+ src/Web/Mangrove/Parse/Tokenize/ScriptDataDoubleEscaped.hs view
@@ -0,0 +1,172 @@+{-|+Description: Tokenization rules for characters within @\<script\>@ ... @\</script\>@+ sections in HTML comments in script data.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.ScriptDataDoubleEscaped+ ( tokenScriptDataDoubleEscaped+ , tokenScriptDataDoubleEscapeStart+ ) where+++import qualified Control.Applicative as A++import qualified Data.Maybe as Y++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Parser++import {-# SOURCE #-} Web.Mangrove.Parse.Tokenize.Dispatcher+++-- | __HTML:__+-- @[script data escape start state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-state)@+-- +-- The parsing instructions for after reading @"\<"@ when the next character is+-- a letter in the 'ScriptDataEscapedState' section of the state machine.+tokenScriptDataDoubleEscapeStart :: Tokenizer [TokenizerOutput Token]+tokenScriptDataDoubleEscapeStart = tokenScriptDataDoubleEscapeStart' >>= \t' ->+ case map toAsciiLower $ tokenizedOut t' of+ "script" -> do+ recovery <- map unpackToken <$> output t'+ tokenizers (Just $ recovery ++ [([EOFInScriptHtmlCommentLikeText], EndOfStream)])+ [ ifsChar isAsciiWhitespace $ escape t'+ , ifsChar (== '/') $ escape t'+ , ifsChar (== '>') $ escape t'+ , elsePush_ $ output t'+ ]+ _ -> output t'+ where escape t' c = do+ changeState ScriptDataDoubleEscapedState+ cs <- finalStateList Nothing <$> output t'+ c' <- emit' ([], Character c)+ cs' <- tokenScriptDataDoubleEscaped+ return $ cs ++ c' ++ cs'+ output t' = case tokenizedOut t' of+ [] -> case tokenizedErrs t' of+ [] -> return []+ errs -> consTokenErrorsList errs <$> dispatcher+ (c:cs) -> emits (tokenizedState t') $+ (tokenizedErrs t', Character c) : [([], Character c') | c' <- cs]+ emits state ts = finalStateList state <$> mapM emit ts+ unpackToken t' = (tokenizedErrs t', tokenizedOut t')++-- | Loop within the __HTML__ @[script data double escape start state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-start-state)@+-- to read the name of the tag.+tokenScriptDataDoubleEscapeStart' :: Tokenizer (TokenizerOutput String)+tokenScriptDataDoubleEscapeStart' = tokenizer (Just ([], ""))+ [ ifChar isAsciiAlpha $ \c -> consOut c <$> tokenScriptDataDoubleEscapeStart'+ , elsePush_ $ packToken ([], "")+ ]++-- | __HTML:__+-- @[script data double escaped state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state)@+-- +-- The parsing instructions rooted in the 'ScriptDataDoubleEscapedState'+-- section of the state machine.+tokenScriptDataDoubleEscaped :: Tokenizer [TokenizerOutput Token]+tokenScriptDataDoubleEscaped = tokenizers (Just [([EOFInScriptHtmlCommentLikeText], EndOfStream)])+ [ ifs_ (== '-') $ consEmit ([], Character '-') tokenScriptDataDoubleEscapedDash+ , ifs_ (== '<') $ consEmit ([], Character '<') tokenScriptDataDoubleEscapedLessThanSign+ , ifs_ (== '\NUL') $ emit' ([UnexpectedNullCharacter], Character replacementChar)+ , elsesChar $ \c -> do+ e <- A.optional end+ consEmit ([], Character c) $ if Y.isJust e+ then emit' ([EOFInScriptHtmlCommentLikeText], EndOfStream)+ else return []+ ]++-- | __HTML:__+-- @[script data double escaped dash state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-double-dash-state)@+-- +-- The parsing instructions for after reading @"-"@ in the+-- 'ScriptDataDoubleEscapedState' section of the state machine.+tokenScriptDataDoubleEscapedDash :: Tokenizer [TokenizerOutput Token]+tokenScriptDataDoubleEscapedDash = tokenizers (Just [([EOFInScriptHtmlCommentLikeText], EndOfStream)])+ [ ifs_ (== '-') $ consEmit ([], Character '-') tokenScriptDataDoubleEscapedDashDash+ , ifs_ (== '<') $ consEmit ([], Character '<') tokenScriptDataDoubleEscapedLessThanSign+ , ifs_ (== '\NUL') $ emit' ([UnexpectedNullCharacter], Character replacementChar)+ , elsesChar $ \c -> emit' ([], Character c)+ ]++-- | __HTML:__+-- @[script data double escaped dash dash state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-double-dash-dash-state)@+-- +-- The parsing instructions for after reading @"--"@ in the+-- 'ScriptDataDoubleEscapedState' section of the state machine.+tokenScriptDataDoubleEscapedDashDash :: Tokenizer [TokenizerOutput Token]+tokenScriptDataDoubleEscapedDashDash = tokenizers (Just [([EOFInScriptHtmlCommentLikeText], EndOfStream)])+ [ ifs_ (== '-') $ consEmit ([], Character '-') tokenScriptDataDoubleEscapedDashDash+ , ifs_ (== '<') $ consEmit ([], Character '<') tokenScriptDataDoubleEscapedLessThanSign+ , ifs_ (== '>') $ changeState ScriptDataState *> emit' ([], Character '>')+ , ifs_ (== '\NUL') $ emit' ([UnexpectedNullCharacter], Character replacementChar)+ , elsesChar $ \c -> emit' ([], Character c)+ ]++-- | __HTML:__+-- @[script data double escaped less-than state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-less-than-state)@+-- +-- The parsing instructions for after reading @"\<"@ in the+-- 'ScriptDataDoubleEscapedState' section of the state machine.+tokenScriptDataDoubleEscapedLessThanSign :: Tokenizer [TokenizerOutput Token]+tokenScriptDataDoubleEscapedLessThanSign = tokenizers (Just [([EOFInScriptHtmlCommentLikeText], EndOfStream)])+ [ ifs_ (== '/') $ consEmit ([], Character '/') tokenScriptDataDoubleEscapeEnd+ , elsePush_ $ return []+ ]++-- | __HTML:__+-- @[script data double escape end state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-end-state)@+-- +-- The parsing instructions for after reading @"\</"@ in the+-- 'ScriptDataDoubleEscapedState' section of the state machine.+tokenScriptDataDoubleEscapeEnd :: Tokenizer [TokenizerOutput Token]+tokenScriptDataDoubleEscapeEnd = tokenScriptDataDoubleEscapeEnd' >>= \t' -> do+ case map toAsciiLower $ tokenizedOut t' of+ "script" -> do+ recovery <- map unpackToken <$> output t'+ tokenizers (Just $ recovery ++ [([EOFInScriptHtmlCommentLikeText], EndOfStream)])+ [ ifsChar isAsciiWhitespace $ escape t'+ , ifsChar (== '/') $ escape t'+ , ifsChar (== '>') $ escape t'+ , elsePush_ $ output t'+ ]+ _ -> output t'+ where escape t' c = do+ changeState ScriptDataEscapedState+ cs <- finalStateList Nothing <$> output t'+ c' <- emit' ([], Character c)+ return $ cs ++ c'+ output t' = do+ buffer <- emits (tokenizedState t') [([], Character c) | c <- tokenizedOut t']+ case tokenizedErrs t' of+ [] -> return buffer+ errs -> consTokenErrorsList errs <$> case buffer of+ [] -> dispatcher+ _ -> return buffer+ emits state ts = finalStateList state <$> mapM emit ts+ unpackToken t' = (tokenizedErrs t', tokenizedOut t')+++-- | Loop within the __HTML__ @[script data double escape end state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-end-state)@+-- to read the name of the tag.+tokenScriptDataDoubleEscapeEnd' :: Tokenizer (TokenizerOutput String)+tokenScriptDataDoubleEscapeEnd' = tokenizer (Just ([], ""))+ [ ifChar isAsciiAlpha $ \c -> consOut c <$> tokenScriptDataDoubleEscapeEnd'+ , elsePush_ $ packToken ([], "")+ ]
+ src/Web/Mangrove/Parse/Tokenize/ScriptDataEscaped.hs view
@@ -0,0 +1,103 @@+{-|+Description: Tokenization rules for characters within @\<!--@ ... @--\>@+ sections in script data.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.ScriptDataEscaped+ ( tokenScriptDataEscaped+ , tokenScriptDataEscapeStart+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tokenize.ScriptDataDoubleEscaped+import Web.Mangrove.Parse.Tokenize.Tag+import Web.Willow.Common.Encoding.Character+++-- | __HTML:__+-- @[script data escape start state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-state)@+-- +-- The parsing instructions for after reading @"\<!"@ in the 'ScriptDataState'+-- section of the state machine.+tokenScriptDataEscapeStart :: Tokenizer [TokenizerOutput Token]+tokenScriptDataEscapeStart = tokenizers (Just [([], EndOfStream)])+ [ ifs_ (== '-') $ consEmit ([], Character '-') tokenScriptDataEscapeStartDash+ , elsePush_ $ return []+ ]++-- | __HTML:__+-- @[script data escape start dash state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-state)@+-- +-- The parsing instructions for after reading @"\<!-"@ in the 'ScriptDataState'+-- section of the state machine.+tokenScriptDataEscapeStartDash :: Tokenizer [TokenizerOutput Token]+tokenScriptDataEscapeStartDash = tokenizers (Just [([], EndOfStream)])+ [ ifs_ (== '-') $ changeState ScriptDataEscapedState *> emit' ([], Character '-')+ , elsePush_ $ return []+ ]++-- | __HTML:__+-- @[script data escaped state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-state)@+-- +-- The parsing instructions rooted in the 'ScriptDataEscapedState' section of+-- the state machine.+tokenScriptDataEscaped :: Tokenizer [TokenizerOutput Token]+tokenScriptDataEscaped = tokenizers (Just [([EOFInScriptHtmlCommentLikeText], EndOfStream)])+ [ ifs_ (== '-') $ consEmit ([], Character '-') tokenScriptDataEscapedDash+ , ifs_ (== '<') tokenScriptDataEscapedLessThanSign+ , ifs_ (== '\NUL') $ emit' ([UnexpectedNullCharacter], Character replacementChar)+ , elsesChar $ \c -> emit' ([], Character c)+ ]++-- | __HTML:__+-- @[script data escaped dash state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-state)@+-- +-- The parsing instructions for after reading @"-"@ in the+-- 'ScriptDataEscapedState' section of the state machine.+tokenScriptDataEscapedDash :: Tokenizer [TokenizerOutput Token]+tokenScriptDataEscapedDash = tokenizers (Just [([EOFInScriptHtmlCommentLikeText], EndOfStream)])+ [ ifs_ (== '-') $ consEmit ([], Character '-') tokenScriptDataEscapedDashDash+ , ifPush_ (== '<') $ return []+ , ifPush_ (== '\NUL') $ return []+ , elsePush_ $ return []+ ]++-- | __HTML:__+-- @[script data escaped dash dash state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-dash-state)@+-- +-- The parsing instructions for after reading @"--"@ in the+-- 'ScriptDataEscapedState' section of the state machine.+tokenScriptDataEscapedDashDash :: Tokenizer [TokenizerOutput Token]+tokenScriptDataEscapedDashDash = tokenizers (Just [([EOFInScriptHtmlCommentLikeText], EndOfStream)])+ [ ifs_ (== '-') $ consEmit ([], Character '-') tokenScriptDataEscapedDashDash+ , ifPush_ (== '<') $ return []+ , ifs_ (== '>') $ changeState ScriptDataState *> emit' ([], Character '>')+ , ifPush_ (== '\NUL') $ return []+ , elsePush_ $ return []+ ]++-- | __HTML:__+-- @[script data escaped less-than state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-less-than-state)@+-- +-- The parsing instructions for after reading @"\<"@ in the+-- 'ScriptDataEscapedState' section of the state machine.+tokenScriptDataEscapedLessThanSign :: Tokenizer [TokenizerOutput Token]+tokenScriptDataEscapedLessThanSign = tokenizers (Just [([], Character '<')])+ [ ifs_ (== '/') tokenAppropriateEndTagOpen+ , ifPush_ isAsciiAlpha $ consEmit ([], Character '<') tokenScriptDataDoubleEscapeStart+ , elsePush_ $ emit' ([], Character '<')+ ]
+ src/Web/Mangrove/Parse/Tokenize/Tag.hs view
@@ -0,0 +1,505 @@+{-|+Description: Tokenization rules for characters comprising a markup element.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tokenize.Tag+ ( tokenTagOpen+ , tokenBeforeAttributeName+ , tokenSelfClosingStartTag+ , tokenAppropriateEndTagLessThanSign+ , tokenAppropriateEndTagOpen+ -- * Types+ , TagParams' ( .. )+ , emptyTagData+ , packTagToken+ ) where+++import qualified Control.Applicative as A+import qualified Control.Monad.Trans.State as N.S++import qualified Data.Bifunctor as F.B+import qualified Data.List as L+import qualified Data.HashMap.Strict as M+import qualified Data.Maybe as Y+import qualified Data.Text as T++import Data.Functor ( ($>) )++import Web.Willow.DOM++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tokenize.Character+import Web.Mangrove.Parse.Tokenize.Comment+import Web.Mangrove.Parse.Tokenize.Doctype+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Parser.Util++import {-# SOURCE #-} Web.Mangrove.Parse.Tokenize.Dispatcher+++-- | An intermediate form of 'TagParams' with unpacked parameter types to+-- facilitate construction. Specifically, 'Char'-by-'Char' construction of a+-- 'String' followed by a single 'T.pack' is much faster than repeated @O(n)@+-- calls to 'T.cons'. Values may be easily instantiated as updates to+-- @emptyTagData@.+data TagParams' = TagParams'+ { tagName' :: String+ -- ^ See 'tagName'.+ , tagIsSelfClosing' :: Bool+ -- ^ See 'tagIsSelfClosing'.+ , tagAttributes' :: [(String, String)]+ -- ^ See 'tagAttributes'.+ }+ deriving ( Eq, Show, Read )++-- | A sane default collection for easy record initialization; namely,+-- 'null's and 'False'.+emptyTagData :: TagParams'+emptyTagData = TagParams'+ { tagName' = ""+ , tagIsSelfClosing' = False+ , tagAttributes' = []+ }+++-- | Wrap a given collection of data into the payload of a 'StartTag' or+-- 'EndTag', according to the given constructor.+packTagToken+ :: (TagParams -> Token)+ -> TokenizerOutput (Maybe TagParams')+ -> Tokenizer [TokenizerOutput Token]+packTagToken toToken d' = case tokenizedOut d' of+ Just tagData -> do+ let tok = toToken $ emptyTagParams+ { tagName = T.pack $ tagName' tagData+ , tagIsSelfClosing = tagIsSelfClosing' tagData+ , tagAttributes = attrs+ }+ (attrErrs, attrs) = packAttributes $ tagAttributes' tagData+ tok' <- emit (tokenizedErrs d' ++ attrErrs, tok)+ return [tok' { tokenizedState = tokenizedState d' }]+ Nothing -> case tokenizedErrs d' of+ [] -> return []+ errs -> do+ atEOS <- atEndOfStream <$> N.S.get+ consTokenErrorsList errs <$> if atEOS+ then Y.fromMaybe [] <$> A.optional dispatcher+ else dispatcher++-- | Wrap a loose collection of unpacked 'String's into a form suitable for+-- representing the 'tagAttributes' of a markup element, removing all but the+-- first occurrence of any single name.+-- +-- Note that any nameless attributes at the head of the list are dropped, to+-- work around the edge case where @newAttr@ is called immediately following+-- the tag name; this shouldn't cause any issue as the specification doesn't+-- seem to output any such attributes in the first place.+packAttributes+ :: [(String, String)]+ -> ([ParseError], M.HashMap T.Text T.Text)+packAttributes = foldr filterAttr ([], M.empty) . L.sortOn fst . dropWhile (null . fst)++-- | Add a single attribute to the processed collection, potentially throwing a+-- 'DuplicateAttribute' warning (with the old value) if one with the same name+-- already exists.+filterAttr+ :: (String, String)+ -> ([ParseError], M.HashMap T.Text T.Text)+ -> ([ParseError], M.HashMap T.Text T.Text)+filterAttr (n, v) (errs, as)+ | M.null as = (errs, M.singleton name value)+ | otherwise = case M.lookup name as of+ Nothing -> (errs, M.insert name value as)+ Just value' -> (DuplicateAttribute (name, value') : errs, M.insert name value as)+ where name = T.pack n+ value = T.pack v+++-- | Finalize the currently "active" attribute, so that any further calls to,+-- e.g., @consAttrValue@ will be applied to a different (initially empty) pair+-- instead.+newAttr :: TokenizerOutput (Maybe TagParams') -> TokenizerOutput (Maybe TagParams')+newAttr = fmap . fmap $ \d -> d+ { tagAttributes' = case tagAttributes' d of+ [] -> []+ as@(("", ""):_) -> as+ as -> ("", "") : as+ }++-- | Prepend a character to the unpacked key of the currently "active" metadata+-- pair in a markup element.+consAttrName :: Char -> TokenizerOutput (Maybe TagParams') -> TokenizerOutput (Maybe TagParams')+consAttrName c = fmap . fmap $ \d -> d+ { tagAttributes' = consAttrName' $ tagAttributes' d+ }+ where consAttrName' [] = [([c], "")]+ consAttrName' (a:as) = F.B.first (c :) a : as++-- | Prepend a character to the unpacked value of the currently "active"+-- metadata pair in a markup element.+consAttrValue :: Char -> TokenizerOutput (Maybe TagParams') -> TokenizerOutput (Maybe TagParams')+consAttrValue c = consAttrValueString [c]++-- | Prepend a character sequence to the unpacked value of the currently+-- "active" metadata pair in a markup element.+consAttrValueString :: String -> TokenizerOutput (Maybe TagParams') -> TokenizerOutput (Maybe TagParams')+consAttrValueString str = fmap . fmap $ \d -> d+ { tagAttributes' = consAttrValueString' $ tagAttributes' d+ }+ where consAttrValueString' [] = [("", str)]+ consAttrValueString' (a:as) = fmap (str ++) a : as+++-- | __HTML:__+-- @[tag open state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state)@+-- +-- The parsing instructions for after reading @"\<"@ in a section of the state+-- machine which allows markup declarations.+tokenTagOpen :: Tokenizer [TokenizerOutput Token]+tokenTagOpen = tokenizers (Just [([EOFBeforeTagName], Character '<')])+ [ ifs_ (== '!') tokenMarkupDeclarationOpen+ , ifs_ (== '/') tokenEndTagOpen+ , ifPush_ isAsciiAlpha $ tokenTagName >>= packTagToken StartTag+ , ifPush_ (== '?') $ (: []) . packCommentToken .+ consTokenError UnexpectedQuestionMarkInsteadOfTagName <$>+ tokenBogusComment+ , elsePush_ $ changeState DataState *>+ emit' ([InvalidFirstCharacterOfTagName], Character '<')+ ]++-- | __HTML:__+-- @[end tag open state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state)@+-- +-- The parsing instructions for after reading @"\</"@ in a section of the state+-- machine which allows markup declarations.+tokenEndTagOpen :: Tokenizer [TokenizerOutput Token]+tokenEndTagOpen = tokenizers (Just recovery)+ [ ifPush_ isAsciiAlpha $ tokenTagName >>= packTagToken EndTag+ , ifs_ (== '>') $ do+ changeState DataState+ consTokenErrorsList [MissingEndTagName] <$> dispatcher+ , elsePush_ $ (: []) . packCommentToken . consTokenError InvalidFirstCharacterOfTagName <$>+ tokenBogusComment+ ]+ where recovery =+ [ ([], Character '<')+ , ([], Character '/')+ , ([EOFBeforeTagName], EndOfStream)+ ]++-- | __HTML:__+-- @[tag name state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#tag-name-state)@+-- +-- The parsing instructions for after reading @"\<"@ followed by a letter in a+-- section of the state machine which allows markup declarations.+tokenTagName :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenTagName = tokenizer (Just ([EOFInTag], Nothing))+ [ if_ isAsciiWhitespace tokenBeforeAttributeName+ , if_ (== '/') tokenSelfClosingStartTag+ , if_ (== '>') $ packToken ([], Just emptyTagData)+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consTagName replacementChar <$>+ tokenTagName+ , elseChar $ \c -> consTagName (toAsciiLower c) <$> tokenTagName+ ]+ where consTagName char = fmap . fmap $ \d -> d+ { tagName' = char : tagName' d+ }++-- | __HTML:__+-- @[before attribute name state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state)@+-- +-- The parsing instructions for reading whitespace interspersed with key-value+-- metadata pairs in the markup tag section of the state machine.+tokenBeforeAttributeName :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenBeforeAttributeName = tokenizer (Just ([EOFInTag], Nothing))+ [ if_ isAsciiWhitespace tokenBeforeAttributeName+ , ifPush_ (`elem` "/>") tokenAfterAttributeName+ , ifChar (== '=') $ \c -> consTokenError UnexpectedEqualsSignBeforeAttributeName . consAttrName c <$>+ tokenAttributeName+ , elsePush_ tokenAttributeName+ ]++-- | __HTML:__+-- @[attribute name state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state)@+-- +-- The parsing instructions for reading the initial (key) part of a key-value+-- metadata pair in the markup tag section of the state machine.+tokenAttributeName :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenAttributeName = tokenizer (Just ([EOFInTag], Nothing))+ [ ifPush_ (`elem` "\t\n\f />") tokenAfterAttributeName+ , if_ (== '=') tokenBeforeAttributeValue+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consAttrName replacementChar <$>+ tokenAttributeName+ , ifChar (`elem` "\"'<") $ \c -> consTokenError UnexpectedCharacterInAttributeName . consAttrName c <$>+ tokenAttributeName+ , elseChar $ \c -> consAttrName (toAsciiLower c) <$> tokenAttributeName+ ]++-- | __HTML:__+-- @[after attribute name state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-name-state)@+-- +-- The parsing instructions for reading whitespace after the initial (key) part+-- of a key-value metadata pair in the markup tag section of the state machine.+tokenAfterAttributeName :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenAfterAttributeName = tokenizer (Just ([EOFInTag], Nothing))+ [ if_ isAsciiWhitespace $ fmap newAttr tokenAfterAttributeName+ , if_ (== '/') $ fmap newAttr tokenSelfClosingStartTag+ , if_ (== '=') tokenBeforeAttributeValue+ , if_ (== '>') $ changeState DataState *> packToken ([], Just emptyTagData)+ , elsePush_ $ fmap newAttr tokenAttributeName+ ]++-- | __HTML:__+-- @[before attribute value state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-value-state)@+-- +-- The parsing instructions for reading whitespace after @"="@ following the+-- initial (key) part of a key-value metadata pair in the markup tag section of+-- the state machine.+tokenBeforeAttributeValue :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenBeforeAttributeValue = tokenizer (Just ([EOFInTag], Nothing))+ [ if_ isAsciiWhitespace tokenBeforeAttributeValue+ , if_ (== '"') tokenAttributeValueDoubleQuoted+ , if_ (== '\'') tokenAttributeValueSingleQuoted+ , if_ (== '>') $ changeState DataState *>+ packToken ([MissingAttributeValue], Just emptyTagData)+ , elsePush_ tokenAttributeValueUnquoted+ ]++-- | __HTML:__+-- @[attribute value (double-quoted) state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted\)-state)@+-- +-- The parsing instructions for reading the second (value) part of a key-value+-- metadata pair following a @'"'@ in the markup tag section of the state+-- machine.+tokenAttributeValueDoubleQuoted :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenAttributeValueDoubleQuoted = tokenizer (Just ([EOFInTag], Nothing))+ [ if_ (== '"') tokenAfterAttributeValueQuoted+ , if_ (== '&') $ tokenAttributeCharacterReference tokenAttributeValueDoubleQuoted+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consAttrValue replacementChar <$>+ tokenAttributeValueDoubleQuoted+ , elseChar $ \c -> consAttrValue c <$> tokenAttributeValueDoubleQuoted+ ]++-- | __HTML:__+-- @[attribute value (single-quoted) state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(single-quoted\)-state)@+-- +-- The parsing instructions for reading the second (value) part of a key-value+-- metadata pair following a @'\''@ in the markup tag section of the state+-- machine.+tokenAttributeValueSingleQuoted :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenAttributeValueSingleQuoted = tokenizer (Just ([EOFInTag], Nothing))+ [ if_ (== '\'') tokenAfterAttributeValueQuoted+ , if_ (== '&') $ tokenAttributeCharacterReference tokenAttributeValueSingleQuoted+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consAttrValue replacementChar <$>+ tokenAttributeValueSingleQuoted+ , elseChar $ \c -> consAttrValue c <$> tokenAttributeValueSingleQuoted+ ]++-- | __HTML:__+-- @[attribute value (unquoted) state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(unquoted\)-state)@+-- +-- The parsing instructions for reading the second (value) part of a key-value+-- metadata pair directly following a @'='@ in the markup tag section of the+-- state machine.+tokenAttributeValueUnquoted :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenAttributeValueUnquoted = tokenizer (Just ([EOFInTag], Nothing))+ [ if_ isAsciiWhitespace $ fmap newAttr tokenBeforeAttributeName+ , if_ (== '&') $ tokenAttributeCharacterReference tokenAttributeValueUnquoted+ , if_ (== '>') $ changeState DataState *> packToken ([], Just emptyTagData)+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consAttrValue replacementChar <$>+ tokenAttributeValueUnquoted+ , ifChar (`elem` "\"'<=`") $ \c ->+ consTokenError UnexpectedCharacterInUnquotedAttributeValue . consAttrValue c <$>+ tokenAttributeValueUnquoted+ , elseChar $ \c -> consAttrValue c <$> tokenAttributeValueUnquoted+ ]++-- | Resolve a character reference occurring within the second (value) part of+-- a key-value metadata pair, and continue parsing using the given tokenizer.+tokenAttributeCharacterReference+ :: Tokenizer (TokenizerOutput (Maybe TagParams'))+ -> Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenAttributeCharacterReference resume = do+ ref <- tokenCharacterReference True+ tag <- resume+ return . consTokenErrors (tokenizedErrs ref) $ consAttrValueString (tokenizedOut ref) tag++-- | __HTML:__+-- @[after attribute value (quoted) state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-value-(quoted\)-state)@+-- +-- The parsing instructions for after reading a @'"'@ or @'\''@ matching the+-- opening mark of the second (value) part of a key-value metadata pair in the+-- markup tag section of the state machine.+tokenAfterAttributeValueQuoted :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenAfterAttributeValueQuoted = tokenizer (Just ([EOFInTag], Nothing))+ [ if_ isAsciiWhitespace $ fmap newAttr tokenBeforeAttributeName+ , if_ (== '/') tokenSelfClosingStartTag+ , if_ (== '>') $ changeState DataState *> packToken ([], Just emptyTagData)+ , elsePush_ $ consTokenError MissingWhitespaceBetweenAttributes . newAttr <$>+ tokenBeforeAttributeName+ ]++-- | __HTML:__+-- @[self closing start tag state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#self-closing-start-tag-state)@+-- +-- The parsing instructions for after reading @"/"@ in the markup tag section+-- of the state machine.+tokenSelfClosingStartTag :: Tokenizer (TokenizerOutput (Maybe TagParams'))+tokenSelfClosingStartTag = tokenizer (Just ([EOFInTag], Nothing))+ [ if_ (== '>') $ changeState DataState *> packToken ([], Just $ emptyTagData+ { tagIsSelfClosing' = True+ })+ , elsePush_ $ consTokenError UnexpectedSolidusInTag <$> tokenBeforeAttributeName+ ]++-- | __HTML:__+-- @[bogus comment state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#bogus-comment-state)@+-- +-- The parsing instructions for after reading @"\<?"@ in a section of the state+-- machine which allows markup declarations.+tokenBogusComment :: Tokenizer (TokenizerOutput String)+tokenBogusComment = tokenizer (Just ([], ""))+ [ if_ (== '>') $ changeState DataState *> packToken ([], "")+ , if_ (== '\NUL') $ consTokenError UnexpectedNullCharacter . consOut replacementChar <$>+ tokenBogusComment+ , elseChar $ \c -> consOut c <$> tokenBogusComment+ ]++-- | __HTML:__+-- @[markup declaration open state]+-- (https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state)@+-- +-- The parsing instructions for after reading @"\<!"@ in a section of the state+-- machine which allows markup declarations.+tokenMarkupDeclarationOpen :: Tokenizer [TokenizerOutput Token]+tokenMarkupDeclarationOpen = do+ parserChoice <- choice+ [ chunk' id "--" *> return commentParser+ , chunk' toAsciiUpper "DOCTYPE" *> return doctypeParser+ , chunk' id "[CDATA[" *> return cdataParser+ , return failureParser+ ]+ parserChoice+ where commentParser = fmap (pure . packCommentToken) tokenCommentStart+ doctypeParser = fmap pure tokenDoctype+ cdataParser = do+ currentNamespace <- currentNodeNamespace <$> N.S.get+ case currentNamespace of+ Just ns | ns /= htmlNamespace ->+ changeState CDataState $> []+ _ -> pure . packCommentToken .+ consTokenError CDataInHtmlContent . consOuts "[CDATA[" <$>+ tokenBogusComment+ failureParser = pure . packCommentToken .+ consTokenError IncorrectlyOpenedComment <$> tokenBogusComment+++-- | __HTML:__+-- +-- * [@RAWTEXT less-than sign state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#rawtext-less-than-sign-state)+-- * [@RCDATA less-than sign state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#rcdata-less-than-sign-state)+-- +-- The parsing instructions for after reading @"\<"@ in sections of the state+-- machine which may be closed by tag, and which emit only 'Character' tokens.+tokenAppropriateEndTagLessThanSign :: Tokenizer [TokenizerOutput Token]+tokenAppropriateEndTagLessThanSign = tokenizers (Just [([], Character '<')])+ [ ifs_ (== '/') tokenAppropriateEndTagOpen+ , elsePush_ $ emit' ([], Character '<')+ ]++-- | __HTML:__+-- +-- * [@RAWTEXT end tag open state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-open-state)+-- * [@RCDATA end tag open state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-open-state)+-- * [@script data end tag open state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-open-state)+-- * [@script data escaped end tag open state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-open-state)+-- +-- The parsing instructions for after reading @"\</"@ in sections of the state+-- machine which may be closed by tag, and which emit only 'Character' tokens.+tokenAppropriateEndTagOpen :: Tokenizer [TokenizerOutput Token]+tokenAppropriateEndTagOpen = tokenizers (Just [([], Character '<'), ([], Character '/')])+ [ ifPush_ isAsciiAlpha tokenAppropriateEndTag+ , elsePush_ $ mapM emit+ [ ([], Character '<')+ , ([], Character '/')+ ]+ ]++-- | __HTML:__+-- +-- * [@RAWTEXT end tag name state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state)+-- * [@RCDATA end tag name state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-name-state)+-- * [@script data end tag name state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-name-state)+-- * [@script data escaped end tag name state@]+-- (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-name-state)+-- +-- The parsing instructions for after reading @"\</"@ in sections of the state+-- machine which may be closed by tag, and which emit only 'Character' tokens.+tokenAppropriateEndTag :: Tokenizer [TokenizerOutput Token]+tokenAppropriateEndTag = tokenAppropriateEndTagName >>= \t' -> do+ close <- appropriateEndTag (map toAsciiLower $ tokenizedOut t')+ if close+ then tokenizers (Just $ fallback t')+ [ ifs_ isAsciiWhitespace $+ tokenBeforeAttributeName >>= packName (tokenizedOut t')+ , ifs_ (== '/') $+ tokenSelfClosingStartTag >>= packName (tokenizedOut t')+ , ifs_ (== '>') $ do+ changeState DataState+ out <- packName (tokenizedOut t') $ t'+ { tokenizedOut = Just emptyTagData+ }+ -- 'packName' keeps the state of the input (i.e. /before/ the+ -- closing @>@), and so that needs to be cleared so 'ifs_' will+ -- assign the state of the input token (/after/ the closing+ -- @>@).+ return $ map continueState out+ , elsePush_ $ emits (tokenizedState t') (anythingElse (tokenizedErrs t') (tokenizedOut t'))+ ]+ else emits (tokenizedState t') (fallback t')+ where fallback t' = anythingElse (tokenizedErrs t') (tokenizedOut t')+ packName buffer = packTagToken $ EndTag . packName' (T.pack buffer)+ packName' buffer tagData = tagData+ { tagName = T.map toAsciiLower buffer+ }+ anythingElse errs buffer =+ [ ([], Character '<')+ , ([], Character '/')+ ] ++ case buffer of+ [] -> []+ (c:cs) -> (errs, Character c) : [([], Character c') | c' <- cs]+ emits state ts = finalStateList state <$> mapM emit ts+ tokenAppropriateEndTagName = tokenizer (Just ([], ""))+ [ ifChar isAsciiAlpha $ \c -> consOut c <$> tokenAppropriateEndTagName+ , elsePush_ $ packToken ([], "")+ ]
+ src/Web/Mangrove/Parse/Tree.hs view
@@ -0,0 +1,288 @@+{-# HLINT ignore "Redundant flip" #-}++{-|+Description: Fold a linear, semantic stream into a tree structure.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: provisional+Portability: portable++This module and the internal branch it heads implement the "Tree Construction"+section of the+__[HTML](https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)__+document parsing specification, operating over the output of the+"Web.Mangrove.Parse.Tokenize" stage to produce a DOM tree representation of a+web page. As this library is still in the early stages of development, the+representation produced here is not actually a proper DOM implementation, but+instead only stores basic parameters in an equivalent (but less-featured)+structure. Nonetheless, it is still enough for basic evaluation and unstyled+rendering.+-}+module Web.Mangrove.Parse.Tree+ ( -- * Types+ -- ** Final+ Tree ( .. )+ , Node ( .. )+ , QuirksMode ( .. )+ -- ** Intermediate+ , Patch+ , TreeState+ , Encoding ( .. )+ , NodeIndex+ , ElementParams ( .. )+ , emptyElementParams+ -- * Initialization+ , defaultTreeState+ , treeEncoding+ , treeFragment+ , treeInIFrame+ -- * Transformations+ , tree+ , treeStep+ , finalizeTree+ ) where+++import qualified Control.Monad.Trans.State as N.S++import qualified Data.ByteString as BS+import qualified Data.ByteString.Short as BS.SH+import qualified Data.List as L+import qualified Data.Maybe as Y+import qualified Data.Text as T++import Web.Willow.DOM++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.Dispatcher+import Web.Mangrove.Parse.Tree.Patch+import Web.Mangrove.Parse.Tree.Patch.Fold+import Web.Willow.Common.Encoding hiding ( setRemainder )+import Web.Willow.Common.Encoding.Sniffer+import Web.Willow.Common.Parser+++-- | __HTML:__+-- @[tree construction]+-- (https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)@+-- +-- Given a starting environment, transform a binary document stream into a+-- hierarchical markup tree. If the parse fails, returns an empty tree (a+-- 'Document' node with no children).+tree :: TreeState -> BS.ByteString -> ([Patch], TreeState)+tree state stream = L.foldl' treeFold ([], state) $ L.unfoldr treeUnfold (state, stream)+ where treeUnfold = unfoldLoop treeStep tokenizerState+ treeFold (ps, _) (ps', state', _) = (ps ++ ps', state')++-- | Wrap a parser in a signature appropriate to pass to 'L.unfoldr'.+unfoldLoop+ :: Eq state+ => (state -> BS.ByteString -> ([out], state, BS.ByteString))+ -- ^ The single-step parser function.+ -> (state -> TokenizerState)+ -- ^ Repack the state used by the step parser into a standardized form.+ -> (state, BS.ByteString)+ -- ^ The initial inputs to the parser.+ -> Maybe (([out], state, BS.ByteString), (state, BS.ByteString))+unfoldLoop step toTokState (state, stream)+ | BS.null stream = Nothing+ | otherwise = case step state stream of+ out@(_, state', _) | state /= state' || hasRemainder state' -> continueUnfold out+ ([], _, stream') | BS.null stream' -> Nothing+ out -> continueUnfold out+ where continueUnfold (ps, state', stream') = Just ((ps, state', stream'), (state', stream'))+ hasRemainder = maybe False (not . BS.SH.null . decoderRemainder) . decoderState . toTokState++-- | Parse a minimal number of tokens from a binary document stream, into a+-- state-independent sequence of folding instructions. Returns all data+-- required to seamlessly resume parsing.+treeStep :: TreeState -> BS.ByteString -> ([Patch], TreeState, BS.ByteString)+treeStep state stream = treeStep' stream' stateRemainder state+ where stateRemainder state' = state'+ { tokenizerState = setRemainder (BS.SH.toShort stream) $ tokenizerState state'+ }+ stream' = L.unfoldr tokenUnfold (tokenizerState state, stream)+ tokenUnfold = unfoldLoop tokenizeStep id++-- | Parse a minimal number of tokens from a binary document stream, performing+-- any backend processing required to correctly generate the document tree.+treeStep'+ :: [([([ParseError], Token)], TokenizerState, BS.ByteString)]+ -> (TreeState -> TreeState)+ -> TreeState+ -> ([Patch], TreeState, BS.ByteString)+treeStep' input fallback state =+ case runParserT (N.S.runStateT recurse $ treeParserState state) stream of+ Just (((ps, tokState, stream'), parserState), _) -> (ps', state', stream')+ where state' = TreeState+ { tokenizerState = tokState+ , treeParserState = parserState+ }+ ps' = map redirectPatches ps+ Nothing -> ([], fallback state, BS.empty)+ where stream = L.foldr repackStream [] input+ redirectPatches p = case fragmentContext $ treeParserState state of+ -- A fragment with an @\<html\>@ context won't create that node in+ -- the folded tree, and so any patches sent there won't find an+ -- anchor.+ Just ctx | nodeIsElement (T.pack "html") $ fst ctx -> case p of+ InsertComment errs InDocument _ -> ErrorList errs+ InsertComment errs InHtmlElement txt -> InsertComment errs InDocument txt+ AddAttribute InDocument _ -> ErrorList []+ AddAttribute InHtmlElement attr -> AddAttribute InDocument attr+ _ -> p+ -- The fragment parsing algorithm returns the children of the+ -- context node, and so any patches destined for locations above+ -- that shouldn't show up in the resulting tree.+ Just _ -> case p of+ InsertComment errs InDocument _ -> ErrorList errs+ InsertComment errs InHtmlElement _ -> ErrorList errs+ AddAttribute InDocument _ -> ErrorList []+ AddAttribute InHtmlElement _ -> ErrorList []+ _ -> p+ _ -> p++-- | Explicitly indicate that the input stream will not contain any further+-- bytes, and perform any finalization processing based on that.+finalizeTree :: [Patch] -> TreeState -> Tree+finalizeTree ps state = buildTree $ ps ++ ps'+ where (ps', _, _) = treeStep' [(ts, setRemainder BS.SH.empty tokState, BS.empty)] id state+ tokState = tokenizerState state+ ts = finalizeTokenizer tokState ++ [([], EndOfStream)]+++-- | Specify the encoding scheme a given parse environment should use to read+-- from the binary document stream. Note that this will always use the initial+-- state for the respective decoder; intermediate states as returned by+-- 'decodeStep' are not supported.+treeEncoding :: Either SnifferEnvironment (Maybe Encoding) -> TreeState -> TreeState+treeEncoding enc state = state+ { tokenizerState = tokenizerEncoding enc $ tokenizerState state+ }++-- | __HTML:__+-- @[fragment parsing algorithm]+-- (https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm)@+-- +-- Transform a given parse environment by adding context for an embedded but+-- separate document fragment. Calling this with an intermediate state+-- returned by 'treeStep' (as opposed to an initial state from+-- 'defaultTreeState') may result in an unexpected tree structure.+treeFragment+ :: ElementParams+ -- ^ __HTML:__+ -- @[context element]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#concept-frag-parse-context)@+ -- + -- The node wrapping -- in one way or another -- the embedded document fragment.+ -> [(NodeIndex, ElementParams)]+ -- ^ The ancestors of the context element, most immediate first.+ -> Maybe QuirksMode+ -- ^ The degree of backwards compatibility used in the node document of+ -- the context element, if it can be determined.+ -> Maybe Bool+ -- ^ Whether the node document of the context element has been parsed+ -- in a way which would require scripting to be enabled (@'Just'+ -- 'True'@) or disabled (@'Just' 'False'@).+ -> TreeState+ -> TreeState+treeFragment ctxNode ctxTree ctxQuirks ctxScript state = state+ { tokenizerState =+ let mode = if elementNamespace ctxNode == Just htmlNamespace+ then case T.unpack $ elementName ctxNode of+ "title" -> RCDataState+ "textarea" -> RCDataState+ "style" -> RawTextState+ "xmp" -> RawTextState+ "iframe" -> RawTextState+ "noembed" -> RawTextState+ "noframes" -> RawTextState+ "script" -> ScriptDataState+ "noscript" -> if ctxScript == Just True+ then RawTextState+ else DataState+ "plaintext" -> PlainTextState+ _ -> DataState+ else DataState+ in tokenizerStartTag (Just htmlNamespace) (T.pack "html") .+ tokenizerMode mode $ tokenizerState state+ , treeParserState = resetInsertionMode' $ (treeParserState state)+ { openElements = [(0, htmlElement)]+ , elementIndex = ctxIndex + 1+ , quirksMode = Y.fromMaybe NoQuirks ctxQuirks+ , templateInsertionModes = if nodeIsElement (T.pack "template") ctxNode+ then [InTemplate]+ else []+ , fragmentContext = Just (ctxNode, ctxTree)+ , scriptingEnabled = Y.fromMaybe False ctxScript+ , formElementPointer = fmap fst . L.find (nodeIsElement (T.pack "form") . snd) $+ (ctxIndex, ctxNode) : ctxTree+ }+ }+ where ctxIndex = foldr (max . fst) 0 ctxTree + 1+ htmlElement = emptyElementParams+ { elementName = T.pack "html"+ , elementNamespace = Just htmlNamespace+ }++-- | Specify whether the given parse environment should be treated as if the+-- document were contained within the @srcdoc@ attribute of an @\<iframe\>@+-- element ('False' by default).+treeInIFrame :: Bool -> TreeState -> TreeState+treeInIFrame b state = state+ { treeParserState = (treeParserState state)+ { isInIFrameSrcDoc = b+ }+ }+++-- | Given the output of 'tokenizeStep', rewrap the token list and single state+-- into a single uniform stream. The final returned tuple will have a 'Just'+-- value containing the single state, while every other element has 'Nothing'+-- (indicating a point where reentry is impossible).+repackStream+ :: ([([ParseError], Token)], TokenizerState, BS.ByteString)+ -> [TreeInput]+ -> [TreeInput]+repackStream ([], _, _) is = is+repackStream (ts, state, stream) is = case reverse ts of+ -- Be sure any 'EndOfStream' tokens encountered mid-stream don't break parsing.+ ((errs, EndOfStream):ts') -> case is of+ [] -> repackStream' ts+ (i:is') -> repackStream' (reverse ts') ++ consErrors errs i : is'+ -- Any other token ending the segment, however, can simply be repacked.+ _ -> repackStream' ts ++ is+ where repackStream' = flip foldr [] $ \(errs, t) ts' -> TreeInput+ { tokenErrs = errs+ , tokenOut = t+ , tokenState = if null ts'+ then Just (state, stream)+ else Nothing+ } : ts'+ consErrors errs i = i+ { tokenErrs = errs ++ tokenErrs i+ }+++-- | Loop the tree dispatcher until it returns a patchset which happens to+-- coincide with a tokenizer breakpoint. Relies on lazy evaluation in the+-- stream generation to avoid forcing the entire thing at once, while still+-- retaining the capability to consume as much input as necessary to get the+-- parsers to line up.+recurse :: TreeBuilder ([Patch], TokenizerState, BS.ByteString)+recurse = do+ out <- dispatcher+ case treeState out of+ -- The tokenizer can't provide a breakpoint at the current token.+ Nothing -> do+ (out', tokState', stream') <- recurse+ return (treePatches out ++ out', tokState', stream')+ -- We have a re-entrant state to seamlessly resume the tokenizer.+ Just (tokState, stream) ->+ return (treePatches out, tokState, stream)
+ src/Web/Mangrove/Parse/Tree/AfterAfterBody.hs view
@@ -0,0 +1,47 @@+{-|+Description: Token processing rules between a closing @\</html\>@ tag containing standard body content.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.AfterAfterBody+ ( treeAfterAfterBody+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "after after body" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#the-after-after-body-insertion-mode)@+-- +-- The parsing instructions corresponding to the 'AfterAfterBody' section of the+-- state machine.+treeAfterAfterBody :: TreeBuilder TreeOutput+treeAfterAfterBody = next >>= switch+ [ If isComment $ insertComment' InDocument+ , If isDoctype $ \t' -> do+ push t'+ treeInBody+ , If isWhitespace $ \t' -> do+ push t'+ treeInBody+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If isEOF stopParsing+ , Else $ \t' -> do+ push t'+ switchMode InBody+ packTreeErrors_ [UnexpectedContentAfterBody]+ ]
+ src/Web/Mangrove/Parse/Tree/AfterAfterFrameset.hs view
@@ -0,0 +1,40 @@+{-|+Description: Token processing rules between a closing @\</html\>@ tag containing frames of embedded external content.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.AfterAfterFrameset+ ( treeAfterAfterFrameset+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "after after frameset" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#the-after-after-frameset-insertion-mode)@+-- +-- The parsing instructions corresponding to the 'AfterAfterFrameset' section+-- of the state machine.+treeAfterAfterFrameset :: TreeBuilder TreeOutput+treeAfterAfterFrameset = next >>= switch+ [ If isComment $ insertComment' InDocument+ , If isDoctype $ \t' -> push t' *> treeInBody+ , If isWhitespace $ \t' -> push t' *> treeInBody+ , If (isStartTag ["html"]) $ \t' -> push t' *> treeInBody+ , If isEOF stopParsing+ , If (isStartTag ["noframes"]) $ \t' -> push t' *> treeInHead+ , Else $ packTreeErrors [UnexpectedNodeAfterFrameset]+ ]
+ src/Web/Mangrove/Parse/Tree/AfterBody.hs view
@@ -0,0 +1,54 @@+{-|+Description: Token processing rules between the closing @\</body\>@ and @\</html\>@ tags.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.AfterBody+ ( treeAfterBody+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "after body" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-afterbody)@+-- +-- The parsing instructions corresponding to the 'AfterBody' section of the+-- state machine.+treeAfterBody :: TreeBuilder TreeOutput+treeAfterBody = next >>= switch+ [ If isWhitespace $ \t' -> do+ push t'+ treeInBody+ , If isComment $ insertComment' InHtmlElement+ -- Even document fragments will always have an @\<html\>@ element as+ -- the root, so this 'InsertAt' will always target "the first element+ -- in the stack of open elements".+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If (isEndTag ["html"]) $ \t' -> do+ isFragment <- inFragment+ if isFragment+ then packTreeErrors [AbruptTemplateFragment] t'+ else switchMode AfterAfterBody *> packTreeErrors [] t'+ , If isEOF stopParsing+ , Else $ \t' -> do+ push t'+ switchMode InBody+ packTreeErrors_ [UnexpectedContentAfterBody]+ ]
+ src/Web/Mangrove/Parse/Tree/AfterFrameset.hs view
@@ -0,0 +1,46 @@+{-|+Description: Token processing rules between the closing @\</frameset\>@ and @\</html\>@ tags.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.AfterFrameset+ ( treeAfterFrameset+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "after frameset" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-afterframeset)@+-- +-- The parsing instructions corresponding to the 'AfterFrameset' section of the+-- state machine.+treeAfterFrameset :: TreeBuilder TreeOutput+treeAfterFrameset = next >>= switch+ [ If isWhitespace insertCharacter+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If (isEndTag ["html"]) $ \t' -> switchMode AfterAfterFrameset *> packTreeErrors [] t'+ , If (isStartTag ["noframes"]) $ \t' -> do+ push t'+ treeInHead+ , If isEOF stopParsing+ , Else $ packTreeErrors [UnexpectedNodeAfterFrameset]+ ]
+ src/Web/Mangrove/Parse/Tree/AfterHead.hs view
@@ -0,0 +1,85 @@+{-|+Description: Token processing rules between the @\</head\>@ and @\<body\>@ tags.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.AfterHead+ ( treeAfterHead+ ) where+++import qualified Data.Text as T++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "after head" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#the-after-head-insertion-mode)@+-- +-- The parsing instructions corresponding to the 'AfterHead' section of the+-- state machine.+treeAfterHead :: TreeBuilder TreeOutput+treeAfterHead = next >>= switch+ [ If isWhitespace insertCharacter+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If (isStartTag ["body"]) $ \t' -> do+ switchMode InBody+ setFramesetNotOk+ close <- closeCurrentNode_+ insert <- insertElement t'+ return $ close ++| insert+ , If (isStartTag ["frameset"]) $ \t' -> do+ switchMode InFrameset+ close <- closeCurrentNode_+ insert <- insertElement t'+ return $ close ++| insert+ , If (isStartTag+ [ "base"+ , "basefont"+ , "bgsound"+ , "link"+ , "meta"+ , "noframes"+ , "script"+ , "style"+ , "template"+ , "title"+ ]) $ \t' -> do+ push t'+ consTreeError UnexpectedMetadataOutsideOfHead <$> treeInHead+ , If (isEndTag ["template"]) $ \t' -> do+ push t'+ treeInHead+ , If (isEndTag ["body", "html", "br"]) anythingElse+ , If (isStartTag ["head"]) $ \t' ->+ packTreeErrors [DuplicateSingletonElement $ tokenElement t'] t'+ , If isAnyEndTag $ \t' ->+ packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , Else anythingElse+ ]+ where anythingElse t' = do+ push t'+ switchMode InBody+ close <- closeCurrentNode_+ insert <- insertElement_ $ emptyTagParams+ { tagName = T.pack "body"+ }+ packTree_ $ close ++ insert
+ src/Web/Mangrove/Parse/Tree/BeforeHead.hs view
@@ -0,0 +1,56 @@+{-|+Description: Token processing rules between the opening @\<html\>@ and @\<head\>@ tags.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.BeforeHead+ ( treeBeforeHead+ ) where+++import qualified Data.Text as T++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "before head" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#the-before-head-insertion-mode)@+-- +-- The parsing instructions corresponding to the 'BeforeHead' section of the+-- state machine.+treeBeforeHead :: TreeBuilder TreeOutput+treeBeforeHead = next >>= switch+ [ If isWhitespace $ packTreeErrors []+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If (isStartTag ["head"]) $ \t' -> do+ switchMode InHead+ insertHeadElement t'+ , If (isEndTag ["head", "body", "html", "br"]) anythingElse+ , If isAnyEndTag $ \t' ->+ packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , Else anythingElse+ ]+ where anythingElse t' = do+ push t'+ switchMode InHead+ ps <- insertHeadElement_ $ emptyTagParams+ { tagName = T.pack "head"+ }+ packTree_ ps
+ src/Web/Mangrove/Parse/Tree/BeforeHtml.hs view
@@ -0,0 +1,52 @@+{-|+Description: Token processing rules before the root element is opened.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.BeforeHtml+ ( treeBeforeHtml+ ) where+++import qualified Data.Text as T++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "before html" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#the-before-html-insertion-mode)@+-- +-- The parsing instructions corresponding to the 'BeforeHtml' section of the+-- state machine.+treeBeforeHtml :: TreeBuilder TreeOutput+treeBeforeHtml = next >>= switch+ [ If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If isComment $ insertComment' InDocument+ , If isWhitespace $ packTreeErrors []+ , If (isStartTag ["html"]) $ \t' -> do+ switchMode BeforeHead+ insertElement t'+ , If (isEndTag ["head", "body", "html", "br"]) anythingElse+ , If isAnyEndTag $ \t' ->+ packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , Else anythingElse+ ]+ where anythingElse t' = do+ push t'+ switchMode BeforeHead+ insert <- insertElement_ emptyTagParams+ { tagName = T.pack "html"+ }+ packTree_ insert
+ src/Web/Mangrove/Parse/Tree/Common.hs view
@@ -0,0 +1,1258 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Functions and objects used to build the tree construction parser.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: provisional+Portability: portable++This module provides the data structures and common functions used in the+first half of this implementation's split+__[HTML](https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)__+tree construction algorithm; for more discussion on the structure, see the+documentation on "Web.Mangrove.Parse.Tree.Patch". In particular, these objects+handle the state transitions and token processing which form the foundation of+that parser on which the 'Web.Mangrove.Parse.Tree.Patch.Patch' generation will+be able to build.+-}+module Web.Mangrove.Parse.Tree.Common+ ( -- * Types+ -- ** Tree construction+ -- *** Parser+ TreeBuilder+ , TreeState ( .. )+ , TreeParserState ( .. )+ , defaultTreeState+ , NodeIndex+ , InsertionMode ( .. )+ -- *** Data+ -- **** Elements+ , nodeIsElement+ , nodeIsSpecial+ , ElementParams ( .. )+ , emptyElementParams+ , packNodeData+ , scopeElements+ , specialElements+ -- **** Attributes+ , AttributeParams ( .. )+ , emptyAttributeParams+ , adjustForeignAttributes+ , adjustMathMLAttributes+ , adjustSvgAttributes+ -- **** Document type declarations+ , DocumentTypeParams ( .. )+ , emptyDocumentTypeParams+ , QuirksMode ( .. )+ -- ** Tokenization+ -- *** Parser+ , TokenizerState ( .. )+ , CurrentTokenizerState ( .. )+ -- *** Data+ , Token ( .. )+ , TreeInput ( .. )+ , TokenizerOutputState+ , tokenRemainder+ , dummyToken+ , dummyStateToken+ , mapTokenState+ , mapTokenState'+ , TagParams ( .. )+ , emptyTagParams+ -- *** Extraction+ , tokenCharacter+ , tokenDoctype+ , tokenDocumentType+ , tokenTag+ , tokenElement+ -- * Parser building+ -- ** Token matching+ , isEOF+ , isCharacter+ , isNull+ , isWhitespace+ , isComment+ , isDoctype+ , isAnyStartTag+ , isAnyEndTag+ , isStartTag+ , isEndTag+ -- ** State modification+ , switchMode+ , resetMode+ , setFramesetNotOk+ , insertFormattingMarker+ , clearFormattingElements+ , pushTemplateMode+ , popTemplateMode+ , resetInsertionMode+ , resetInsertionMode'+ -- ** Current node+ -- *** Retrieval+ , currentNode+ , currentNodeIndex+ , adjustedCurrentNode+ -- *** Namespace boundaries+ , atHtmlIntegration+ , atMathMLIntegration+ , isMathMLAnnotationXml+ -- ** Ancestor testing+ , inFragment+ , inIFrameSrcDoc+ , hasOpenElement+ , hasOpenElementExcept+ -- *** Scope+ , hasInScope+ , hasIndexInScope+ , hasInButtonScope+ , hasInListItemScope+ , hasInTableScope+ , hasInSelectScope+ ) where+++import qualified Control.Monad.Trans.State as N.S++import qualified Data.Bifunctor as F.B+import qualified Data.ByteString as BS+import qualified Data.HashMap.Strict as M+import qualified Data.Maybe as Y+import qualified Data.Text as T++import qualified Numeric.Natural as Z++import Web.Willow.DOM hiding+ ( Tree ( .. )+ , Node ( .. )+ )++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Parser+++-- | Parser combinators written over the output of the+-- "Web.Mangrove.Parse.Tokenize" stage, transforming it into a sequence of+-- folding instruction 'Web.Mangrove.Parse.Tree.Patch.Patch'es.+type TreeBuilder out = StateParser TreeParserState [TreeInput] out++-- | The collection of data returned by the "Web.Mangrove.Parse.Tokenize"+-- stage, and so comprising the input to the tree construction parser.+-- Values may be easily instantiated through 'dummyToken' or dummyStateToken'.+data TreeInput = TreeInput+ { tokenErrs :: [ParseError]+ -- ^ Any authoring errors detected during decoding and tokenization.+ , tokenOut :: Token+ -- ^ The token itself.+ , tokenState :: TokenizerOutputState+ -- ^ The data required to resume decoding immediately following the+ -- value, if possible. See also 'decodedRemainder'.+ }+ deriving ( Eq, Show, Read )++-- | The unparsed portion of the binary stream, /after/ parsing the associated+-- token. See also 'tokenState'.+tokenRemainder :: TreeInput -> Maybe BS.ByteString+tokenRemainder = fmap snd . tokenState++-- | All data with which to re-initialize the tokenizer, to resume as if the+-- state machine transition hadn't been interrupted. If 'Nothing', the+-- associated 'Token' was emitted in the 'init' of several at once; in this+-- case, the tokenizer can't be re-entered in exactly the same place with the+-- data wrapped in this type, and so the stream must continue to be processed+-- until the first 'Just' value.+type TokenizerOutputState = Maybe (TokenizerState, BS.ByteString)+++-- | Generate a token /a priori/ in the tree construction stage to pass to a+-- function expecting the raw tokenizer output. See 'dummyStateToken' if a+-- known 'TokenizerOutputState' must be associated as well.+dummyToken :: [ParseError] -> Token -> TreeInput+dummyToken errs t = dummyStateToken errs t Nothing++-- | Generate a token /a priori/ in the tree construction stage to pass to a+-- function expecting the raw tokenizer output, attaching a specific resume+-- state. For most of the standard parsers, 'dummyToken' should be used+-- instead.+dummyStateToken :: [ParseError] -> Token -> TokenizerOutputState -> TreeInput+dummyStateToken errs t state = TreeInput+ { tokenErrs = errs+ , tokenOut = t+ , tokenState = state+ }++-- | Extract a single 'Char' from an input value. Returns Nothing if the inner+-- 'Token' is not a 'Character'.+tokenCharacter :: TreeInput -> Maybe Char+tokenCharacter t' = case tokenOut t' of+ Character c -> Just c+ _ -> Nothing++-- | Extract the data comprising a DOCTYPE declaration from an input value.+-- Returns an empty collection if the inner 'Token' is not a 'Doctype'.+tokenDoctype :: TreeInput -> DoctypeParams+tokenDoctype t' = case tokenOut t' of+ Doctype d -> d+ _ -> emptyDoctypeParams++-- | Extract the data comprising a DOCTYPE declaration from an input value, in+-- a form suitable for 'ParseError's. Returns an empty collection if the inner+-- 'Token' is not a 'Doctype'.+tokenDocumentType :: TreeInput -> DocumentTypeParams+tokenDocumentType t' = emptyDocumentTypeParams+ { documentTypeName = Y.fromMaybe T.empty $ doctypeName d+ , documentTypePublicId = Y.fromMaybe T.empty $ doctypePublicId d+ , documentTypeSystemId = Y.fromMaybe T.empty $ doctypeSystemId d+ }+ where d = tokenDoctype t'++-- | Extract the data comprising a markup element from an input value. Returns+-- 'emptyTagParams' if the inner 'Token' is neither a 'StartTag' nor an+-- 'EndTag'.+tokenTag :: TreeInput -> TagParams+tokenTag t' = case tokenOut t' of+ StartTag d -> d+ EndTag d -> d+ _ -> emptyTagParams++-- | Extract the data comprising a markup element from an input value, in a+-- form suitable for 'ParseError's. Returns an empty collection in the+-- 'htmlNamespace' if the inner 'Token' is neither a 'StartTag' nor an+-- 'EndTag'.+tokenElement :: TreeInput -> ElementParams+tokenElement = packNodeData (Just htmlNamespace) . tokenTag+++-- | Modify the state of the tokenizer as given by one of the wrapped tokens+-- used as input to the tree construction parsers.+mapTokenState :: TreeInput -> (TokenParserState -> TokenParserState) -> TreeInput+mapTokenState t' f = t'+ { tokenState = F.B.first f' <$> tokenState t'+ }+ where f' state = state+ { tokenParserState = f $ tokenParserState state+ }++-- | Modify the semi-opaque wrapped state of the tokenizer, within one of the+-- wrapped tokens used as input to the tree construction parsers.+mapTokenState' :: TreeInput -> (TokenizerOutputState -> TokenizerOutputState) -> TreeInput+mapTokenState' t' f = t'+ { tokenState = f $ tokenState t'+ }+++-- | Lift the tag data collected by the tokenizer into the namespaced datatype+-- of the DOM tree.+packNodeData :: Maybe Namespace -> TagParams -> ElementParams+packNodeData ns d = emptyElementParams+ { elementName = tagName d+ , elementNamespace = ns+ , elementAttributes = M.fromList . map packAttr . M.toList $ tagAttributes d+ }+ where packAttr (n, v) = ((Nothing, n), (Nothing, v))+++-- | Type-level clarification for an identifier uniquely identifying each+-- element in the document tree, assigned in a rough tree order.+-- +-- The specification assumes a reference-based and mutable memory model, where+-- each element in, e.g., the stack of open elements not only describes the+-- shape of the node, but also is distinct from all other nodes with the same+-- data. Haskell's memory is much less accessible and more likely to be+-- shared, so an extra datapoint needs to be carried along.+type NodeIndex = Z.Natural+++-- | The collection of data required to extract a list of semantic atoms from a+-- binary document stream. Values may be easily instantiated as updates to+-- 'defaultTreeState'.+data TreeState = TreeState+ { treeParserState :: TreeParserState+ -- ^ The state of the current 'Web.Mangrove.Parse.Tree.tree' stage.+ , tokenizerState :: TokenizerState+ -- ^ The state of the previous tokenization stage. Note that the+ -- high-level conceptual view of the parser stack is of each stage+ -- moving along the 'BS.ByteString' as a (more or less) unified+ -- front, rather than each stage independently running over the output+ -- of the previous.+ }+ deriving ( Eq, Show, Read )++-- | All the data which needs to be tracked for correct behaviour in the tree+-- construction stage.+data TreeParserState = TreeParserState+ { insertionMode :: InsertionMode+ -- ^ __HTML:__+ -- @[insertion mode]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#insertion-mode)@+ -- + -- The set of rules currently active in the state machine.+ , originalInsertionMode :: Maybe InsertionMode+ -- ^ __HTML:__+ -- @[original insertion mode]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#original-insertion-mode)@+ -- + -- Which part of the state machine should be returned to after exiting+ -- the 'InText' insertion mode.+ , templateInsertionModes :: [InsertionMode]+ -- ^ __HTML:__+ -- @[stack of template insertion modes]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#stack-of-template-insertion-modes)@+ -- + -- Which part(s) of the state machine should be returned to after+ -- exiting the 'InTemplate' insertion mode.+ , openElements :: [(NodeIndex, ElementParams)]+ -- ^ __HTML:__+ -- @[stack of open elements]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#stack-of-open-elements)@+ -- + -- The ancestors of/path to the currently "active" node, closest parent+ -- first.+ , activeFormattingElements :: [[(NodeIndex, TagParams)]]+ -- ^ __HTML:__+ -- @[list of active formatting elements]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#list-of-active-formatting-elements)@+ -- + -- The elements which should be reconstructed at the beginning of a tag+ -- closed unexpectedly early (overlapping tags). These are divided+ -- into groups at particularly independent sections of the tree.+ , elementIndex :: NodeIndex+ -- ^ The unique ID for the /next/ node which will be generated.+ , isInIFrameSrcDoc :: Bool+ -- ^ __HTML:__+ -- @[an iframe srcdoc document]+ -- (https://html.spec.whatwg.org/multipage/iframe-embed-object.html#an-iframe-srcdoc-document)@+ -- + -- Test whether the current document is being loaded via an @\<iframe\>@.+ , fragmentContext :: Maybe (ElementParams, [(NodeIndex, ElementParams)])+ -- ^ __HTML:__+ -- @[context]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#concept-frag-parse-context)@+ -- + -- The element passed to the [HTML fragment parsing algorithm]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm)+ -- to provide its immediate (conceptual) parent. The first element of+ -- the tuple is a description of the context node itself, while the+ -- second is a list of its ancestors, most recent parent first.+ , quirksMode :: QuirksMode+ -- ^ __DOM:__+ -- @[Document mode]+ -- (https://dom.spec.whatwg.org/#concept-document-mode)@+ -- + -- The degree to which the document should emulate weirdness in+ -- historic browsers' rendering.+ , fosteringEnabled :: Bool+ -- ^ __HTML:__+ -- @[foster parenting]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#foster-parent)@+ -- + -- Whether the [appropriate place for inserting a node]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#appropriate-place-for-inserting-a-node)+ -- should direct new nodes to be created outside the current @\<table\>@.+ , scriptingEnabled :: Bool+ -- ^ __HTML:__+ -- @[scripting flag]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#scripting-flag)@+ -- + -- Whether the document is required to be parsed with JavaScript+ -- enabled or disabled. Currently a binary choice, but the type may+ -- change in the future if non-deterministic parsing is implemented.+ , framesetOk :: Bool+ -- ^ __HTML:__+ -- @[frameset-ok flag]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#frameset-ok-flag)@+ -- + -- Whether a particular class of content, which disallows the+ -- introduction of HTML page inclusion via @\<frameset\>@, has already+ -- been added to the tree.+ , headElementPointer :: Maybe NodeIndex+ -- ^ __HTML:__+ -- @[head element pointer]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#head-element-pointer)@+ -- + -- The unique ID of the document's @\<head\>@ element, if one has been+ -- added to the tree.+ , formElementPointer :: Maybe NodeIndex+ -- ^ __HTML:__+ -- @[form element pointer]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#form-element-pointer)@+ -- + -- The unique ID of the most recent @\<form\>@ element, if one exists.+ }+ deriving ( Eq, Show, Read )++-- | The collection of data which results in behaviour according to the+-- "initially" instructions in the HTML tree construction algorithm.+defaultTreeState :: TreeState+defaultTreeState = TreeState+ { tokenizerState = defaultTokenizerState+ , treeParserState = TreeParserState+ { insertionMode = Initial+ , originalInsertionMode = Nothing+ , templateInsertionModes = []+ , openElements = []+ , activeFormattingElements = []+ , elementIndex = 0+ , isInIFrameSrcDoc = False+ , fragmentContext = Nothing+ , quirksMode = NoQuirks+ , fosteringEnabled = False+ , scriptingEnabled = False+ , framesetOk = True+ , headElementPointer = Nothing+ , formElementPointer = Nothing+ }+ }+++-- | The various fixed points in the tree construction algorithm, where the+-- parser may break and re-enter seamlessly.+data InsertionMode+ = Initial+ -- ^ __HTML:__+ -- @[initial]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#the-initial-insertion-mode)@+ -- + -- Before the doctype declaration, if one exists.+ | BeforeHtml+ -- ^ __HTML:__+ -- @[before html]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#the-before-html-insertion-mode)@+ -- + -- Before the first markup tag in the document.+ | BeforeHead+ -- ^ __HTML:__+ -- @[before head]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#the-before-head-insertion-mode)@+ -- + -- Between the root @\<html\>@ tag and the opening @\<head\>@.+ | InHead+ -- ^ __HTML:__+ -- @[in head]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead)@+ -- + -- Within the @\<head\>@ section, describing document metadata.+ | InHeadNoscript+ -- ^ __HTML:__+ -- @[in head noscript]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inheadnoscript)@+ -- + -- Within a @\<noscript\>@ section of the @\<head\>@, describing+ -- alternate document metadata to be used if script support has been+ -- disabled.+ | AfterHead+ -- ^ __HTML:__+ -- @[after head]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#the-after-head-insertion-mode)@+ -- + -- Between the closing @\</html\>@ and opening @\<body\>@ markup tags.+ | InBody+ -- ^ __HTML:__+ -- @[in body]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody)@+ -- + -- Within the @\<body\>@ section, describing the primary, renderable+ -- document content.+ | InText+ -- ^ __HTML:__+ -- @[text]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-incdata)@+ -- + -- Raw text which should be inserted into the document tree without+ -- translation to markup tags. Corresponds to the+ -- "Web.Mangrove.Parse.Tokenize" state 'RCDataState', and as such it is+ -- critical that the 'currentState' and 'prevStartTag' items be set+ -- appropriately to allow the parser to continue to the next+ -- 'InsertionMode'.+ | InTable+ -- ^ __HTML:__+ -- @[in table]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intable)@+ -- + -- Within a @\<table\>@ markup section, describing content laid out in+ -- a rectangular grid.+ | InTableText+ -- ^ __HTML:__+ -- @[in table text]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intabletext)@+ -- + -- Special processing for content misnested within a @\<table\>@ but+ -- outside any @\<td\>@ or @\<th\>@.+ | InCaption+ -- ^ __HTML:__+ -- @[in caption]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-incaption)@+ -- + -- Within a @\<caption\>@ markup section, describing the data presented+ -- by the enclosing @\<table\>@.+ | InColumnGroup+ -- ^ __HTML:__+ -- @[in column group]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-incolgroup)@+ -- + -- Within a @\<colgroup\>@ markup section, describing how the vertical+ -- columns in the enclosing @\<table\>@ should be rendered.+ | InTableBody+ -- ^ __HTML:__+ -- @[in table body]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intbody)@+ -- + -- Within a @\<tbody\>@ markup section, describing the actual data+ -- presented by the enclosing @\<table\>@.+ | InRow+ -- ^ __HTML:__+ -- @[in row]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intr)@+ -- + -- Within a @\<tr\>@ markup section, describing a line of+ -- mutually-associated data presented by the enclosing @\<table\>@.+ | InCell+ -- ^ __HTML:__+ -- @[in cell]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intd)@+ -- + -- Within a @\<td\>@ markup section, describing a single point of data+ -- presented by the enclosing @\<table\>@.+ | InSelect+ -- ^ __HTML:__+ -- @[in select]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect)@+ -- + -- Within a @\<select\>@ markup section, presenting several predefined+ -- options for the user's input.+ | InSelectInTable+ -- ^ __HTML:__+ -- @[in select in table]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselectintable)@+ -- + -- As 'InSelect', while providing extra cleanup logic for misnested+ -- table structure elements.+ | InTemplate+ -- ^ __HTML:__+ -- @[in template]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intemplate)@+ -- + -- Within a @\<template\>@ section, providing a+ -- 'Web.Mangrove.DOM.DocumentFragment' for simpler script-driven+ -- dynamic generation.+ | AfterBody+ -- ^ __HTML:__+ -- @[after body]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-afterbody)@+ -- + -- Between the closing @\</body\>@ and @\</html\>@ markup tags.+ | InFrameset+ -- ^ __HTML:__+ -- @[in frameset]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inframeset)@+ -- + -- Within the @\<frameset\>@ section, listing several external+ -- documents to display in lieu of local content.+ | AfterFrameset+ -- ^ __HTML:__+ -- @[after frameset]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-afterframeset)@+ -- + -- Between the closing @\</frameset\>@ and @\</html\>@ markup tags.+ | AfterAfterBody+ -- ^ __HTML:__+ -- @[after after body]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#the-after-after-body-insertion-mode)@+ -- + -- After the final @\</html\>@ markup tag, when the primary content was+ -- described by a @\<body\>@ section.+ | AfterAfterFrameset+ -- ^ __HTML:__+ -- @[after after frameset]+ -- (https://html.spec.whatwg.org/multipage/parsing.html#the-after-after-frameset-insertion-mode)@+ -- + -- After the final @\</html\>@ markup tag, when the primary content was+ -- linked via a @\<frameset\>@ section.+ deriving ( Eq, Ord, Bounded, Enum, Show, Read )+++-- | "Switch the insertion mode as specified."+switchMode :: InsertionMode -> TreeBuilder ()+switchMode mode = N.S.modify $ \state -> state+ { insertionMode = mode+ }++-- | "Switch the insertion mode to the original insertion mode."+resetMode :: TreeBuilder ()+resetMode = N.S.modify $ \state -> case originalInsertionMode state of+ Just mode -> state+ { insertionMode = mode+ , originalInsertionMode = Nothing+ }+ Nothing -> state+++-- | Indicate that the content which is or will be added to the document tree,+-- disallows the later use of @\<frameset\>@ transclusion.+setFramesetNotOk :: TreeBuilder ()+setFramesetNotOk = N.S.modify $ \state -> state+ { framesetOk = False+ }+++-- | "Insert a marker at the end of the list of active formatting elements."+insertFormattingMarker :: TreeBuilder ()+insertFormattingMarker = N.S.modify $ \state -> state+ { activeFormattingElements = [] : activeFormattingElements state+ }++-- | "Clear the list of active formatting elements up to the last marker."+clearFormattingElements :: TreeBuilder ()+clearFormattingElements = N.S.modify $ \state -> state+ { activeFormattingElements = drop 1 $ activeFormattingElements state+ }+++-- | "Push the specified insertion mode onto the stack of template insertion+-- modes."+pushTemplateMode :: InsertionMode -> TreeBuilder ()+pushTemplateMode mode = N.S.modify $ \state -> state+ { templateInsertionModes = mode : templateInsertionModes state+ }++-- | "Pop the current template insertion mode off the stack of template+-- insertion modes."+popTemplateMode :: TreeBuilder ()+popTemplateMode = N.S.modify $ \state -> state+ { templateInsertionModes = drop 1 $ templateInsertionModes state+ }+++-- | __HTML:__+-- @[an iframe srcdoc document]+-- (https://html.spec.whatwg.org/multipage/iframe-embed-object.html#an-iframe-srcdoc-document)@+-- +-- Test whether the current document is being loaded via an @\<iframe\>@.+inIFrameSrcDoc :: TreeBuilder Bool+inIFrameSrcDoc = isInIFrameSrcDoc <$> N.S.get+++-- | __HTML:__+-- @[current node]+-- (https://html.spec.whatwg.org/multipage/parsing.html#current-node)@+-- +-- The most-recently opened markup tag which has not yet been closed.+currentNode :: TreeBuilder (Maybe ElementParams)+currentNode = fmap snd . Y.listToMaybe . openElements <$> N.S.get++-- | The unique ID of the element described by 'currentNode'.+currentNodeIndex :: TreeBuilder (Maybe NodeIndex)+currentNodeIndex = fmap fst . Y.listToMaybe . openElements <$> N.S.get++-- | __HTML:__+-- @[adjusted current node]+-- (https://html.spec.whatwg.org/multipage/parsing.html#adjusted-current-node)@+-- +-- The most-recently opened markup tag which has not yet been closed, or the+-- context element if the document is being read as a document fragment and+-- everything else has been closed.+adjustedCurrentNode :: TreeBuilder (Maybe ElementParams)+adjustedCurrentNode = do+ state <- N.S.get+ if length (openElements state) <= 1 && Y.isJust (fragmentContext state)+ then return . fmap fst $ fragmentContext state+ else currentNode++-- | "Whether the parser was created as part of the HTML fragment parsing+-- algorithm."+inFragment :: TreeBuilder Bool+inFragment = do+ state <- N.S.get+ return . Y.isJust $ fragmentContext state+++-- | Map the given function over all attributes in the token data.+adjustAttributes :: (AttributeName -> AttributeName) -> TagParams -> TagParams+adjustAttributes adjust d = d+ { tagAttributes = M.fromList . map (F.B.first adjust) . M.toList $ tagAttributes d+ }++-- | __HTML:__+-- @[adjust MathML attributes]+-- (https://html.spec.whatwg.org/multipage/parsing.html#adjust-mathml-attributes)@+-- +-- Some attributes on [MathML](https://www.w3.org/TR/MathML/) elements are+-- defined in mixed case; restore that distinction to the case-folded token+-- data.+adjustMathMLAttributes :: TagParams -> TagParams+adjustMathMLAttributes = adjustAttributes adjust+ where adjust :: T.Text -> T.Text+ adjust "definitionurl" = "definitionURL"+ adjust name = name++-- | __HTML:__+-- @[adjust SVG attributes]+-- (https://html.spec.whatwg.org/multipage/parsing.html#adjust-svg-attributes)@+-- +-- Some attributes on [SVG](https://www.w3.org/TR/SVG/) elements are defined in+-- mixed case; restore that distinction to the case-folded token data.+adjustSvgAttributes :: TagParams -> TagParams+adjustSvgAttributes = adjustAttributes adjust+ where adjust :: T.Text -> T.Text+ adjust "attributename" = "attributeName"+ adjust "attributetype" = "attributeType"+ adjust "basefrequency" = "baseFrequency"+ adjust "baseprofile" = "baseProfile"+ adjust "calcmode" = "calcMode"+ adjust "clippathunits" = "clipPathUnits"+ adjust "diffuseconstant" = "diffuseConstant"+ adjust "edgemode" = "edgeMode"+ adjust "filterunits" = "filterUnits"+ adjust "glyphref" = "glyphRef"+ adjust "gradienttransform" = "gradientTransform"+ adjust "gradientunits" = "gradientUnits"+ adjust "kernelmatrix" = "kernelMatrix"+ adjust "kernelunitlength" = "kernelUnitLength"+ adjust "keypoints" = "keyPoints"+ adjust "keysplines" = "keySplines"+ adjust "keytimes" = "keyTimes"+ adjust "lengthadjust" = "lengthAdjust"+ adjust "limitingconeangle" = "limitingConeAngle"+ adjust "markerheight" = "markerHeight"+ adjust "markerunits" = "markerUnits"+ adjust "markerwidth" = "markerWidth"+ adjust "maskcontentunits" = "maskContentUnits"+ adjust "maskunits" = "maskUnits"+ adjust "numoctaves" = "numOctaves"+ adjust "pathlength" = "pathLength"+ adjust "patterncontentunits" = "patternContentUnits"+ adjust "patterntransform" = "patternTransform"+ adjust "patternunits" = "patternUnits"+ adjust "pointsatx" = "pointsAtX"+ adjust "pointsaty" = "pointsAtY"+ adjust "pointsatz" = "pointsAtZ"+ adjust "preservealpha" = "preserveAlpha"+ adjust "preserveaspectratio" = "preserveAspectRatio"+ adjust "primitiveunits" = "primitiveUnits"+ adjust "refx" = "refX"+ adjust "refy" = "refY"+ adjust "repeatcount" = "repeatCount"+ adjust "repeatdur" = "repeatDur"+ adjust "requiredextensions" = "requiredExtensions"+ adjust "requiredfeatures" = "requiredFeatures"+ adjust "specularconstant" = "specularConstant"+ adjust "specularexponent" = "specularExponent"+ adjust "spreadmethod" = "spreadMethod"+ adjust "startoffset" = "startOffset"+ adjust "stddeviation" = "stdDeviation"+ adjust "stitchtiles" = "stitchTiles"+ adjust "surfacescale" = "surfaceScale"+ adjust "systemlanguage" = "systemLanguage"+ adjust "tablevalues" = "tableValues"+ adjust "targetx" = "targetX"+ adjust "targety" = "targetY"+ adjust "textlength" = "textLength"+ adjust "viewbox" = "viewBox"+ adjust "viewtarget" = "viewTarget"+ adjust "xchannelselector" = "xChannelSelector"+ adjust "ychannelselector" = "yChannelSelector"+ adjust "zoomandpan" = "zoomAndPan"+ adjust name = name+++-- | __HTML:__+-- @[adjust foreign attributes]+-- (https://html.spec.whatwg.org/multipage/parsing.html#adjust-foreign-attributes)@+-- +-- The HTML specification expects most names to not carry XML-style prefixes+-- (e.g. @xlink:role@), and handles most which do anyway without issue.+-- However, that assumption proves disruptive for a few attributes, and those+-- exceptions should therefore be fixed.+-- +-- Note that this is a very simple algorithm in simply assuming a standard+-- prefix<->namespace assignment, and doesn't perform any scope test. As+-- perfect representation of the structured data isn't actually the goal, that+-- winds up being enough /in this case/. Do not expect actual XML to play+-- nicely.+adjustForeignAttributes :: ElementParams -> ElementParams+adjustForeignAttributes tag = case M.intersection attrs foreignNames of+ foreignAttrs | M.null foreignAttrs -> tag+ foreignAttrs ->+ let foreignAttrs' = fromAttrList . map adjustForeignAttribute $ toAttrList foreignAttrs+ in tag+ { elementAttributes = M.union foreignAttrs' $ M.difference attrs foreignNames+ }+ where attrs = elementAttributes tag+ foreignNames = M.fromList+ [ ((Nothing, n), ())+ | n <-+ [ "xlink:actuate"+ , "xlink:arcrole"+ , "xlink:href"+ , "xlink:role"+ , "xlink:show"+ , "xlink:title"+ , "xlink:type"+ , "xml:lang"+ , "xml:space"+ , "xmlns"+ , "xmlns:xlink"+ ]+ ]++-- | Compare the name of a single attribute to the known exceptions described+-- in 'adjustForeignAttributes', and update the values if it matches.+adjustForeignAttribute :: AttributeParams -> AttributeParams+adjustForeignAttribute attr = case T.splitOn ":" $ attrName attr of+ [p@"xlink", n@"actuate"] -> updateAttr p n xlinkNamespace+ [p@"xlink", n@"arcrole"] -> updateAttr p n xlinkNamespace+ [p@"xlink", n@"href"] -> updateAttr p n xlinkNamespace+ [p@"xlink", n@"role"] -> updateAttr p n xlinkNamespace+ [p@"xlink", n@"show"] -> updateAttr p n xlinkNamespace+ [p@"xlink", n@"title"] -> updateAttr p n xlinkNamespace+ [p@"xlink", n@"type"] -> updateAttr p n xlinkNamespace+ [p@"xml", n@"lang"] -> updateAttr p n xmlNamespace+ [p@"xml", n@"space"] -> updateAttr p n xmlNamespace+ [n@"xmlns"] -> attr+ { attrPrefix = Nothing+ , attrName = n+ , attrNamespace = Just xmlnsNamespace+ }+ [p@"xmlns", n@"xlink"] -> updateAttr p n xmlnsNamespace+ _ -> attr+ where updateAttr p n ns = attr+ { attrPrefix = Just p+ , attrName = n+ , attrNamespace = Just ns+ }+++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token indicates the end of the stream.+isEOF :: TreeInput -> Bool+isEOF t' = case tokenOut t' of+ -- 'EndOfStream' only works as an explicit EOF because of the manipulations in+ -- the parser runner ('Web.Mangrove.Parse.Tree.repackStream').+ EndOfStream -> True+ _ -> False++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token is a whitespace 'Character' in the ASCII range.+isWhitespace :: TreeInput -> Bool+isWhitespace t' = case tokenOut t' of+ Character c | isAsciiWhitespace c -> True+ _ -> False++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token is any 'Character'.+isCharacter :: TreeInput -> Bool+isCharacter t' = case tokenOut t' of+ Character _ -> True+ _ -> False++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token is specifically a @U+0000@ null 'Character'.+isNull :: TreeInput -> Bool+isNull t' = case tokenOut t' of+ Character '\NUL' -> True+ _ -> False++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token is any 'Comment'.+isComment :: TreeInput -> Bool+isComment t' = case tokenOut t' of+ Comment _ -> True+ _ -> False++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token is any 'Doctype'.+isDoctype :: TreeInput -> Bool+isDoctype t' = case tokenOut t' of+ Doctype _ -> True+ _ -> False++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token is any 'StartTag'. If the behaviour further requires the+-- 'StartTag' to have a specific name, use 'isStartTag' instead.+isAnyStartTag :: TreeInput -> Bool+isAnyStartTag t' = case tokenOut t' of+ StartTag _ -> True+ _ -> False++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token is a 'StartTag' with any of the given names; the list should+-- be entirely in lower-case. If the exact name doesn't actually matter, use+-- 'isAnyStartTag' instead.+isStartTag :: [String] -> TreeInput -> Bool+isStartTag names t' = case tokenOut t' of+ StartTag d | elem (T.unpack $ tagName d) names -> True+ _ -> False++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token is any 'EndTag'. If the behaviour further requires the+-- 'EndTag' to have a specific name, use 'isEndTag' instead.+isAnyEndTag :: TreeInput -> Bool+isAnyEndTag t' = case tokenOut t' of+ EndTag _ -> True+ _ -> False++-- | 'Web.Mangrove.Parse.Common.Parser.switch' case guard testing whether the+-- wrapped token is a 'EndTag' with any of the given names; the list should be+-- entirely in lower-case. If the exact name doesn't actually matter, use+-- 'isAnyEndTag' instead.+isEndTag :: [String] -> TreeInput -> Bool+isEndTag names t' = case tokenOut t' of+ EndTag d | elem (T.unpack $ tagName d) names -> True+ _ -> False+++-- | Check if the described node has the given name, and is in the HTML+-- namespace.+nodeIsElement :: T.Text -> ElementParams -> Bool+nodeIsElement name node = elementNamespace node == Just htmlNamespace && elementName node == name++-- | Check if the described node is one of the many which have specific behaviour+-- governing their addition to the document tree. See 'specialElements' for+-- the nodes themselves.+nodeIsSpecial :: ElementParams -> Bool+nodeIsSpecial node = case elementNamespace node of+ Just ns -> elem (ns, elementName node) specialElements+ Nothing -> False+++-- | Check whether any of the given tags, in the HTML namespace, are in the+-- stack of open elements.+hasOpenElement :: [ElementName] -> TreeBuilder Bool+hasOpenElement names = any isElement . filter isHtml . map snd . openElements <$> N.S.get+ where isElement tag = elem (elementName tag) names+ isHtml tag = elementNamespace tag == Just htmlNamespace++-- | Check if there are any tags on the stack of open elements, which are not+-- one of the given list, or are in a non-HTML namespace.+hasOpenElementExcept :: [ElementName] -> TreeBuilder Bool+hasOpenElementExcept names = any isNotElement . map snd . openElements <$> N.S.get+ where isNotElement tag = isNotHtml tag || notElem (elementName tag) names+ isNotHtml tag = elementNamespace tag /= Just htmlNamespace+++-- | __HTML:__+-- the elements listed for @[has a particular element in scope]+-- (https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-scope)@+-- +-- Several elements provide a breakpoint which limits how far up the tree some+-- searches for open tags will propagate; this list defines those. See+-- 'hasInScope' for a test using this list.+scopeElements :: [(Namespace, ElementName)]+scopeElements =+ [ (htmlNamespace, n)+ | n <-+ [ "applet"+ , "caption"+ , "html"+ , "table"+ , "td"+ , "th"+ , "marquee"+ , "object"+ , "template"+ ]+ ] +++ [ (mathMLNamespace, n)+ | n <-+ [ "mi"+ , "mo"+ , "mn"+ , "ms"+ , "mtext"+ , "annotation-xml"+ ]+ ] +++ [ (svgNamespace, n)+ | n <-+ [ "foreignObject"+ , "desc"+ , "title"+ ]+ ]++-- | __HTML:__+-- @[special category]+-- (https://html.spec.whatwg.org/multipage/parsing.html#special)@+-- +-- The specification describes specific behaviour for many tags, and refers+-- back to those same tags at a few points in the parse algorithm. This then+-- collects everything in that category; see 'nodeIsSpecial' to test against+-- it.+specialElements :: [(Namespace, ElementName)]+specialElements =+ [ (htmlNamespace, n)+ | n <-+ [ "address"+ , "area"+ , "article"+ , "aside"+ , "base"+ , "basefont"+ , "bgsound"+ , "blockquote"+ , "body"+ , "br"+ , "button"+ , "center"+ , "col"+ , "colgroup"+ , "dd"+ , "details"+ , "dir"+ , "div"+ , "dl"+ , "dt"+ , "embed"+ , "fieldset"+ , "figcaption"+ , "figure"+ , "footer"+ , "form"+ , "frame"+ , "frameset"+ , "h1"+ , "h2"+ , "h3"+ , "h4"+ , "h5"+ , "h6"+ , "head"+ , "header"+ , "hgroup"+ , "hr"+ , "iframe"+ , "img"+ , "input"+ , "keygen"+ , "li"+ , "link"+ , "listing"+ , "main"+ , "menu"+ , "meta"+ , "nav"+ , "noembed"+ , "noframes"+ , "noscript"+ , "ol"+ , "p"+ , "param"+ , "plaintext"+ , "pre"+ , "script"+ , "section"+ , "select"+ , "style"+ , "source"+ , "summary"+ , "tbody"+ , "textarea"+ , "tfoot"+ , "thead"+ , "title"+ , "tr"+ , "track"+ , "ul"+ , "wbr"+ , "xmp"+ ]+ ] ++ scopeElements+++-- | __HTML:__+-- @[has a particular element in a specific scope]+-- (https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-the-specific-scope)@+-- +-- Check if a tag with any of the given names, in the HTML namespace, is in the+-- stack of open elements more recently than the closest boundary element. See+-- 'scopeElements' for the typical list, but generally 'hasInScope',+-- 'hasInButtonScope', 'hasInListItemScope', 'hasInSelectScope', or+-- 'hasInTableScope' should be used instead.+inScope :: [(Namespace, ElementName)] -> [T.Text] -> TreeBuilder Bool+inScope bound names = recurse . openElements <$> N.S.get+ where recurse [] = False+ recurse ((_, e):es)+ | elem (elementName e) names = True+ | elem (elementNamespace e, elementName e) (map (F.B.first Just) bound) = False+ | otherwise = recurse es++-- | Given a node's unique ID, check if it is in the stack of open elements,+-- more recently than the closest boundary node from 'scopeElements'. See+-- 'hasInScope' if any node with a given name will suffice.+hasIndexInScope :: NodeIndex -> TreeBuilder Bool+hasIndexInScope index = any ((== index) . fst) . takeWhile (not . isScopeElement) .+ openElements <$> N.S.get+ where isScopeElement (_, e) = case elementNamespace e of+ Just ns -> elem (ns, elementName e) scopeElements+ Nothing -> False++-- | __HTML:__+-- @[has a particular element in scope]+-- (https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-scope)@+-- +-- Check if a tag with any of the given names, in the HTML namespace, is in the+-- stack of open elements more recently than the closest boundary node from+-- 'scopeElements'. See 'hasIndexInScope' if a specific existing element is+-- required.+hasInScope :: [T.Text] -> TreeBuilder Bool+hasInScope = inScope scopeElements++-- | __HTML:__+-- @[has a particular element in list item scope]+-- (https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-list-item-scope)@+-- +-- Check if a tag with any of the given names, in the HTML namespace, is in the+-- stack of open elements more recently than the closest boundary node of+-- either @\<ol\>@, @\<ul\>@, or any from 'scopeElements'.+hasInListItemScope :: [T.Text] -> TreeBuilder Bool+hasInListItemScope = inScope $+ [ (htmlNamespace, n)+ | n <-+ [ "ol"+ , "ul"+ ]+ ] ++ scopeElements++-- | __HTML:__+-- @[has a particular element in button scope]+-- (https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-button-scope)@+-- +-- Check if a tag with any of the given names, in the HTML namespace, is in the+-- stack of open elements more recently than the closest boundary node of+-- either @\<button\>@ or any from 'scopeElements'.+hasInButtonScope :: [T.Text] -> TreeBuilder Bool+hasInButtonScope = inScope $ (htmlNamespace, "button") : scopeElements++-- | __HTML:__+-- @[has a particular element in table scope]+-- (https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-table-scope)@+-- +-- Check if a tag with any of the given names, in the HTML namespace, is in the+-- stack of open elements more recently than the closest boundary node of+-- either @\<table\>@ or a top-level element.+hasInTableScope :: [T.Text] -> TreeBuilder Bool+hasInTableScope = inScope+ [ (htmlNamespace, n)+ | n <-+ [ "html"+ , "table"+ , "template"+ ]+ ]++-- | __HTML:__+-- @[has a particular element in select scope]+-- (https://html.spec.whatwg.org/multipage/parsing.html#has-an-element-in-select-scope)@+-- +-- Check if a tag with any of the given names, in the HTML namespace, is in the+-- stack of open elements more recently than /any/ node except an+-- @\<optgroup\>@ or @\<option\>@ element.+hasInSelectScope :: [T.Text] -> TreeBuilder Bool+hasInSelectScope names = recurse . openElements <$> N.S.get+ where recurse [] = False+ recurse ((_, e):es)+ | elem (elementName e) names = True+ | elem (elementName e) ["optgroup", "option"]+ && elementNamespace e == Just htmlNamespace = recurse es+ | otherwise = False+++-- | __HTML:__+-- @[reset the insertion mode appropriately]+-- (https://html.spec.whatwg.org/multipage/parsing.html#reset-the-insertion-mode-appropriately)@+-- +-- Guess what part of the state machine should be active, according to the+-- stack of open elements. See 'resetInsertionMode'' for use outside a parser+-- monad.+resetInsertionMode :: TreeBuilder ()+resetInsertionMode = N.S.modify resetInsertionMode'++-- | __HTML:__+-- @[reset the insertion mode appropriately]+-- (https://html.spec.whatwg.org/multipage/parsing.html#reset-the-insertion-mode-appropriately)@+-- +-- Guess what part of the state machine should be active, according to the+-- stack of open elements. See 'resetInsertionMode' if the calculation is+-- being made as part of the tree construction parsing algorithm.+resetInsertionMode' :: TreeParserState -> TreeParserState+resetInsertionMode' state = resetInsertionMode'' $ openElements state+ where resetInsertionMode'' [] = switchMode' InBody+ resetInsertionMode'' ((_, e):es)+ | isLast && nodeIsElement "select" e' = switchMode' InSelect+ | nodeIsElement "select" e' = loopSelect es+ | not isLast && nodeIsElement "td" e' = switchMode' InCell+ | not isLast && nodeIsElement "th" e' = switchMode' InCell+ | nodeIsElement "tr" e' = switchMode' InRow+ | nodeIsElement "tbody" e' = switchMode' InTableBody+ | nodeIsElement "thead" e' = switchMode' InTableBody+ | nodeIsElement "tfoot" e' = switchMode' InTableBody+ | nodeIsElement "caption" e' = switchMode' InCaption+ | nodeIsElement "colgroup" e' = switchMode' InColumnGroup+ | nodeIsElement "table" e' = switchMode' InTable+ | nodeIsElement "template" e' = maybe state switchMode' . Y.listToMaybe $ templateInsertionModes state+ | not isLast && nodeIsElement "head" e' = switchMode' InHead+ | nodeIsElement "body" e' = switchMode' InBody+ | nodeIsElement "frameset" e' = switchMode' InFrameset+ | nodeIsElement "html" e' =+ switchMode' $ if Y.isNothing $ headElementPointer state+ then BeforeHead+ else AfterHead+ | isLast = switchMode' InBody+ | otherwise = resetInsertionMode'' es+ where isLast = null es+ e' = case fragmentContext state of+ Nothing -> e+ Just context -> fst context+ loopSelect [] = switchMode' InSelect+ loopSelect [_] = switchMode' InSelect+ loopSelect ((_, n):ns)+ | nodeIsElement "template" n = switchMode' InSelect+ | nodeIsElement "table" n = switchMode' InSelectInTable+ | otherwise = loopSelect ns+ switchMode' mode = state+ { insertionMode = mode+ }+++-- | __HTML:__+-- @[MathML text integration point]+-- (https://html.spec.whatwg.org/multipage/parsing.html#mathml-text-integration-point)@+-- +-- Whether the markup tag introduces a section of less-structured text content+-- embedded within a MathML object.+atMathMLIntegration :: ElementParams -> Bool+atMathMLIntegration current =+ elementNamespace current == Just mathMLNamespace+ && elem (elementName current) ["mi", "mo", "mn", "ms", "mtext"]++-- | Whether the markup tag is specifically an @\<annotation-xml\>@ tag within+-- the MathML namespace.+isMathMLAnnotationXml :: ElementParams -> Bool+isMathMLAnnotationXml current+ | elementNamespace current == Just mathMLNamespace+ && elementName current == "annotation-xml" = True+ | otherwise = False++-- | __HTML:__+-- @[HTML integration point]+-- (https://html.spec.whatwg.org/multipage/parsing.html#html-integration-point)@+-- +-- Whether the markup tag introduces a section of HTML content embedded within+-- a MathML or SVG object.+atHtmlIntegration :: ElementParams -> Bool+atHtmlIntegration current+ | isMathMLAnnotationXml current+ && isIntegrationAttribute (M.lookup (Nothing, "encoding") $ elementAttributes current) = True+ | elementNamespace current == Just svgNamespace+ && elem (elementName current) ["foreignObject", "desc", "title"] = True+ | otherwise = False+ where isIntegrationAttribute (Just (_, value)) = case T.map toAsciiLower value of+ "text/html" -> True+ "application/xhtml+xml" -> True+ _ -> False+ isIntegrationAttribute _ = False
+ src/Web/Mangrove/Parse/Tree/Dispatcher.hs view
@@ -0,0 +1,123 @@+{-|+Description: Token processing rules within non-HTML content.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.Dispatcher+ ( dispatcher+ , dispatchHtml+ ) where+++import qualified Control.Monad.Trans.State as N.S++import qualified Data.Text as T++import Web.Willow.DOM++import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch++import Web.Mangrove.Parse.Tree.Initial+import Web.Mangrove.Parse.Tree.AfterAfterBody+import Web.Mangrove.Parse.Tree.AfterAfterFrameset+import Web.Mangrove.Parse.Tree.AfterBody+import Web.Mangrove.Parse.Tree.AfterFrameset+import Web.Mangrove.Parse.Tree.AfterHead+import Web.Mangrove.Parse.Tree.BeforeHead+import Web.Mangrove.Parse.Tree.BeforeHtml+import Web.Mangrove.Parse.Tree.Foreign+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.InCaption+import Web.Mangrove.Parse.Tree.InCell+import Web.Mangrove.Parse.Tree.InColumnGroup+import Web.Mangrove.Parse.Tree.InFrameset+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.InHeadNoscript+import Web.Mangrove.Parse.Tree.InRow+import Web.Mangrove.Parse.Tree.InSelect+import Web.Mangrove.Parse.Tree.InSelectInTable+import Web.Mangrove.Parse.Tree.InTable+import Web.Mangrove.Parse.Tree.InTableBody+import Web.Mangrove.Parse.Tree.InTableText+import Web.Mangrove.Parse.Tree.InTemplate+import Web.Mangrove.Parse.Tree.InText+++-- | __HTML:__+-- @[tree construction dispatcher]+-- (https://html.spec.whatwg.org/multipage/parsing.html#tree-construction-dispatcher)@+-- +-- Delegate parsing the binary stream to the appropriate content class: lenient+-- HTML (via insertion mode) or embedded, more-structured MathML/SVG content.+dispatcher :: TreeBuilder TreeOutput+dispatcher = dispatchToken+ where dispatchToken = do+ state <- N.S.get+ adjusted <- adjustedCurrentNode+ flip switch adjusted+ [ If_ (const $ null (openElements state)) dispatchHtml+ , If_ (\n -> (n >>= elementNamespace) == Just htmlNamespace) dispatchHtml+ , If_ (maybe False atMathMLIntegration) $ lookAhead next >>= switch+ [ If_ (hasStartTagName $ T.pack "mglyph") treeForeign+ , If_ (hasStartTagName $ T.pack "malignmark") treeForeign+ , If_ isAnyStartTag dispatchHtml+ , If_ isCharacter dispatchHtml+ , Else_ treeForeign+ ]+ , If_ (maybe False atHtmlIntegration) $ lookAhead next >>= switch+ [ If_ isAnyStartTag dispatchHtml+ , If_ isCharacter dispatchHtml+ , Else_ treeForeign+ ]+ -- 'isMathMLAnnotationXml' is less specific than+ -- 'atHtmlIntegration' and so needs to appear after it.+ , If_ (maybe False isMathMLAnnotationXml) $ lookAhead next >>= switch+ [ If_ (hasStartTagName $ T.pack "svg") dispatchHtml+ , Else_ treeForeign+ ]+ , Else_ treeForeign+ ]+ hasStartTagName name t' = case tokenOut t' of+ StartTag d -> tagName d == name+ _ -> False+++-- | __HTML:__+-- @[the rules for parsing tokens in HTML content]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhtml)@+-- +-- Defer processing of the current token to the instructions defined by the+-- active 'insertionMode'.+dispatchHtml :: TreeBuilder TreeOutput+dispatchHtml = N.S.get >>= \state -> case insertionMode state of+ Initial -> treeInitial+ BeforeHtml -> treeBeforeHtml+ BeforeHead -> treeBeforeHead+ InHead -> treeInHead+ InHeadNoscript -> treeInHeadNoscript+ AfterHead -> treeAfterHead+ InBody -> treeInBody+ InText -> treeInText+ InTable -> treeInTable+ InTableText -> treeInTableText+ InCaption -> treeInCaption+ InColumnGroup -> treeInColumnGroup+ InTableBody -> treeInTableBody+ InRow -> treeInRow+ InCell -> treeInCell+ InSelect -> treeInSelect+ InSelectInTable -> treeInSelectInTable+ InTemplate -> treeInTemplate+ AfterBody -> treeAfterBody+ InFrameset -> treeInFrameset+ AfterFrameset -> treeAfterFrameset+ AfterAfterBody -> treeAfterAfterBody+ AfterAfterFrameset -> treeAfterAfterFrameset
+ src/Web/Mangrove/Parse/Tree/Dispatcher.hs-boot view
@@ -0,0 +1,17 @@+{-|+Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com+-}+module Web.Mangrove.Parse.Tree.Dispatcher+ ( dispatcher+ , dispatchHtml+ ) where+++import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.Patch+++dispatcher :: TreeBuilder TreeOutput+dispatchHtml :: TreeBuilder TreeOutput
+ src/Web/Mangrove/Parse/Tree/Foreign.hs view
@@ -0,0 +1,212 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within non-HTML content.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.Foreign+ ( treeForeign+ ) where+++import qualified Control.Monad.Trans.State as N.S++import qualified Data.HashMap.Strict as M+import qualified Data.Maybe as Y+import qualified Data.Text as T++import Web.Willow.DOM++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch++import {-# SOURCE #-} Web.Mangrove.Parse.Tree.Dispatcher+++-- | __HTML:__+-- @[the rules for parsing tokens in foreign content]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inforeign)@+-- +-- The parsing instructions for non-HTML content, which follows a simpler+-- recovery model more in line with HTML.+treeForeign :: TreeBuilder TreeOutput+treeForeign = next >>= switch+ [ If isNull $ \t' ->+ consTreeError UnexpectedNullCharacter <$> insertCharacter (mapTokenOut (const $ Character replacementChar) t')+ , If isWhitespace insertCharacter+ , If isCharacter $ \t' -> setFramesetNotOk *> insertCharacter t'+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If isEOF $ \t' -> push t' *> treeInBody+ , If (isStartTag+ [ "b"+ , "big"+ , "blockquote"+ , "body"+ , "br"+ , "center"+ , "code"+ , "dd"+ , "div"+ , "dl"+ , "dt"+ , "em"+ , "embed"+ , "h1"+ , "h2"+ , "h3"+ , "h4"+ , "h5"+ , "h6"+ , "head"+ , "hr"+ , "i"+ , "img"+ , "li"+ , "listing"+ , "menu"+ , "meta"+ , "nobr"+ , "ol"+ , "p"+ , "pre"+ , "ruby"+ , "s"+ , "small"+ , "span"+ , "strong"+ , "strike"+ , "sub"+ , "sup"+ , "table"+ , "tt"+ , "u"+ , "ul"+ , "var"+ ]) htmlStartTag+ , If (isStartTag ["font"]) $ \t' ->+ if any (`elem` ["color", "face", "size"]) (map fst . M.toList . tokenAttributes $ tokenOut t')+ then htmlStartTag t'+ else anyOtherStartTag t'+ , If isAnyStartTag anyOtherStartTag+ , If (isEndTag ["script"]) $ \t' -> do+ current <- currentNode+ state <- N.S.get+ if (current >>= elementNamespace) == Just mathMLNamespace+ then closeCurrentNode t' <* closeSvgScript+ else anyOtherEndTag (openElements state) t'+ , If isAnyEndTag $ \t' -> do+ state <- N.S.get+ anyOtherEndTag (openElements state) t'+ ]+ where anyOtherStartTag t' = case tokenOut t' of+ StartTag d -> do+ current <- adjustedCurrentNode+ let ns = Y.fromMaybe htmlNamespace $ current >>= elementNamespace+ d' = case ns of+ ns' | mathMLNamespace == ns' -> adjustMathMLAttributes d+ ns' | svgNamespace == ns' -> adjustSvgAttributes $ case tagName d of+ "altglyph" -> d { tagName = "altGlyph" }+ "altglyphdef" -> d { tagName = "altGlyphDef" }+ "altglyphitem" -> d { tagName = "altGlyphItem" }+ "animatecolor" -> d { tagName = "animateColor" }+ "animatemotion" -> d { tagName = "animateMotion" }+ "animatetransform" -> d { tagName = "animateTransform" }+ "clippath" -> d { tagName = "clipPath" }+ "feblend" -> d { tagName = "feBlend" }+ "fecolormatrix" -> d { tagName = "feColorMatrix" }+ "fecomponenttransfer" -> d { tagName = "feComponentTransfer" }+ "fecomposite" -> d { tagName = "feComposite" }+ "feconvolvematrix" -> d { tagName = "feConvolveMatrix" }+ "fediffuselighting" -> d { tagName = "feDiffuseLighting" }+ "fedisplacementmap" -> d { tagName = "feDisplacementMap" }+ "fedistantlight" -> d { tagName = "feDistantLight" }+ "fedropshadow" -> d { tagName = "feDropShadow" }+ "feflood" -> d { tagName = "feFlood" }+ "fefunca" -> d { tagName = "feFuncA" }+ "fefuncb" -> d { tagName = "feFuncB" }+ "fefuncg" -> d { tagName = "feFuncG" }+ "fefuncr" -> d { tagName = "feFuncR" }+ "fegaussianblur" -> d { tagName = "feGaussianBlur" }+ "feimage" -> d { tagName = "feImage" }+ "femerge" -> d { tagName = "feMerge" }+ "femergenode" -> d { tagName = "feMergeNode" }+ "femorphology" -> d { tagName = "feMorphology" }+ "feoffset" -> d { tagName = "feOffset" }+ "fepointlight" -> d { tagName = "fePointLight" }+ "fespecularlighting" -> d { tagName = "feSpecularLighting" }+ "fespotlight" -> d { tagName = "feSpotLight" }+ "fetile" -> d { tagName = "feTile" }+ "feturbulence" -> d { tagName = "feTurbulence" }+ "foreignobject" -> d { tagName = "foreignObject" }+ "glyphref" -> d { tagName = "glyphRef" }+ "lineargradient" -> d { tagName = "linearGradient" }+ "radialgradient" -> d { tagName = "radialGradient" }+ "textpath" -> d { tagName = "textPath" }+ _ -> d+ _ -> d+ t'' = mapTokenOut (const $ StartTag d') t'+ if tagIsSelfClosing d'+ then if tagName d' == "script" && ns == svgNamespace+ then insertForeignNullElement ns t'' <* closeSvgScript+ else insertForeignNullElement ns t''+ else insertForeignElement ns t''+ _ -> packTreeErrors [] t'+ anyOtherEndTag [] t' = packTreeErrors [] t'+ anyOtherEndTag es@(e:_) t' = case tokenOut t' of+ EndTag d -> if T.map toAsciiLower (elementName $ snd e) == tagName d+ then loopEndTag d es t'+ else consTreeError UnexpectedElementWithImpliedEndTag <$> loopEndTag d es t'+ _ -> packTreeErrors [] t'+ loopEndTag d es t' = either id id <$> loopEndTag' d es t'+ loopEndTag' _ [] t' = Left <$> packTreeErrors [] t'+ loopEndTag' _ [_] t' = Left <$> packTreeErrors [] t'+ loopEndTag' d (e:es) t'+ | T.map toAsciiLower (elementName $ snd e) == tagName d =+ Right <$> closeCurrentNode t'+ | otherwise = case es of+ (e':_) | elementNamespace (snd e') == Just htmlNamespace -> push t' *> fmap Left dispatchHtml+ _ -> do+ recurse <- loopEndTag' d es t'+ case recurse of+ l@(Left _) -> return l+ Right clear -> do+ close <- closeCurrentNode_+ return . Right $ close ++| clear+ tokenAttributes (StartTag d) = tagAttributes d+ tokenAttributes (EndTag d) = tagAttributes d+ tokenAttributes _ = M.empty+ htmlStartTag t' = do+ isFragment <- inFragment+ consTreeError UnexpectedHtmlElementInForeignContent <$> if isFragment+ then anyOtherStartTag t'+ else do+ push t'+ close <- closeCurrentNode_+ clear <- clearToIntegration+ packTree_ $ close ++ clear+ clearToIntegration = do+ current <- currentNode+ let mathML = maybe False atMathMLIntegration current+ html = maybe False atHtmlIntegration current+ ns = Y.fromMaybe htmlNamespace $ current >>= elementNamespace+ if mathML || html || ns == htmlNamespace+ then return []+ else do+ close <- closeCurrentNode_+ clear <- clearToIntegration+ return $ close ++ clear+ closeSvgScript = undefined
+ src/Web/Mangrove/Parse/Tree/InBody.hs view
@@ -0,0 +1,971 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within the @\<body\>@ section.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: provisional+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InBody+ ( treeInBody+ ) where+++import qualified Control.Monad as N+import qualified Control.Monad.Trans.State as N.S++import qualified Data.HashMap.Strict as M+import qualified Data.List as L+import qualified Data.Maybe as Y+import qualified Data.Text as T++import Web.Willow.DOM++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.InTemplate+import Web.Mangrove.Parse.Tree.InText+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch++import {-# SOURCE #-} Web.Mangrove.Parse.Tree.Dispatcher++import Control.Applicative ( (<|>) )+import Data.Functor ( ($>) )+++-- | __HTML:__+-- @[the "in body" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody)@+-- +-- The parsing instructions corresponding to the 'InBody' section of the state+-- machine.+treeInBody :: TreeBuilder TreeOutput+treeInBody = next >>= switch+ [ If isNull $ packTreeErrors [UnexpectedNullCharacter]+ , If isWhitespace $ \t' -> do+ format <- reconstructFormattingElements+ insert <- insertCharacter t'+ return $ format ++| insert+ , If isCharacter $ \t' -> do+ setFramesetNotOk+ format <- reconstructFormattingElements+ insert <- insertCharacter t'+ return $ format ++| insert+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ errs <- packTreeErrors [NestedSingletonElement] t'+ elements <- openElements <$> N.S.get+ if Y.isJust $ L.find (nodeIsElement "template" . snd) elements+ then return errs+ else case reverse elements of+ [] -> return errs+ ((i, _):_) -> do+ add <- mapM (addAttribute InHtmlElement i) .+ M.toList . tagAttributes $ tokenTag t'+ return $ errs |++ concat add+ , If (isStartTag+ [ "base"+ , "basefont"+ , "bgsound"+ , "link"+ , "meta"+ , "noframes"+ , "script"+ , "style"+ , "template"+ , "title"+ ]) $ \t' -> do+ push t'+ treeInHead+ , If (isEndTag ["template"]) $ \t' -> do+ push t'+ treeInHead+ , If (isStartTag ["body"]) $ \t' -> do+ errs <- packTreeErrors [NestedSingletonElement] t'+ elements <- openElements <$> N.S.get+ case reverse elements of+ [] -> return errs+ [_] -> return errs+ es | Y.isJust $ L.find (nodeIsElement "template" . snd) es -> return errs+ (_:(i, e):es)+ | not (nodeIsElement "body" e) -> return errs+ | otherwise -> do+ setFramesetNotOk+ add <- mapM+ (addAttribute (RelativeLocation . fromIntegral $ length es) i) .+ M.toList . tagAttributes $ tokenTag t'+ return $ errs |++ concat add+ , If (isStartTag ["frameset"]) $ \t' -> do+ state <- N.S.get+ let elements = map snd $ openElements state+ err = FramesetInBody $ tokenElement t'+ errs <- packTreeErrors [err] t'+ case reverse elements of+ [] -> return errs+ [_] -> return errs+ (_:e:_) | not (nodeIsElement "body" e) -> return errs+ _ | not (framesetOk state) -> return errs+ _ -> do+ clear <- N.replicateM (length elements - 1) dropCurrentNode+ insert <- insertElement t'+ switchMode InFrameset+ return . consTreeError err $ concat clear ++| insert+ , If isEOF $ \t' -> do+ modes <- templateInsertionModes <$> N.S.get+ if null modes+ then do+ hasUnexpected <- hasUnexpectedOpenElement+ if hasUnexpected+ then consTreeError UnexpectedElementWithImpliedEndTag <$> stopParsing t'+ else stopParsing t'+ else do+ push t'+ treeInTemplate+ , If (isEndTag ["body"]) $ \t' -> do+ switchMode AfterBody+ hasBody <- hasInScope ["body"]+ hasUnexpected <- hasUnexpectedOpenElement+ if hasBody+ then if hasUnexpected+ then packTreeErrors [UnexpectedElementWithImpliedEndTag] t'+ else packTreeErrors [] t'+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isEndTag ["html"]) $ \t' -> do+ push t'+ switchMode AfterBody+ hasBody <- hasInScope ["body"]+ hasUnexpected <- hasUnexpectedOpenElement+ if hasBody+ then if hasUnexpected+ then consTreeError UnexpectedElementWithImpliedEndTag <$> dispatchHtml+ else packTreeErrors_ []+ else consTreeError (UnmatchedEndTag $ tokenElement t') <$> dispatchHtml+ , If (isStartTag+ [ "address"+ , "article"+ , "aside"+ , "blockquote"+ , "center"+ , "details"+ , "dialog"+ , "dir"+ , "div"+ , "dl"+ , "fieldset"+ , "figcaption"+ , "figure"+ , "footer"+ , "header"+ , "hgroup"+ , "main"+ , "menu"+ , "nav"+ , "ol"+ , "p"+ , "section"+ , "summary"+ , "ul"+ ]) $ \t' -> do+ hasP <- hasInButtonScope ["p"]+ if hasP+ then do+ close <- closePElement+ insert <- insertElement t'+ return $ close ++| insert+ else insertElement t'+ , If (isStartTag $ map T.unpack headerNames) $ \t' -> do+ current <- currentNode+ hasP <- hasInButtonScope ["p"]+ close <- if hasP+ then closePElement+ else return []+ nest <- case fmap elementName current of+ Just h | elem h headerNames -> consTreeError_ OverlappingHeaderElements <$> closeCurrentNode_+ _ -> return []+ insert <- insertElement t'+ return $ close ++ nest ++| insert+ , If (isStartTag ["pre", "listing"]) $ \t' -> do+ lineFeed <- next+ lineFeedState <- if tokenOut lineFeed == Character '\n'+ then return $ tokenState lineFeed+ else push lineFeed $> Nothing+ setFramesetNotOk+ hasP <- hasInButtonScope ["p"]+ close <- if hasP+ then closePElement+ else return []+ insert <- insertElement $ mapTokenState' t' (lineFeedState <|>)+ return $ close ++| insert+ , If (isStartTag ["form"]) $ \t' -> do+ state <- N.S.get+ let hasTemplate = any (nodeIsElement "template" . snd) $ openElements state+ if hasTemplate || Y.isJust (formElementPointer state)+ then packTreeErrors [] t'+ else do+ hasP <- hasInButtonScope ["p"]+ close <- if hasP+ then closePElement+ else return []+ N.S.modify $ \state' -> state'+ { formElementPointer = Just $ elementIndex state'+ }+ insert <- insertElement t'+ return $ close ++| insert+ , If (isStartTag ["li"]) $ \t' -> do+ setFramesetNotOk+ state <- N.S.get+ clear <- liLoop $ openElements state+ hasP <- hasInButtonScope ["p"]+ close <- if hasP+ then closePElement+ else return []+ insert <- insertElement t'+ return $ clear ++ close ++| insert+ , If (isStartTag ["dd", "dt"]) $ \t' -> do+ setFramesetNotOk+ state <- N.S.get+ clear <- ddLoop $ openElements state+ hasP <- hasInButtonScope ["p"]+ close <- if hasP+ then closePElement+ else return []+ insert <- insertElement t'+ return $ clear ++ close ++| insert+ , If (isStartTag ["plaintext"]) $ \t' -> do+ hasP <- hasInButtonScope ["p"]+ close <- if hasP+ then closePElement+ else return []+ insert <- insertElement . mapTokenState t' $ \state -> state+ { currentState = PlainTextState+ }+ return $ close ++| insert+ , If (isStartTag ["button"]) $ \t' -> do+ hasButton <- hasInScope ["button"]+ nested <- if hasButton+ then do+ generate <- generateEndTags impliedEndTags+ clear <- closeElement "button"+ return . consTreeError_ NestedNonRecursiveElement $ generate ++ clear+ else return []+ format <- reconstructFormattingElements+ insert <- insertElement t'+ setFramesetNotOk+ return $ nested ++ format ++| insert+ , If (isEndTag+ [ "address"+ , "article"+ , "aside"+ , "blockquote"+ , "button"+ , "center"+ , "details"+ , "dialog"+ , "dir"+ , "div"+ , "dl"+ , "fieldset"+ , "figcaption"+ , "figure"+ , "footer"+ , "header"+ , "hgroup"+ , "listing"+ , "main"+ , "menu"+ , "nav"+ , "ol"+ , "pre"+ , "section"+ , "summary"+ , "ul"+ ]) $ \t' -> do+ let d = tokenTag t'+ hasMatch <- hasInScope [tagName d]+ if hasMatch+ then do+ generate <- generateEndTags impliedEndTags+ current <- currentNode+ let errF = if maybe True (nodeIsElement $ tagName d) current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ clear <- closeElement $ tagName d+ packTree t' $ generate ++ errF clear+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isEndTag ["form"]) $ \t' -> do+ state <- N.S.get+ if Y.isJust . L.find (nodeIsElement "template" . snd) $ openElements state+ then do+ hasTemplate <- hasInScope ["template"]+ if hasTemplate+ then do+ generate <- generateEndTags impliedEndTags+ current <- currentNode+ let errF = if maybe True (nodeIsElement "form") current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ clear <- closeElement "form"+ packTree t' $ generate ++ errF clear+ else packTreeErrors [] t'+ else do+ let formElement = formElementPointer state+ N.S.put $ state+ { formElementPointer = Nothing+ }+ hasFormElement <- maybe (return False) hasIndexInScope formElement+ if hasFormElement+ then do+ generate <- generateEndTags impliedEndTags+ let current = maybe 0 fst . Y.listToMaybe $ openElements state+ es = takeWhile ((/=) (Y.fromMaybe current formElement) . fst) $+ openElements state+ errF = if null es+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ close <- closeAncestorNode_ . fromIntegral $ length es+ packTree t' $ generate ++ errF close+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isEndTag ["p"]) $ \t' -> do+ hasP <- hasInButtonScope ["p"]+ ps <- if hasP+ then closePElement+ else fmap (consTreeError_ . UnmatchedEndTag $ tokenElement t') . insertNullElement_ $ emptyTagParams+ { tagName = "p"+ }+ packTree t' ps+ , If (isEndTag ["li"]) $ \t' -> do+ hasLi <- hasInListItemScope ["li"]+ if hasLi+ then do+ generate <- generateEndTags $ L.delete "li" impliedEndTags+ current <- currentNode+ let errF = if maybe True (nodeIsElement "li") current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ clear <- closeElement "li"+ packTree t' $ generate ++ errF clear+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isEndTag ["dd", "dt"]) $ \t' -> do+ let d = tokenTag t'+ hasMatch <- hasInScope [tagName d]+ if hasMatch+ then do+ generate <- generateEndTags $ L.delete (tagName d) impliedEndTags+ current <- currentNode+ let errF = if maybe True (nodeIsElement $ tagName d) current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ clear <- closeElement $ tagName d+ packTree t' $ generate ++ errF clear+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isEndTag $ map T.unpack headerNames) $ \t' -> do+ let d = tokenTag t'+ hasMatch <- hasInScope headerNames+ if hasMatch+ then do+ generate <- generateEndTags $ L.delete (tagName d) impliedEndTags+ current <- currentNode+ let errF = if maybe True (nodeIsElement $ tagName d) current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ clear <- closeElements headerNames+ packTree t' $ generate ++ errF clear+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isStartTag ["a"]) $ \t' -> do+ state <- N.S.get+ let formatting = activeFormattingElements state+ active = Y.fromMaybe [] $ Y.listToMaybe formatting+ nested <- case L.find (\f -> tagName (snd f) == "a") active of+ Just (i, _) -> do+ adopt <- consTreeError NestedElementForAdoptionAgency <$> runAdoptionAgency t'+ open' <- openElements <$> N.S.get+ close <- case break ((==) i . fst) open' of+ (_, []) -> return []+ (es1, _) -> do+ N.S.modify $ \state' -> state'+ { activeFormattingElements = case activeFormattingElements state' of+ [] -> []+ (es:ess) -> filter ((/=) i . fst) es : ess+ }+ closeAncestorNode_ . fromIntegral $ length es1+ return $ adopt |++ close+ Nothing -> packTreeErrors [] t'+ format <- reconstructFormattingElements+ insert <- insertFormattingElement t'+ return $ nested |++| format ++| insert+ , If (isStartTag+ [ "b"+ , "big"+ , "code"+ , "em"+ , "font"+ , "i"+ , "s"+ , "small"+ , "strike"+ , "strong"+ , "tt"+ , "u"+ ]) $ \t' -> do+ format <- reconstructFormattingElements+ insert <- insertFormattingElement t'+ return $ format ++| insert+ , If (isStartTag ["nobr"]) $ \t' -> do+ format <- reconstructFormattingElements+ hasNobr <- hasInScope ["nobr"]+ nested <- if hasNobr+ then do+ --BUG: Adoption agency + insertion may duplicate the errors?+ adopt <- consTreeError NestedElementForAdoptionAgency <$> runAdoptionAgency t'+ format' <- reconstructFormattingElements+ return $ adopt |++ format'+ else packTreeErrors [] t'+ insert <- insertFormattingElement t'+ return $ format ++| nested |++| insert+ , If (isEndTag+ [ "a"+ , "b"+ , "big"+ , "code"+ , "em"+ , "font"+ , "i"+ , "nobr"+ , "s"+ , "small"+ , "strike"+ , "strong"+ , "tt"+ , "u"+ ]) runAdoptionAgency+ , If (isStartTag ["applet", "marquee", "object"]) $ \t' -> do+ setFramesetNotOk+ format <- reconstructFormattingElements+ insertFormattingMarker+ insert <- insertElement t'+ return $ format ++| insert+ , If (isEndTag ["applet", "marquee", "object"]) $ \t' -> do+ let d = tokenTag t'+ hasMatch <- hasInScope [tagName d]+ if hasMatch+ then do+ generate <- generateEndTags impliedEndTags+ current <- currentNode+ let errF = if maybe True (nodeIsElement $ tagName d) current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ clear <- closeElement $ tagName d+ clearFormattingElements+ packTree t' $ generate ++ errF clear+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isStartTag ["table"]) $ \t' -> do+ hasP <- hasInButtonScope ["p"]+ quirks <- (== FullQuirks) . quirksMode <$> N.S.get+ clear <- if hasP && not quirks+ then closePElement+ else return []+ insert <- insertElement t'+ setFramesetNotOk+ switchMode InTable+ return $ clear ++| insert+ , If (isEndTag ["br"]) $ \t' -> do+ push . mapTokenErrs (BREndTag :) . flip mapTokenOut t' $+ const (StartTag $ (tokenTag t') { tagAttributes = M.empty })+ treeInBody+ , If (isStartTag ["area", "br", "embed", "img", "keygen", "wbr"]) $ \t' -> do+ setFramesetNotOk+ format <- reconstructFormattingElements+ insert <- insertNullElement t'+ return $ format ++| insert+ , If (isStartTag ["input"]) $ \t' -> do+ case fmap snd . L.find (\a -> fst a == "type") . M.toList . tagAttributes $ tokenTag t' of+ Nothing -> setFramesetNotOk+ Just "hidden" -> return ()+ Just _ -> setFramesetNotOk+ format <- reconstructFormattingElements+ insert <- insertNullElement t'+ return $ format ++| insert+ , If (isStartTag ["param", "source", "track"]) insertNullElement+ , If (isStartTag ["hr"]) $ \t' -> do+ setFramesetNotOk+ hasP <- hasInButtonScope ["p"]+ close <- if hasP+ then closePElement+ else return []+ insert <- insertNullElement t'+ return $ close ++| insert+ , If (isStartTag ["image"]) $ \t' -> do+ push . mapTokenErrs (ObsoleteTagName "image" :) . flip mapTokenOut t' $+ const (StartTag $ (tokenTag t') { tagName = "img" })+ treeInBody+ , If (isStartTag ["textarea"]) $ \t' -> do+ lineFeed <- next+ lineFeedState <- if tokenOut lineFeed == Character '\n'+ then return $ tokenState lineFeed+ else push lineFeed $> Nothing+ setFramesetNotOk+ genericRCDataElement $ mapTokenState' t' (lineFeedState <|>)+ , If (isStartTag ["xmp"]) $ \t' -> do+ setFramesetNotOk+ hasP <- hasInButtonScope ["p"]+ close <- if hasP+ then closePElement+ else return []+ format <- reconstructFormattingElements+ text <- genericRawTextElement t'+ return $ close ++ format ++| text+ , If (isStartTag ["iframe"]) $ \t' -> do+ setFramesetNotOk+ genericRawTextElement t'+ , If (isStartTag ["noembed"]) genericRawTextElement+ , If (isStartTag ["noscript"]) $ \t' -> do+ state <- N.S.get+ if scriptingEnabled state+ then genericRawTextElement t'+ else do -- "any other start tag"+ format <- reconstructFormattingElements+ insert <- insertElement t'+ return $ format ++| insert+ , If (isStartTag ["select"]) $ \t' -> do+ setFramesetNotOk+ state <- N.S.get+ switchMode $ if elem (insertionMode state)+ [ InTable+ , InCaption+ , InTableBody+ , InRow+ , InCell+ ]+ then InSelectInTable+ else InSelect+ format <- reconstructFormattingElements+ insert <- insertElement t'+ return $ format ++| insert+ , If (isStartTag ["optgroup", "option"]) $ \t' -> do+ current <- currentNode+ format <- case fmap elementName current of+ Just "option" -> do+ close <- closeCurrentNode_+ format' <- reconstructFormattingElements+ return $ close ++ format'+ _ -> reconstructFormattingElements+ insert <- insertElement t'+ return $ format ++| insert+ , If (isStartTag ["rb", "rtc"]) $ \t' -> do+ hasRuby <- hasInScope ["ruby"]+ generate <- if hasRuby+ then do+ generate' <- generateEndTags impliedEndTags+ current <- currentNode+ let errF = if maybe True (nodeIsElement "ruby") current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ return $ errF generate'+ else return []+ insert <- insertElement t'+ return $ generate ++| insert+ , If (isStartTag ["rp", "rt"]) $ \t' -> do+ hasRuby <- hasInScope ["ruby"]+ generate <- if hasRuby+ then do+ generate' <- generateEndTags $ L.delete "rtc" impliedEndTags+ current <- currentNode+ let errF = if maybe True (\e -> nodeIsElement "ruby" e || nodeIsElement "rtc" e) current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ return $ errF generate'+ else return []+ insert <- insertElement t'+ return $ generate ++| insert+ , If (isStartTag ["math"]) $ \t' -> do+ let d = tokenTag t'+ insertF+ | tagIsSelfClosing d = insertForeignNullElement+ | otherwise = insertForeignElement+ format <- reconstructFormattingElements+ insert <- insertF mathMLNamespace $ mapTokenOut (const . StartTag $ adjustMathMLAttributes d) t'+ return $ format ++| insert+ , If (isStartTag ["svg"]) $ \t' -> do+ let d = tokenTag t'+ insertF+ | tagIsSelfClosing d = insertForeignNullElement+ | otherwise = insertForeignElement+ format <- reconstructFormattingElements+ insert <- insertF svgNamespace $ mapTokenOut (const . StartTag $ adjustSvgAttributes d) t'+ return $ format ++| insert+ , If (isStartTag+ [ "caption"+ , "col"+ , "colgroup"+ , "frame"+ , "head"+ , "tbody"+ , "td"+ , "tfoot"+ , "th"+ , "thead"+ , "tr"+ ]) $ \t' ->+ packTreeErrors [UnexpectedDescendantElement $ tokenElement t'] t'+ , If isAnyStartTag $ \t' -> do+ format <- reconstructFormattingElements+ insert <- insertElement t'+ return $ format ++| insert+ , If isAnyEndTag $ \t' -> do+ state <- N.S.get+ anyOtherEndTag (openElements state) t'+ ]+ where headerNames = [ T.snoc "h" i | i <- ['1'..'6'] ]+ hasUnexpectedOpenElement =+ hasOpenElementExcept+ [ "dd"+ , "dt"+ , "li"+ , "optgroup"+ , "option"+ , "p"+ , "rb"+ , "rp"+ , "rt"+ , "rtc"+ , "tbody"+ , "td"+ , "tfoot"+ , "th"+ , "thead"+ , "tr"+ , "body"+ , "html"+ ]++-- | __HTML:__+-- the "any other end tag" entry in @[the "in body" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody)@+-- +-- Delegate a token to to the 'InBody' section of the state machine, but skip+-- the token-dependent behaviour and instead simply treat it according to the+-- fallback case for 'EndTag' tokens.+anyOtherEndTag+ :: [(NodeIndex, ElementParams)]+ -- ^ The stack of open elements.+ -> TreeInput+ -- ^ The token to process.+ -> TreeBuilder TreeOutput+anyOtherEndTag [] t' = packTreeErrors [] t'+anyOtherEndTag ((i, e):es) t' = case tokenOut t' of+ EndTag d | nodeIsElement (tagName d) e -> do+ generate <- generateEndTags $ L.delete (tagName d) impliedEndTags+ elementIndices <- map fst . openElements <$> N.S.get+ let errF = if Y.listToMaybe elementIndices == Just i+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ count = length (takeWhile (/= i) elementIndices) + 1+ clear <- clearCount $ fromIntegral count+ packTree t' $ generate ++ errF clear+ EndTag _ -> if nodeIsSpecial e+ then packTreeErrors [UnexpectedElementWithImpliedEndTag] t'+ else anyOtherEndTag es t'+ _ -> packTreeErrors [] t'+++-- | Check whether a node is in 'scopeElements' -- the tags which break+-- open-tag searches.+isScopeElement :: ElementParams -> Bool+isScopeElement d' = elem (Y.fromMaybe T.empty $ elementNamespace d', elementName d') scopeElements++-- | Check whether a node is in 'specialElements' -- the tags which are subject+-- to custom handling.+isSpecialElement :: ElementParams -> Bool+isSpecialElement d' = elem (Y.fromMaybe T.empty $ elementNamespace d', elementName d') specialElements+++-- | __HTML:__+-- the substeps spanned by the /"Loop"/ for processing @\<dd\>@ and+-- @\<dt\>@ tags in @[the "in body" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody)@+-- +-- @\<dd\>@ and @\<dt\>@ elements should close most opened elements if the+-- relevant end tag is left implied.+ddLoop+ :: [(NodeIndex, ElementParams)]+ -- ^ The stack of open elements.+ -> TreeBuilder [Patch]+ddLoop [] = return []+ddLoop ((_, e):es)+ | nodeIsElement "dd" e = do+ generate <- generateEndTags $ L.delete "dd" impliedEndTags+ current <- currentNode+ let err = case current of+ Just e' | not $ nodeIsElement "dd" e' -> consTreeError_ UnexpectedElementWithImpliedEndTag+ _ -> id+ clear <- closeElement "dd"+ return . err $ generate ++ clear+ | nodeIsElement "dt" e = do+ generate <- generateEndTags $ L.delete "dt" impliedEndTags+ current <- currentNode+ let err = case current of+ Just e' | not $ nodeIsElement "dt" e' -> consTreeError_ UnexpectedElementWithImpliedEndTag+ _ -> id+ clear <- closeElement "dt"+ return . err $ generate ++ clear+ | nodeIsElement "address" e = ddLoop es+ | nodeIsElement "div" e = ddLoop es+ | nodeIsElement "p" e = ddLoop es+ | isSpecialElement e = return []+ | otherwise = ddLoop es++-- | __HTML:__+-- the substeps spanned by the /"Loop"/ for processing @\<li\>@ tags in+-- @[the "in body" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inbody)@+-- +-- @\<li\>@ elements should close most opened elements if the relevant end tag+-- is left implied.+liLoop+ :: [(NodeIndex, ElementParams)]+ -- ^ The stack of open elements.+ -> TreeBuilder [Patch]+liLoop [] = return []+liLoop ((_, e):es)+ | nodeIsElement "li" e = do+ generate <- generateEndTags $ L.delete "li" impliedEndTags+ current <- currentNode+ let err = case current of+ Just e' | not $ nodeIsElement "li" e' -> consTreeError_ UnexpectedElementWithImpliedEndTag+ _ -> id+ clear <- closeElement "li"+ return . err $ generate ++ clear+ | nodeIsElement "address" e = liLoop es+ | nodeIsElement "div" e = liLoop es+ | nodeIsElement "p" e = liLoop es+ | isSpecialElement e = return []+ | otherwise = liLoop es+++-- | __HTML:__+-- @[adoption agency algorithm]+-- (https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm)@+-- +-- Handle a misnested formatting element by closing its ancestors up to a+-- stable branch point, and then reconstructing the tree in a more logical+-- order.+runAdoptionAgency :: TreeInput -> TreeBuilder TreeOutput+runAdoptionAgency t' = case tokenOut t' of+ StartTag d -> runAdoptionAgency' d+ EndTag d -> runAdoptionAgency' d+ _ -> packTreeErrors [] t'+ where runAdoptionAgency' d = do+ current <- currentNode+ index <- currentNodeIndex+ active <- concat . activeFormattingElements <$> N.S.get+ let isCurrent = maybe False (nodeIsElement $ tagName d) current+ inActive = Y.isJust $ L.find (\(i, _) -> Just i == index) active+ if isCurrent && not inActive+ then closeCurrentNode_ >>= packTree t'+ else runAdoptionAgencyOuterLoop 8 t' d++-- | __HTML:__+-- the substeps spanned by the /"Outer loop"/ within the+-- @[adoption agency algorithm]+-- (https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm)@+-- +-- Determine whether the current formatting element close tag is mis-nested,+-- and if so, close the element and reconstruct an equivalent formatting tree.+runAdoptionAgencyOuterLoop+ :: Word+ -- ^ The maximum number of times the loop should be run, to avoid+ -- overly-costly input. Note that this implementation counts down+ -- rather than up, and so this function should generally be passed @8@+ -- when it is originally called to implement the official behaviour.+ -> TreeInput+ -- ^ The token to process.+ -> TagParams+ -- ^ The inner element data extracted from the second parameter.+ -> TreeBuilder TreeOutput+-- 4.+runAdoptionAgencyOuterLoop 0 t' _ = packTreeErrors [] t'+runAdoptionAgencyOuterLoop i t' d = do+ _ <- error "Adoption agency not yet implemented"+ -- 6.+ state <- N.S.get+ let formatting = activeFormattingElements state+ open = openElements state+ case L.find (\f -> tagName (snd $ snd f) == tagName d) . zip [0..] $ concat formatting of+ Nothing -> anyOtherEndTag open t'+ Just (indexFormat, formattingElement) -> do+ let removeFormattingElement = N.S.put $ state+ { activeFormattingElements = map (L.delete formattingElement) formatting+ }+ -- 7.+ case L.elemIndex (fst formattingElement) (map fst open) of+ Nothing -> do+ removeFormattingElement+ packTreeErrors [IncompletelyClosedFormattingElement] t'+ Just indexOpen -> do+ -- 8. Make use of the fact we can get an index rather than+ -- just a 'Bool' when checking if the formatting element is+ -- in the stack of open elements, to check if it's in+ -- scope ('hasInScope' only checks the tag name, not UID).+ let descendants = take indexOpen open+ formattingNode = head $ drop indexOpen open+ if any (isScopeElement . snd) descendants+ then packTreeErrors [UnexpectedFormattingElementOutOfScope] t'+ else do+ -- 9.+ let overlap = if maybe False (fst formattingElement ==) . Y.listToMaybe $ map fst open+ then consTreeError_ OverlappingFormattingElements+ else id+ -- 10.+ case L.find (isSpecialElement . snd) $ reverse descendants of+ Nothing -> do+ -- 11.+ clear <- clearCount $ fromIntegral indexOpen+ close <- closeCurrentNode_+ removeFormattingElement+ packTree t' $ overlap clear ++ close+ Just furthestBlock -> do+ -- 14.+ (bookmark, lastTag) <-+ runAdoptionAgencyInnerLoop 3 indexFormat formattingElement furthestBlock furthestBlock .+ drop 1 $ dropWhile (/= furthestBlock) open+ -- 15.+ reparent <- case L.elemIndex (fst lastTag) $ map fst open of+ Nothing -> return []+ Just indexInner -> if indexInner < indexOpen+ then closeAncestorNodes_+ (fromIntegral indexInner)+ (fromIntegral $ indexOpen - indexInner)+ else return []+ -- 16.+ newElement <- createElement $ snd formattingNode+ let newTag = unpackNodeData <$> newElement+ N.S.modify $ \state' -> state'+ -- 19.+ { activeFormattingElements =+ let findBookmark _ [] = []+ findBookmark bookmark' (es:ess)+ | bookmark' > l = es : findBookmark (bookmark' - l) ess+ | otherwise = (ds ++ newTag : drop 1 as) : ess+ where l = length es+ (ds, as) = splitAt bookmark' es+ in findBookmark bookmark formatting+ -- 20.+ , openElements =+ let es = L.delete formattingNode open+ (ds, as) = break (== furthestBlock) es+ in ds ++ newElement : as+ }+ -- 21.+ recurse <- runAdoptionAgencyOuterLoop (i - 1) t' d+ packTree t' $ overlap reparent ++ treePatches recurse++-- | __HTML:__+-- the substeps spanned by the /"Inner loop"/ within the+-- @[adoption agency algorithm]+-- (https://html.spec.whatwg.org/multipage/parsing.html#adoption-agency-algorithm)@+-- +-- Close ancestor elements until reaching a stable node for the new tree, and+-- then reconstruct an equivalent tree.+runAdoptionAgencyInnerLoop+ :: Word+ -- ^ __HTML:__+ -- @inner loop counter@+ -- + -- The maximum number of times the loop should be run, to avoid+ -- overly-costly input. Note that this implementation counts down+ -- rather than up, and so this function should generally be passed @3@.+ -> Int+ -- ^ __HTML:__+ -- the bookmark noting the position of @formatting element@+ -> (NodeIndex, TagParams)+ -- ^ __HTML:__+ -- @formatting element@+ -- + -- The node selected as the base of the reparenting process.+ -> (NodeIndex, ElementParams)+ -- ^ __HTML:__+ -- @last node@+ -- + -- The child node actively being reparented.+ -> (NodeIndex, ElementParams)+ -- ^ __HTML:__+ -- @furthest block@+ -- + -- The most senior descendant of @formatting element@ which will serve+ -- as a secondary fixed point.+ -> [(NodeIndex, ElementParams)]+ -- ^ The stack of open elements; @inner node@ is derived from this.+ -> TreeBuilder (Int, (NodeIndex, TagParams))+runAdoptionAgencyInnerLoop _ bookmark formattingElement _ _ [] = return (bookmark, formattingElement)+runAdoptionAgencyInnerLoop i bookmark formattingElement lastNode furthestBlock (innerNode:ns)+ -- 4.+ | fst formattingElement == fst innerNode = return (bookmark, formattingElement)+ | otherwise = do+ state <- N.S.get+ let formatting = activeFormattingElements state+ case L.elemIndex innerTag $ concat formatting of+ Just innerIndex -> if i == 0+ -- 5. / 6.+ then do+ N.S.modify $ \state' -> state'+ --BUG: Not reflected in the patch list.+ { openElements = L.delete innerNode $ openElements state'+ , activeFormattingElements = map (L.delete innerTag) formatting+ }+ let bookmark'+ | innerIndex <= bookmark = pred bookmark+ | otherwise = bookmark+ runAdoptionAgencyInnerLoop i' bookmark' formattingElement lastNode furthestBlock ns+ else do+ -- 7.+ newElement <- createElement $ snd innerNode+ let newTag = unpackNodeData <$> newElement+ N.S.modify $ \state' -> state'+ { openElements = replaceNode newElement innerNode $ openElements state'+ , activeFormattingElements = map (replaceNode newTag innerTag) formatting+ }+ -- 8.+ let bookmark'+ | lastNode == furthestBlock =+ maybe bookmark succ . L.elemIndex newTag $ concat formatting+ | otherwise = bookmark+ -- 10. / 11.+ runAdoptionAgencyInnerLoop i' bookmark' formattingElement innerNode furthestBlock ns+ -- 6.+ Nothing -> do+ N.S.modify $ \state' -> state'+ --BUG: Not reflected in the patch list.+ { openElements = L.delete innerNode $ openElements state'+ }+ runAdoptionAgencyInnerLoop i' bookmark formattingElement lastNode furthestBlock ns+ where i' = if i == 0 then 0 else i - 1+ replaceNode newElement innerElement es = ds ++ newElement : drop 1 as+ where (ds, as) = break (== innerElement) es+ innerTag = unpackNodeData <$> innerNode++-- | Retrieve the data from the finalized form of the collection, and repack it+-- in the form expected by some of the parser combinators used by the+-- @runAdoptionAgency@ algorithm.+unpackNodeData :: ElementParams -> TagParams+unpackNodeData d = emptyTagParams+ { tagName = case elementPrefix d of+ Just prefix -> prefix <> ":" <> elementName d+ Nothing -> elementName d+ , tagAttributes = M.fromList . map unpackAttribute . toAttrList $ elementAttributes d+ -- Don't have to worry about self-closing tags, as if they're taking part+ -- in the adoption agency algorithm, they must have been able to have+ -- children.+ }+ where unpackAttribute d' = case attrPrefix d' of+ Just prefix -> (prefix <> ":" <> attrName d', attrValue d')+ Nothing -> (attrName d', attrValue d')
+ src/Web/Mangrove/Parse/Tree/InBody.hs-boot view
@@ -0,0 +1,15 @@+{-|+Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com+-}+module Web.Mangrove.Parse.Tree.InBody+ ( treeInBody+ ) where+++import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.Patch+++treeInBody :: TreeBuilder TreeOutput
+ src/Web/Mangrove/Parse/Tree/InCaption.hs view
@@ -0,0 +1,104 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within a @\<caption\>@ section describing a table.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InCaption+ ( treeInCaption+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in caption" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-incaption)@+-- +-- The parsing instructions corresponding to the 'InCaption' section of the+-- state machine.+treeInCaption :: TreeBuilder TreeOutput+treeInCaption = next >>= switch+ [ If (isEndTag ["caption"]) $ \t' -> do+ hasCaption <- hasInTableScope ["caption"]+ if hasCaption+ then do+ generate <- generateEndTags impliedEndTags+ current <- currentNode+ unexpected <- if maybe True (nodeIsElement "caption") current+ then packTree t' []+ else packTreeErrors [UnexpectedElementWithImpliedEndTag] t'+ close <- closeElement "caption"+ clearFormattingElements+ switchMode InTable+ return $ generate ++| unexpected |++ close+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isStartTag+ [ "caption"+ , "col"+ , "colgroup"+ , "tbody"+ , "td"+ , "tfoot"+ , "th"+ , "thead"+ , "tr"+ ]) $ \t' -> do+ hasCaption <- hasInTableScope ["caption"]+ if hasCaption+ then do+ push t'+ generate <- generateEndTags impliedEndTags+ current <- currentNode+ let unexpected = if maybe True (nodeIsElement "caption") current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ close <- closeElement "caption"+ clearFormattingElements+ switchMode InTable+ packTree_ $ generate ++ unexpected close+ else packTreeErrors [UnexpectedDescendantElement $ tokenElement t'] t'+ , If (isEndTag ["table"]) $ \t' -> do+ hasCaption <- hasInTableScope ["caption"]+ if hasCaption+ then do+ push t'+ generate <- generateEndTags impliedEndTags+ current <- currentNode+ let unexpected = if maybe True (nodeIsElement "caption") current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ close <- closeElement "caption"+ clearFormattingElements+ switchMode InTable+ packTree_ $ generate ++ unexpected close+ else packTreeErrors [UnexpectedDescendantElement $ tokenElement t'] t'+ , If (isEndTag+ [ "body"+ , "col"+ , "colgroup"+ , "html"+ , "tbody"+ , "td"+ , "tfoot"+ , "th"+ , "thead"+ , "tr"+ ]) $ \t' ->+ packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , Else $ \t' -> do+ push t'+ treeInBody+ ]
+ src/Web/Mangrove/Parse/Tree/InCell.hs view
@@ -0,0 +1,100 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules for a @\<td\>@ data point in a table.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InCell+ ( treeInCell+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in cell" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intd)@+-- +-- The parsing instructions corresponding to the 'InCell' section of the state+-- machine.+treeInCell :: TreeBuilder TreeOutput+treeInCell = next >>= switch+ [ If (isEndTag ["td", "th"]) $ \t' -> do+ let d = tokenTag t'+ hasMatch <- hasInTableScope [tagName d]+ if hasMatch+ then do+ generate <- generateEndTags impliedEndTags+ current <- currentNode+ let unexpected = if maybe True (nodeIsElement $ tagName d) current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ close <- closeElement $ tagName d+ clearFormattingElements+ switchMode InRow+ packTree t' $ generate ++ unexpected close+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isStartTag+ [ "caption"+ , "col"+ , "colgroup"+ , "tbody"+ , "td"+ , "tfoot"+ , "th"+ , "thead"+ , "tr"]) $ \t' -> do+ hasMatch <- hasInTableScope ["td", "th"]+ if hasMatch+ then do+ push t'+ close <- closeCell+ packTree_ close+ else packTreeErrors [MalformedTableStructure $ tokenElement t'] t'+ , If (isEndTag+ [ "body"+ , "caption"+ , "col"+ , "colgroup"+ , "html"+ ]) $ \t' ->+ packTreeErrors [UnexpectedEndTag $ tokenElement t'] t'+ , If (isEndTag+ [ "table"+ , "tbody"+ , "tfoot"+ , "thead"+ , "tr"+ ]) $ \t' -> do+ hasMatch <- hasInTableScope [tagName $ tokenTag t']+ if hasMatch+ then do+ push t'+ close <- closeCell+ packTree_ close+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , Else $ \t' -> push t' *> treeInBody+ ]+ where closeCell = do+ generate <- generateEndTags impliedEndTags+ current <- currentNode+ let errF = if elem (fmap elementName current) [Just "td", Just "th"]+ then consTreeError_ UnexpectedElementWithImpliedEndTag+ else id+ close <- closeElements ["td", "th"]+ clearFormattingElements+ switchMode InRow+ return $ generate ++ errF close
+ src/Web/Mangrove/Parse/Tree/InColumnGroup.hs view
@@ -0,0 +1,69 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within a @\<colgroup\>@ markup section.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InColumnGroup+ ( treeInColumnGroup+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in column group" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-incolgroup)@+-- +-- The parsing instructions corresponding to the 'InColumnGroup' section of the+-- state machine.+treeInColumnGroup :: TreeBuilder TreeOutput+treeInColumnGroup = next >>= switch+ [ If isWhitespace insertCharacter+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If (isStartTag ["col"]) insertNullElement+ , If (isEndTag ["colgroup"]) $ \t' -> do+ current <- currentNode+ if maybe False (nodeIsElement "colgroup") current+ then do+ switchMode InTable+ closeCurrentNode t'+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isEndTag ["col"]) $ packTreeErrors [UnexpectedEndColInColumnGroup]+ , If (isStartTag ["template"]) $ \t' -> do+ push t'+ treeInHead+ , If (isEndTag ["template"]) $ \t' -> do+ push t'+ treeInHead+ , If isEOF $ \t' -> do+ push t'+ treeInBody+ , Else $ \t' -> do+ current <- currentNode+ if maybe False (nodeIsElement "colgroup") current+ then do+ push t'+ switchMode InTable+ close <- closeCurrentNode_+ packTree_ close+ else packTreeErrors [UnexpectedElementWithImpliedEndTag] t'+ ]
+ src/Web/Mangrove/Parse/Tree/InFrameset.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within a @\<frameset\>@ section.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InFrameset+ ( treeInFrameset+ ) where+++import qualified Control.Monad as N+import qualified Control.Monad.Trans.State as N.S++import qualified Data.Maybe as Y++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in frameset" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inframeset)@+-- +-- The parsing instructions corresponding to the 'InFrameset' section of the+-- state machine.+treeInFrameset :: TreeBuilder TreeOutput+treeInFrameset = next >>= switch+ [ If isWhitespace insertCharacter+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If (isStartTag ["frameset"]) insertElement+ , If (isEndTag ["frameset"]) $ \t' -> do+ state <- N.S.get+ current <- currentNode+ close <- if length (openElements state) <= 1 && maybe True (nodeIsElement "html") current+ then packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ else closeCurrentNode t'+ current' <- currentNode+ let notFragment = Y.isNothing $ fragmentContext state+ currentFrameset = maybe False (nodeIsElement "frameset") current'+ N.when (notFragment && not currentFrameset) $ switchMode AfterFrameset+ return close+ , If (isStartTag ["frame"]) insertNullElement+ , If (isStartTag ["noframes"]) $ \t' -> do+ push t'+ treeInHead+ , If isEOF $ \t' -> do+ elements <- openElements <$> N.S.get+ let errF = if length elements > 1+ then consTreeError UnexpectedElementWithImpliedEndTag+ else id+ eof <- stopParsing t'+ return $ errF eof+ , Else $ packTreeErrors [UnexpectedNodeInFrameset]+ ]
+ src/Web/Mangrove/Parse/Tree/InHead.hs view
@@ -0,0 +1,151 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within the @\<head\>@ section.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InHead+ ( treeInHead+ ) where+++import qualified Control.Applicative as A+import qualified Control.Monad as N+import qualified Control.Monad.Trans.State as N.S++import qualified Data.Bifunctor as F.B+import qualified Data.ByteString.Short as BS.SH+import qualified Data.HashMap.Strict as M+import qualified Data.Text as T+import qualified Data.Text.Encoding as T++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InText+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Encoding+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Encoding.Labels+import Web.Willow.Common.Encoding.Sniffer+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch++import {-# SOURCE #-} Web.Mangrove.Parse.Tree.InBody++import Control.Applicative ( (<|>) )+++-- | __HTML:__+-- @[the "in head" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead)@+-- +-- The parsing instructions corresponding to the 'InHead' section of the state+-- machine.+treeInHead :: TreeBuilder TreeOutput+treeInHead = next >>= switch+ [ If isWhitespace insertCharacter+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If (isStartTag ["base", "basefont", "bgsound", "link"]) insertNullElement+ , If (isStartTag ["meta"]) $ \t' -> do+ insert <- insertNullElement t'+ change' <- A.optional $ changeEncoding t'+ return $ case change' of+ Just change -> insert |++| change+ Nothing -> insert+ , If (isStartTag ["title"]) genericRCDataElement+ , If (isStartTag ["noscript"]) $ \t' -> do+ state <- N.S.get+ if scriptingEnabled state+ then genericRawTextElement t'+ else do+ switchMode InHeadNoscript+ insertElement t'+ , If (isStartTag ["noframes", "style"]) genericRawTextElement+ , If (isStartTag ["script"]) $ \t' -> do+ N.S.modify $ \state -> state+ { originalInsertionMode = Just $ insertionMode state+ }+ switchMode InText+ insert <- insertElement . mapTokenState t' $ \state -> state+ { currentState = ScriptDataState+ }+ return insert+ , If (isEndTag ["head"]) $ \t' -> do+ switchMode AfterHead+ packTree t' softCloseCurrentNode_+ , If (isEndTag ["body", "html", "br"]) anythingElse+ , If (isStartTag ["template"]) $ \t' -> do+ insert <- insertElement t'+ insertFormattingMarker+ setFramesetNotOk+ switchMode InTemplate+ pushTemplateMode InTemplate+ return insert+ , If (isEndTag ["template"]) $ \t' -> do+ generate <- generateEndTags thoroughlyImpliedEndTags+ current <- currentNode+ let errF = case nodeIsElement "template" <$> current of+ Just True -> id+ _ -> consTreeError_ UnexpectedElementWithImpliedEndTag+ clear <- closeElement "template"+ clearFormattingElements+ popTemplateMode+ resetInsertionMode+ packTree t' $ errF generate ++ clear+ , If (isStartTag ["head"]) $ packTreeErrors [NestedSingletonElement]+ , If isAnyEndTag $ \t' ->+ packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , Else anythingElse+ ]+ where anythingElse t' = do+ push t'+ switchMode AfterHead+ packTree_ softCloseCurrentNode_+ changeEncoding t' = do+ let d = tokenTag t'+ state = tokenState t' >>= decoderState . fst+ enc' <- maybe A.empty return $ do+ e <- changeEncodingCharset d <|> changeEncodingContentType d+ return $ case e of+ Utf16be -> Utf8+ Utf16le -> Utf8+ UserDefined -> Windows1252+ enc -> enc+ case state of+ Just s -> case decoderEncoding s of+ Utf16be -> putDecoderState t' $ setEncodingCertain Utf16be s+ Utf16le -> putDecoderState t' $ setEncodingCertain Utf16be s+ enc | enc == enc' -> putDecoderState t' $ setEncodingCertain enc' s+ _ -> case decoderConfidence s of+ Tentative _ rec | not $ encodingEquivalent rec enc' -> do+ _ <- putDecoderState t' $ initialDecoderState enc'+ restartParsing $ streamStart rec+ _ -> putDecoderState t' $ initialDecoderState enc'+ Nothing -> putDecoderState t' $ initialDecoderState enc'+ changeEncodingCharset d = do+ charset <- M.lookup "charset" $ tagAttributes d+ lookupEncoding charset+ changeEncodingContentType d = do+ httpEquiv <- M.lookup "http-equiv" $ tagAttributes d+ N.unless (T.map toAsciiLower httpEquiv == "content-type") A.empty+ content <- M.lookup "content" $ tagAttributes d+ extractEncoding $ T.encodeUtf8 content+ encodingEquivalent rec enc = uncurry (==) .+ F.B.bimap (map $ parseChar enc) (map T.singleton) . unzip . M.toList $ parsedChars rec+ parseChar enc = fst . decode' (initialDecoderState enc) . BS.SH.fromShort+ putDecoderState t' decState =+ flip packTree [] . mapTokenState' t' . fmap . F.B.first $ \state -> state+ { decoderState_ = Right $ Just decState+ }
+ src/Web/Mangrove/Parse/Tree/InHeadNoscript.hs view
@@ -0,0 +1,67 @@+{-|+Description: Token processing rules within a @\<noscript\>@ section of the @\<head\>@.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InHeadNoscript+ ( treeInHeadNoscript+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in head noscript" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inheadnoscript)@+-- +-- The parsing instructions corresponding to the 'InHeadNoscript' section of+-- the state machine.+treeInHeadNoscript :: TreeBuilder TreeOutput+treeInHeadNoscript = next >>= switch+ [ If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If (isEndTag ["noscript"]) $ \t' -> do+ switchMode InHead+ closeCurrentNode t'+ , If isWhitespace $ \t' -> do+ push t'+ treeInHead+ , If isComment $ \t' -> do+ push t'+ treeInHead+ , If (isStartTag+ [ "basefont"+ , "bgsound"+ , "link"+ , "meta"+ , "noframes"+ , "style"+ ]) $ \t' -> do+ push t'+ treeInHead+ , If (isEndTag ["br"]) anythingElse+ , If (isStartTag ["head", "noscript"]) $ packTreeErrors [NestedSingletonElement]+ , If isAnyEndTag $ \t' ->+ packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , Else anythingElse+ ]+ where anythingElse t' = do+ push t'+ switchMode InHead+ close <- closeCurrentNode_+ packTree_ $ consTreeError_ UnexpectedNodeInHeadNoscript close
+ src/Web/Mangrove/Parse/Tree/InRow.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules for a @\<tr\>@ data segment in a table.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InRow+ ( treeInRow+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InTable+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in row" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intr)@+-- +-- The parsing instructions corresponding to the 'InRow' section of the state+-- machine.+treeInRow :: TreeBuilder TreeOutput+treeInRow = next >>= switch+ [ If (isStartTag ["th", "td"]) $ \t' -> do+ switchMode InCell+ clear <- clearToContext tableRowContext+ insertFormattingMarker+ insert <- insertElement t'+ return $ clear ++| insert+ , If (isEndTag ["tr"]) $ \t' -> do+ hasTr <- hasInTableScope ["tr"]+ if hasTr+ then do+ switchMode InTableBody+ close <- closeTableRow+ packTree t' close+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isStartTag+ [ "caption"+ , "col"+ , "colgroup"+ , "tbody"+ , "tfoot"+ , "thead"+ , "tr"+ ]) $ \t' -> do+ hasTr <- hasInTableScope ["tr"]+ if hasTr+ then do+ push t'+ switchMode InTableBody+ close <- closeTableRow+ packTree_ close+ else packTreeErrors [MalformedTableStructure $ tokenElement t'] t'+ , If (isEndTag ["table"]) $ \t' -> do+ hasTr <- hasInTableScope ["tr"]+ if hasTr+ then do+ push t'+ switchMode InTableBody+ close <- closeTableRow+ packTree_ close+ else packTreeErrors [MalformedTableStructure $ tokenElement t'] t'+ , If (isEndTag ["tbody", "tfoot", "thead"]) $ \t' -> do+ hasMatch <- hasInTableScope [tagName $ tokenTag t']+ if hasMatch+ then do+ push t'+ switchMode InTableBody+ close <- closeTableRow+ packTree_ close+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isEndTag+ [ "body"+ , "caption"+ , "col"+ , "colgroup"+ , "html"+ , "td"+ , "th"+ ]) $ \t' ->+ packTreeErrors [UnexpectedEndTag $ tokenElement t'] t'+ , Else $ \t' -> do+ push t'+ treeInTable+ ]+ where closeTableRow = do+ clear <- clearToContext tableRowContext+ close <- closeCurrentNode_+ return $ clear ++ close
+ src/Web/Mangrove/Parse/Tree/InSelect.hs view
@@ -0,0 +1,119 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within a @\<select\>@ set of choices for user input.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InSelect+ ( treeInSelect+ ) where+++import qualified Control.Monad.Trans.State as N.S++import qualified Data.Maybe as Y++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in select" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselect)@+-- +-- The parsing instructions corresponding to the 'InSelect' section of the+-- state machine.+treeInSelect :: TreeBuilder TreeOutput+treeInSelect = next >>= switch+ [ If isNull $ packTreeErrors [UnexpectedNullCharacter]+ , If isCharacter insertCharacter+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["html"]) $ \t' -> do+ push t'+ treeInBody+ , If (isStartTag ["option"]) $ \t' -> do+ current <- currentNode+ close <- if maybe False (nodeIsElement "option") current+ then closeCurrentNode_+ else return []+ insert <- insertElement t'+ return $ close ++| insert+ , If (isStartTag ["optgroup"]) $ \t' -> do+ current <- currentNode+ option <- if maybe False (nodeIsElement "option") current+ then closeCurrentNode_+ else return []+ current' <- currentNode+ optgroup <- if maybe False (nodeIsElement "optgroup") current'+ then closeCurrentNode_+ else return []+ insert <- insertElement t'+ return $ option ++ optgroup ++| insert+ , If (isEndTag ["optgroup"]) $ \t' -> do+ current <- currentNode+ second <- fmap snd . Y.listToMaybe . drop 1 . openElements <$> N.S.get+ let firstIsOption = maybe False (nodeIsElement "option") current+ secondIsOptgroup = maybe False (nodeIsElement "optgroup") second+ option <- if firstIsOption && secondIsOptgroup+ then closeCurrentNode_+ else return []+ current' <- currentNode+ optgroup <- if maybe False (nodeIsElement "optgroup") current'+ then closeCurrentNode t'+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ return $ option ++| optgroup+ , If (isEndTag ["option"]) $ \t' -> do+ current <- currentNode+ if maybe False (nodeIsElement "option") current+ then closeCurrentNode t'+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isEndTag ["select"]) $ \t' -> do+ hasSelect <- hasInSelectScope ["select"]+ if hasSelect+ then do+ close <- closeElement "select"+ resetInsertionMode+ packTree t' close+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isStartTag ["select"]) $ \t' -> do+ hasSelect <- hasInSelectScope ["select"]+ consTreeError NestedNonRecursiveElement <$> if hasSelect+ then do+ close <- closeElement "select"+ resetInsertionMode+ packTree t' close+ else packTreeErrors [] t'+ , If (isStartTag ["input", "keygen", "textarea"]) $ \t' -> do+ hasSelect <- hasInSelectScope ["select"]+ consTreeError OverlappingInputElements <$> if hasSelect+ then do+ push t'+ close <- closeElement "select"+ resetInsertionMode+ packTree_ close+ else packTreeErrors [] t'+ , If (isStartTag ["script", "template"]) $ \t' -> do+ push t'+ treeInHead+ , If (isEndTag ["template"]) $ \t' -> do+ push t'+ treeInHead+ , If isEOF $ \t' -> do+ push t'+ treeInBody+ , Else $ \t' ->+ packTreeErrors [UnexpectedDescendantElement $ tokenElement t'] t'+ ]
+ src/Web/Mangrove/Parse/Tree/InSelectInTable.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within a @\<select\>@ input, which is contained within a table.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InSelectInTable+ ( treeInSelectInTable+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.Patch+import Web.Mangrove.Parse.Tree.InSelect+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in select in table" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inselectintable)@+-- +-- The parsing instructions corresponding to the 'InSelectInTable' section of the+-- state machine.+treeInSelectInTable :: TreeBuilder TreeOutput+treeInSelectInTable = next >>= switch+ [ If (isStartTag+ [ "caption"+ , "table"+ , "tbody"+ , "tfoot"+ , "thead"+ , "tr"+ , "td"+ , "th"+ ]) $ \t' -> do+ push t'+ close <- closeElement "select"+ resetInsertionMode+ packTree_ . consTreeError_ (MalformedTableStructure $ tokenElement t') $ close+ , If (isEndTag+ [ "caption"+ , "table"+ , "tbody"+ , "tfoot"+ , "thead"+ , "tr"+ , "td"+ , "th"+ ]) $ \t' -> do+ hasMatch <- hasInTableScope [tagName $ tokenTag t']+ if hasMatch+ then do+ push t'+ close <- closeElement "select"+ resetInsertionMode+ packTree_ close+ else packTreeErrors [UnexpectedElementWithImpliedEndTag] t'+ , Else $ \t' -> push t' *> treeInSelect+ ]
+ src/Web/Mangrove/Parse/Tree/InTable.hs view
@@ -0,0 +1,167 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within a @\<table\>@ markup section.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InTable+ ( treeInTable+ , anythingElse+ ) where+++import qualified Control.Monad.Trans.State as N.S++import qualified Data.HashMap.Strict as M+import qualified Data.List as L+import qualified Data.Maybe as Y++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InBody+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in table" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intable)@+-- +-- The parsing instructions corresponding to the 'InTable' section of the state+-- machine.+treeInTable :: TreeBuilder TreeOutput+treeInTable = next >>= switch+ [ If isCharacter $ \t' -> do+ current <- currentNode+ let toTableText = do+ N.S.modify $ \state -> state+ { insertionMode = InTableText+ , originalInsertionMode = Just $ insertionMode state+ }+ push t'+ switchMode InTableText+ packTree_ []+ case fmap elementName current of+ Just "table" -> toTableText+ Just "tbody" -> toTableText+ Just "tfoot" -> toTableText+ Just "thead" -> toTableText+ Just "tr" -> toTableText+ _ -> anythingElse t'+ , If isComment insertComment+ , If isDoctype $ \t' ->+ packTreeErrors [UnexpectedDoctype $ tokenDocumentType t'] t'+ , If (isStartTag ["caption"]) $ \t' -> do+ switchMode InCaption+ clear <- clearToContext tableContext+ insertFormattingMarker+ insert <- insertElement t'+ return $ clear ++| insert+ , If (isStartTag ["colgroup"]) $ \t' -> do+ switchMode InColumnGroup+ clear <- clearToContext tableContext+ insert <- insertElement t'+ return $ clear ++| insert+ , If (isStartTag ["col"]) $ \t' -> do+ push t'+ switchMode InColumnGroup+ clear <- clearToContext tableContext+ insert <- insertElement_ $ emptyTagParams+ { tagName = "colgroup"+ }+ packTree_ $ clear ++ insert+ , If (isStartTag ["tbody", "tfoot", "thead"]) $ \t' -> do+ switchMode InTableBody+ clear <- clearToContext tableContext+ insert <- insertElement t'+ return $ clear ++| insert+ , If (isStartTag ["td", "th", "tr"]) $ \t' -> do+ push t'+ switchMode InTableBody+ clear <- clearToContext tableContext+ insert <- insertElement_ $ emptyTagParams+ { tagName = "tbody"+ }+ packTree_ $ clear ++ insert+ , If (isStartTag ["table"]) $ \t' -> do+ hasTable <- hasInTableScope ["table"]+ if hasTable+ then do+ push t'+ close <- closeTable+ packTree_ close+ else packTreeErrors [NestedNonRecursiveElement] t'+ , If (isEndTag ["table"]) $ \t' -> do+ hasTable <- hasInTableScope ["table"]+ if hasTable+ then fmap (|++) (packTreeErrors [] t') <*> closeTable+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isEndTag+ [ "body"+ , "caption"+ , "col"+ , "colgroup"+ , "html"+ , "tbody"+ , "td"+ , "tfoot"+ , "th"+ , "thead"+ , "tr"+ ]) $ \t' ->+ packTreeErrors [UnexpectedDescendantElement $ tokenElement t'] t'+ , If (isStartTag ["style", "script", "template"]) $ \t' -> do+ push t'+ treeInHead+ , If (isEndTag ["template"]) $ \t' -> do+ push t'+ treeInHead+ , If (isStartTag ["input"]) $ \t' ->+ case L.find (== "type") . map fst . M.toList . tagAttributes $ tokenTag t' of+ Just "hidden" -> consTreeError UnexpectedElementInTableStructure <$> insertNullElement t'+ _ -> anythingElse t'+ , If (isStartTag ["form"]) $ \t' -> do+ state <- N.S.get+ let hasTemplate = any (nodeIsElement "template" . snd) $ openElements state+ if hasTemplate || Y.isJust (formElementPointer state)+ then packTreeErrors [UnexpectedElementInTableStructure] t'+ else do+ N.S.modify $ \state' -> state'+ { formElementPointer = Just $ elementIndex state'+ }+ insertNullElement t'+ , If isEOF $ \t' -> do+ push t'+ treeInBody+ , Else anythingElse+ ]+ where closeTable = closeElement "table" <* resetInsertionMode+++-- | __HTML:__+-- the "anything else" entry in @[the "in table" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intable)@+-- +-- Delegate a token to to the 'InTable' section of the state machine, but skip+-- the token-dependent behaviour and instead simply treat it according to the+-- fallback case.+anythingElse :: TreeInput -> TreeBuilder TreeOutput+anythingElse t' = do+ _ <- error "Foster parenting not yet implemented"+ N.S.modify $ \state -> state+ { fosteringEnabled = True+ }+ push t'+ out <- treeInBody+ N.S.modify $ \state -> state+ { fosteringEnabled = False+ }+ return $ consTreeError UnexpectedNodeInTableStructure out
+ src/Web/Mangrove/Parse/Tree/InTableBody.hs view
@@ -0,0 +1,99 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules for the primary content of a @\<table\>@.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InTableBody+ ( treeInTableBody+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InTable+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "in table body" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intbody)@+-- +-- The parsing instructions corresponding to the 'InTableBody' section of the+-- state machine.+treeInTableBody :: TreeBuilder TreeOutput+treeInTableBody = next >>= switch+ [ If (isStartTag ["tr"]) $ \t' -> do+ switchMode InRow+ clear <- clearToContext tableBodyContext+ insert <- insertElement t'+ return $ clear ++| insert+ , If (isStartTag ["th", "td"]) $ \t' -> do+ push t'+ switchMode InRow+ clear <- clearToContext tableBodyContext+ insert <- fmap (consTreeError_ UnexpectedTableCellOutsideOfRow) . insertElement_ $ emptyTagParams+ { tagName = "tr"+ }+ packTree_ $ clear ++ insert+ , If (isEndTag ["tbody", "tfoot", "thead"]) $ \t' -> do+ hasTable <- hasInTableScope [tagName $ tokenTag t']+ if hasTable+ then do+ switchMode InTable+ close <- closeTableBody+ packTree t' close+ else packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If (isStartTag+ [ "caption"+ , "col"+ , "colgroup"+ , "tbody"+ , "tfoot"+ , "thead"+ ]) $ \t' -> do+ hasMatch <- hasInTableScope ["tbody", "thead", "tfoot"]+ if hasMatch+ then do+ push t'+ switchMode InTable+ close <- closeTableBody+ packTree_ close+ else packTreeErrors [UnexpectedDescendantElement $ tokenElement t'] t'+ , If (isEndTag ["table"]) $ \t' -> do+ hasMatch <- hasInTableScope ["tbody", "thead", "tfoot"]+ if hasMatch+ then do+ push t'+ switchMode InTable+ close <- closeTableBody+ packTree_ close+ else packTreeErrors [UnexpectedDescendantElement $ tokenElement t'] t'+ , If (isEndTag+ [ "body"+ , "caption"+ , "col"+ , "colgroup"+ , "html"+ , "td"+ , "th"+ , "tr"+ ]) $ \t' ->+ packTreeErrors [UnexpectedEndTag $ tokenElement t'] t'+ , Else $ \t' -> do+ push t'+ treeInTable+ ]+ where closeTableBody = do+ clear <- clearToContext tableBodyContext+ close <- closeCurrentNode_+ return $ clear ++ close
+ src/Web/Mangrove/Parse/Tree/InTableText.hs view
@@ -0,0 +1,75 @@+{-|+Description: Token processing rules for content misnested within a @\<table\>@.+Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InTableText+ ( treeInTableText+ ) where+++import qualified Data.Foldable as D+import qualified Data.List as L+import qualified Data.Tuple.HT as U.HT++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InTable+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch++import {-# SOURCE #-} Web.Mangrove.Parse.Tree.Dispatcher+++-- | __HTML:__+-- @[the "in table text" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intabletext)@+-- +-- The parsing instructions corresponding to the 'InTableText' section of the+-- state machine.+treeInTableText :: TreeBuilder TreeOutput+treeInTableText = do+ resetMode+ pending <- treeInTableText'+ let processF = if not $ all (maybe True isAsciiWhitespace . U.HT.snd3) pending+ then anythingElse+ else insertCharacter+ ps <- D.foldrM (repack processF) [] pending+ start <- packTree_ []+ return $ L.foldr foldOut start ps+ where repack _ ([], Nothing, _) [] = return []+ repack _ (errs, Nothing, _) [] = pure . consTreeErrors errs <$> dispatchHtml+ repack _ (errs, Nothing, _) (t:ts) = return $ consTreeErrors errs t : ts+ repack f (errs, Just c, state) ts = (: ts) <$> f (dummyStateToken errs (Character c) state)+ foldOut ps ps' = ps' { treePatches = treePatches ps ++ treePatches ps' }+ consTreeErrors = flip $ foldr consTreeError++-- | __HTML:__+-- @[the "in table" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intable)@+-- +-- The parsing instructions corresponding to the 'InTableText' section of the+-- state machine. Specifically, this consumes all following 'Character' tokens+-- to construct the list of [pending table character tokens]+-- (https://html.spec.whatwg.org/multipage/parsing.html#concept-pending-table-char-tokens),+-- while the surrounding 'treeInTableText' is what actually sends them to the+-- 'InTable' state for insertion.+treeInTableText' :: TreeBuilder [([ParseError], Maybe Char, TokenizerOutputState)]+treeInTableText' = next >>= switch+ [ If isNull $ \t' -> do+ cs <- treeInTableText'+ return $ (UnexpectedNullCharacter : tokenErrs t', Nothing, tokenState t') : cs+ , If isCharacter $ \t' -> do+ cs <- treeInTableText'+ return $ (tokenErrs t', tokenCharacter t', tokenState t') : cs+ , Else $ \t' -> do+ push t'+ return []+ ]
+ src/Web/Mangrove/Parse/Tree/InTemplate.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules within a @\<template\>@ section providing a fragment for script processing.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InTemplate+ ( treeInTemplate+ ) where+++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.InHead+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch++import {-# SOURCE #-} Web.Mangrove.Parse.Tree.InBody+++-- | __HTML:__+-- @[the "in template" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-intemplate)@+-- +-- The parsing instructions corresponding to the 'InTemplate' section of the+-- state machine.+treeInTemplate :: TreeBuilder TreeOutput+treeInTemplate = next >>= switch+ [ If isCharacter $ \t' -> do+ push t'+ treeInBody+ , If isComment $ \t' -> do+ push t'+ treeInBody+ , If isDoctype $ \t' -> do+ push t'+ treeInBody+ , If (isStartTag+ [ "base"+ , "basefont"+ , "bgsound"+ , "link"+ , "meta"+ , "noframes"+ , "script"+ , "style"+ , "template"+ , "title"+ ]) $ \t' -> do+ push t'+ treeInHead+ , If (isEndTag ["template"]) $ \t' -> do+ push t'+ treeInHead+ , If (isStartTag+ [ "caption"+ , "colgroup"+ , "tbody"+ , "tfoot"+ , "thead"+ ]) $ \t' -> do+ push t'+ _ <- popTemplateMode+ pushTemplateMode InTable+ switchMode InTable+ packTree_ []+ , If (isStartTag ["col"]) $ \t' -> do+ push t'+ popTemplateMode+ pushTemplateMode InColumnGroup+ switchMode InColumnGroup+ packTree_ []+ , If (isStartTag ["tr"]) $ \t' -> do+ push t'+ popTemplateMode+ pushTemplateMode InTableBody+ switchMode InTableBody+ packTree_ []+ , If (isStartTag ["td", "th"]) $ \t' -> do+ push t'+ popTemplateMode+ pushTemplateMode InRow+ switchMode InRow+ packTree_ []+ , If isAnyStartTag $ \t' -> do+ push t'+ popTemplateMode+ pushTemplateMode InBody+ switchMode InBody+ packTree_ []+ , If isAnyEndTag $ \t' ->+ packTreeErrors [UnmatchedEndTag $ tokenElement t'] t'+ , If isEOF $ \t' -> do+ hasTemplate <- hasOpenElement ["template"]+ if hasTemplate+ then do+ push t'+ close <- closeElement "template"+ clearFormattingElements+ popTemplateMode+ resetInsertionMode+ packTree_ close+ else stopParsing t'+ ]
+ src/Web/Mangrove/Parse/Tree/InText.hs view
@@ -0,0 +1,88 @@+{-|+Description: Token processing rules for spans of raw character strings.++Copyright: (c) 2020-2021 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.InText+ ( treeInText+ , genericRawTextElement+ , genericRCDataElement+ ) where+++import qualified Control.Monad.Trans.State as N.S++import qualified Data.Maybe as Y++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "text" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-incdata)@+-- +-- The parsing instructions corresponding to the 'InText' section of the state+-- machine.+treeInText :: TreeBuilder TreeOutput+treeInText = next >>= switch+ [ If isCharacter insertCharacter+ , If isEOF $ \t' -> do+ push t'+ N.S.modify $ \state -> state+ { insertionMode = Y.fromMaybe InText $ originalInsertionMode state+ , originalInsertionMode = Nothing+ }+ close <- consTreeError_ EOFInText <$> closeCurrentNode_+ packTree_ close+ , If (isEndTag ["script"]) $ \t' -> do+ resetMode+ close <- closeCurrentNode_+ packTree t' close+ , If isAnyEndTag $ \t' -> do+ resetMode+ closeCurrentNode t'+ ]+++-- | __HTML:__+-- @[generic raw text element parsing algorithm]+-- (https://html.spec.whatwg.org/multipage/parsing.html#generic-raw-text-element-parsing-algorithm)@+-- +-- Insert an element containing unescaped plain text.+genericRawTextElement :: TreeInput -> TreeBuilder TreeOutput+genericRawTextElement = genericParsingAlgorithm RawTextState++-- | __HTML:__+-- @[generic RCDATA element parsing algorithm]+-- (https://html.spec.whatwg.org/multipage/parsing.html#generic-rcdata-element-parsing-algorithm)@+-- +-- Insert an element containing plain text, potentially with character+-- references.+genericRCDataElement :: TreeInput -> TreeBuilder TreeOutput+genericRCDataElement = genericParsingAlgorithm RCDataState++-- | __HTML:__+-- @[parsing elements that contain only text]+-- (https://html.spec.whatwg.org/multipage/parsing.html#parsing-elements-that-contain-only-text)@+-- +-- The actual algorithm described for 'genericRawTextElement' and+-- 'genericRCDataElement', with all variables exported.+genericParsingAlgorithm :: CurrentTokenizerState -> TreeInput -> TreeBuilder TreeOutput+genericParsingAlgorithm mode t' = do+ N.S.modify $ \state -> state+ { insertionMode = InText+ , originalInsertionMode = Just $ insertionMode state+ }+ insertElement . mapTokenState t' $ \state -> state+ { currentState = mode+ }
+ src/Web/Mangrove/Parse/Tree/Initial.hs view
@@ -0,0 +1,136 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: Token processing rules before the doctype declaration.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: stable+Portability: portable+-}+module Web.Mangrove.Parse.Tree.Initial+ ( treeInitial+ ) where+++import qualified Data.Maybe as Y+import qualified Data.Text as T++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Mangrove.Parse.Tree.Patch+import Web.Willow.Common.Encoding.Character+import Web.Willow.Common.Parser+import Web.Willow.Common.Parser.Switch+++-- | __HTML:__+-- @[the "initial" insertion mode]+-- (https://html.spec.whatwg.org/multipage/parsing.html#the-initial-insertion-mode)@+-- +-- The parsing instructions corresponding to the 'Initial' section of the+-- state machine.+treeInitial :: TreeBuilder TreeOutput+treeInitial = next >>= switch+ [ If isWhitespace $ packTreeErrors []+ , If isComment $ insertComment' InDocument+ , If isDoctype $ \t' -> do+ switchMode BeforeHtml+ doctype <- insertDoctype t'+ srcdoc <- inIFrameSrcDoc+ quirks <- setDocumentQuirks . requiredQuirks srcdoc $ tokenDoctype t'+ return $ doctype |++ quirks+ , Else $ \t' -> do+ push t'+ switchMode BeforeHtml+ srcdoc <- inIFrameSrcDoc+ ps <- if srcdoc+ then return []+ else do+ quirks <- setDocumentQuirks FullQuirks+ return $ consTreeError_ MissingDoctype quirks+ packTree_ ps+ ]++-- | Compare the addresses in the document type declaration to known values+-- which should trigger the backwards-compatible rendering mode.+requiredQuirks :: Bool -> DoctypeParams -> QuirksMode+requiredQuirks True _ = FullQuirks+requiredQuirks False d+ | doctypeQuirks d = FullQuirks+ | Y.isJust name && name /= Just "html" = FullQuirks+ | public == "-//W3O//DTD W3 HTML STRICT 3.0//EN//" = FullQuirks+ | public == "-/W3C/DTD HTML 4.0 TRANSITIONAL/EN" = FullQuirks+ | public == "HTML" = FullQuirks+ | system == "HTTP://WWW.IBM.COM/DATA/DTD/V11/IBMXHTML1-TRANSITIONAL.DTD" = FullQuirks+ | startsWith "+//SILMARIL//DTD HTML PRO V0R11 19970101//" public = FullQuirks+ | startsWith "-//AS//DTD HTML 3.0 ASWEDIT + EXTENSIONS//" public = FullQuirks+ | startsWith "//ADVASOFT LTD//DTD HTML 3.0 ASWEDIT + EXTENSIONS//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 2.0 LEVEL 1//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 2.0 LEVEL 2//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 2.0 STRICT LEVEL 1//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 2.0 STRICT LEVEL 2//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 2.0 STRICT//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 2.0//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 2.1E//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 3.0//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 3.2 FINAL//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 3.2//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML 3//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML LEVEL 0//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML LEVEL 1//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML LEVEL 2//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML LEVEL 3//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML STRICT LEVEL 0//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML STRICT LEVEL 1//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML STRICT LEVEL 2//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML STRICT LEVEL 3//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML STRICT//" public = FullQuirks+ | startsWith "-//IETF//DTD HTML//" public = FullQuirks+ | startsWith "-//METRIUS//DTD METRIUS PRESENTATIONAL//" public = FullQuirks+ | startsWith "-//MICROSOFT//DTD INTERNET EXPLORER 2.0 HTML STRICT//" public = FullQuirks+ | startsWith "-//MICROSOFT//DTD INTERNET EXPLORER 2.0 HTML//" public = FullQuirks+ | startsWith "-//MICROSOFT//DTD INTERNET EXPLORER 2.0 TABLES//" public = FullQuirks+ | startsWith "-//MICROSOFT//DTD INTERNET EXPLORER 3.0 HTML STRICT//" public = FullQuirks+ | startsWith "-//MICROSOFT//DTD INTERNET EXPLORER 3.0 HTML//" public = FullQuirks+ | startsWith "-//MICROSOFT//DTD INTERNET EXPLORER 3.0 TABLES//" public = FullQuirks+ | startsWith "-//NETSCAPE COMM. CORP.//DTD HTML//" public = FullQuirks+ | startsWith "-//NETSCAPE COMM. CORP.//DTD STRICT HTML//" public = FullQuirks+ | startsWith "-//O'REILLY AND ASSOCIATES//DTD HTML 2.0//" public = FullQuirks+ | startsWith "-//O'REILLY AND ASSOCIATES//DTD HTML EXTENDED 1.0//" public = FullQuirks+ | startsWith "-//O'REILLY AND ASSOCIATES//DTD HTML EXTENDED RELAXED 1.0//" public = FullQuirks+ | startsWith "-//SQ//DTD HTML 2.0 HOTMETAL + EXTENSIONS//" public = FullQuirks+ | startsWith "-//SOFTQUAD SOFTWARE//DTD HOTMETAL PRO 6.0::19990601::EXTENSIONS TO HTML 4.0//" public = FullQuirks+ | startsWith "-//SOFTQUAD//DTD HOTMETAL PRO 4.0::19971010::EXTENSIONS TO HTML 4.0//" public = FullQuirks+ | startsWith "-//SPYGLASS//DTD HTML 2.0 EXTENDED//" public = FullQuirks+ | startsWith "-//SUN MICROSYSTEMS CORP.//DTD HOTJAVA HTML//" public = FullQuirks+ | startsWith "-//SUN MICROSYSTEMS CORP.//DTD HOTJAVA STRICT HTML//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML 3 1995-03-24//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML 3.2 DRAFT//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML 3.2 FINAL//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML 3.2//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML 3.2S DRAFT//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML 4.0 FRAMESET//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML 4.0 TRANSITIONAL//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML EXPERIMENTAL 19960712//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML EXPERIMENTAL 970421//" public = FullQuirks+ | startsWith "-//W3C//DTD W3 HTML//" public = FullQuirks+ | startsWith "-//W3O//DTD W3 HTML 3.0//" public = FullQuirks+ | startsWith "-//WEBTECHS//DTD MOZILLA HTML 2.0//" public = FullQuirks+ | startsWith "-//WEBTECHS//DTD MOZILLA HTML//" public = FullQuirks+ | startsWith "-//W3C//DTD HTML 4.01 FRAMESET//" public && Y.isNothing system' = FullQuirks+ | startsWith "-//W3C//DTD HTML 4.01 TRANSITIONAL//" public && Y.isNothing system' = FullQuirks+ | startsWith "-//W3C//DTD XHTML 1.0 FRAMESET//" public = LimitedQuirks+ | startsWith "-//W3C//DTD XHTML 1.0 TRANSITIONAL//" public = LimitedQuirks+ | startsWith "-//W3C//DTD HTML 4.01 FRAMESET//" public = LimitedQuirks+ | startsWith "-//W3C//DTD HTML 4.01 TRANSITIONAL//" public = LimitedQuirks+ | otherwise = NoQuirks+ where name = doctypeName d+ public = maybe T.empty (T.map toAsciiUpper) public'+ public' = doctypePublicId d+ system = maybe T.empty (T.map toAsciiUpper) system'+ system' = doctypeSystemId d+ startsWith = T.isPrefixOf
+ src/Web/Mangrove/Parse/Tree/Patch.hs view
@@ -0,0 +1,860 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Description: A stateless reformulation of the components of the tree construction algorithm.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: provisional+Portability: portable++This module provides the data structures used in the second half of this+implementation's split+__[HTML](https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)__+tree construction algorithm, and the common functions generating them. While+it would certainly be possible to implement the specification more directly,+the instructions it provides are not only (very strongly) build around mutable+data structures, they're also tailored to a pass-by-reference core library.+Haskell idiomatically being neither, it's actually easier (and likely more+performant, though that's not been tested) to implement a fourth stage from+scratch.++In effect, the main parser body handles all stateful computation ---tracking+the stack of open elements, managing transitions through the finite state+machine, etc.--- but doesn't know the shape of the document tree beyond its+very narrow window. Instead, it emits a self-contained sequence of+instructions which can be very simply consumed with neither (much) further+modification nor external state; this then is the source from which the final+document tree is built. Some duplication /is/ admittedly involved in doing so,+as the emitted instruction is typically accompanied by a corresponding change+to the parser state, but the relative logic simplicity makes doing so worth it.+-}+module Web.Mangrove.Parse.Tree.Patch+ ( -- * Types+ Patch ( .. )+ , TreeOutput ( .. )+ , treeRemainder+ , TokenizerOutputState+ , InsertAt ( .. )+ , TargetNode+ , ReparentDepth+ -- * Utility functions+ -- ** Tree types+ -- $utility-state+ , packTree+ , packTree_+ , packTreeErrors+ , packTreeErrors_+ , consTreeError+ , consTreeError_+ , (++|)+ , (|++)+ , (|++|)+ -- ** Token types+ , mapTokenErrs+ , mapTokenOut+ -- * Instructions+ , setDocumentQuirks+ , restartParsing+ , stopParsing+ -- ** Opening nodes+ , insertCharacter+ , insertComment+ , insertComment'+ , insertDoctype+ , addAttribute+ -- *** Elements+ , createElement+ , insertElement+ , insertElement_+ , insertNullElement+ , insertNullElement_+ , insertHeadElement+ , insertHeadElement_+ -- **** Formatting+ , insertForeignElement+ , insertForeignNullElement+ , insertFormattingElement+ , reconstructFormattingElements+ -- ** Closing nodes+ -- *** Single+ , closeCurrentNode+ , closeCurrentNode_+ , dropCurrentNode+ , softCloseCurrentNode_+ , closeAncestorNode_+ , closeAncestorNodes_+ -- *** Multiple+ -- **** Exclusive clear+ , clearToContext+ , tableContext+ , tableBodyContext+ , tableRowContext+ -- **** Inclusive clear+ , clearCount+ , closeElement+ , closeElements+ , closePElement+ , generateEndTags+ , impliedEndTags+ , thoroughlyImpliedEndTags+ ) where+++import qualified Control.Applicative as A+import qualified Control.Monad as N+import qualified Control.Monad.Trans.State as N.S++import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BS.L+import qualified Data.IntMap.Strict as M.I+import qualified Data.List as L+import qualified Data.Maybe as Y+import qualified Data.Text as T++import Data.Function ( (&) )+import Data.Functor ( ($>) )++import Web.Willow.DOM++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize as Tokenize+import Web.Mangrove.Parse.Tokenize.Common+import Web.Mangrove.Parse.Tree.Common+import Web.Willow.Common.Encoding+++-- | The atomic, self-contained instruction set describing how to build a final+-- document tree.+data Patch+ = ErrorList [ParseError]+ -- ^ "Ignore the token", or add errors to, e.g., 'SetDocumentQuirks'.+ | SetDocumentQuirks QuirksMode+ -- ^ Specify which degree of backwards compatibility should be used in+ -- rendering the document.+ | CloseNodes (M.I.IntMap ReparentDepth)+ -- ^ "Pop the current node off the stack of open elements."+ -- + -- The first 'TargetNode' of every tuple is treated identically to+ -- 'RelativeLocation', while the second 'ReparentDepth' indicates how+ -- many ancestors should be closed (i.e., @'CloseNodes' [(1, 1)]@+ -- closes the parent node).+ | SoftCloseCurrentNode+ -- ^ Float any following non-'Element' nodes (until the next+ -- 'CloseNodes' or 'DropCurrentNode') to the parent rather than+ -- inserting them under the current.+ | DropCurrentNode+ -- ^ "Remove the current node from its parent node, if it has one. Pop+ -- it off the stack of open elements."+ | InsertCharacter [ParseError] Char+ -- ^ "Insert the character."+ | InsertComment [ParseError] InsertAt T.Text+ -- ^ "Insert a comment."+ | InsertElement [ParseError] ElementParams+ -- ^ "Insert a foreign element for the token, in the given namespace."+ | InsertAndSetDocumentType [ParseError] DocumentTypeParams+ -- ^ "Append a 'DocumentType' node to the @Document@ node. Associate+ -- the former with the latter so that it is returned as the value of+ -- the @doctype@ attribute."+ -- + -- While the specification requires the 'DocumentType' be explicitly+ -- associated as the @doctype@ attribute of the document, the parsing+ -- rules only allow for a single such node to be produced, so the extra+ -- processing isn't actually required.+ | AddAttribute InsertAt AttributeParams+ -- ^ "Add the attribute and its corresponding value to the top element+ -- of the stack of open elements."+ | RestartParsing+ -- ^ Discard the previous tree and begin a new, empty one.+ deriving ( Eq, Show, Read )+++-- | Some 'Patch'es represent instruction categories which may not always be+-- intended for the current location in the tree, and the resulting 'Tree's may+-- likewise migrate from where they're generated. These are the "addresses"+-- which direct those datatypes through the hierarchy.+data InsertAt+ = RelativeLocation ReparentDepth+ -- ^ Insert a specific number of nodes /up/ the tree from where the+ -- 'Patch' or 'Tree' was generated.+ | InDocument+ -- ^ Insert as a direct child of the 'Web.Willow.IDL.Document.Document'+ -- itself.+ | InHtmlElement+ -- ^ Insert as a direct child of the top-level @html@ element in the+ -- document.+ deriving ( Eq, Show, Read )+++-- | Type-level clarification for a count of how many levels a 'Patch' should+-- rise in the hierarchy without affecting the intervening nodes; a value of+-- @0@ indicates the current node.+type TargetNode = Word++-- | Type-level clarification for how many levels up the hierarchy the+-- associated object should be moved. Note that a value of @0@ won't result in+-- any noticable change.+type ReparentDepth = Word+++-- | The standard output of parsers used in the first tree construction stage.+-- Specifically, it contains the final state of the tokenization stage in+-- addition to the generated instructions, to enable the recursion loop to+-- detect the end of multi-token outputs and properly update the resume state.+data TreeOutput = TreeOutput+ { treePatches :: [Patch]+ -- ^ The instructions generated by the parser.+ , treeState :: TokenizerOutputState+ -- ^ The data required to resume tokenization immediately following the+ -- value, if possible.+ }+ deriving ( Eq, Show, Read )++-- | The unparsed portion of the binary stream, after building the+-- associated instruction set.+treeRemainder :: TreeOutput -> Maybe BS.ByteString+treeRemainder = fmap snd . treeState+++-- $utility-state+-- Many functions producing 'TreeOutput' objects have two forms: one taking a+-- 'TreeInput' (typically passed via the enclosing+-- 'Web.Willow.Common.Parser.Switch.switch') alongside some data to pack, and+-- one with only the latter. That 'TreeInput' is the means by which errors+-- from earlier stages are propagated forward, and by which the support+-- framework identifies breakpoints in the tokenizer. Therefore, most case+-- logic must be written to:+-- +-- * Emit the value returned by a function referencing the 'TreeInput',+-- unless that function is a 'Web.Willow.Common.Parser.push' to allow it to+-- be reconsumed.+-- * Only use the 'TreeInput' with a single function to avoid duplicating+-- errors.+-- +-- In many cases, it may be possible to pass the 'TreeInput' to one of+-- several functions. So long as the above rules are followed, it often+-- doesn't matter which function performs the wrapping; attaching it in a way+-- that makes logical or proximal sense could provide clearer error display+-- positioning.+++-- | Emit a patchset from a tree construction parser, referencing the state+-- when the original token was produced.+-- +-- This produces a stateful output, and should only be used where the token+-- would otherwise be discarded; use 'packTree_' elsewhere.+packTree :: TreeInput -> [Patch] -> TreeBuilder TreeOutput+packTree t' ps = return $ TreeOutput+ { treePatches = ps+ , treeState = tokenState t'+ }++-- | Emit a patchset from a tree construction parser without any reference to+-- the original token.+-- +-- This produces a stateless output, and should only be used after the token+-- has been 'Web.Willow.Common.Parser.push'ed for reconsumption; use 'packTree'+-- elsewhere.+packTree_ :: [Patch] -> TreeBuilder TreeOutput+packTree_ ps = return $ TreeOutput+ { treePatches = ps+ , treeState = Nothing+ }+++-- | Modify the collection of errors associated with a wrapped token.+mapTokenErrs :: ([ParseError] -> [ParseError]) -> TreeInput -> TreeInput+mapTokenErrs f t' = t'+ { tokenErrs = f $ tokenErrs t'+ }++-- | Modify a wrapped token without affecting the associated data.+mapTokenOut :: (Token -> Token) -> TreeInput -> TreeInput+mapTokenOut f t' = t'+ { tokenOut = f $ tokenOut t'+ }+++-- | Emit a collection of errors from a tree construction parser, if and only+-- if that collection is non-empty.+-- +-- This produces a stateful output, and should only be used where the token+-- would otherwise be discarded; use 'packTreeErrors_' elsewhere.+packTreeErrors :: [ParseError] -> TreeInput -> TreeBuilder TreeOutput+packTreeErrors errs t' = do+ ps <- packTreeErrors_ $ errs ++ tokenErrs t'+ return $ ps+ { treeState = tokenState t'+ }++-- | Emit a collection of errors from a tree construction parser, if and only+-- if that collection is non-empty.+-- +-- This produces a stateless output, and should only be used after the token+-- has been 'Web.Willow.Common.Parser.push'ed for reconsumption; use+-- 'packTreeErrors' elsewhere, or 'consTreeError' and 'consTreeError_' if a+-- relevant set of patches already exists.+packTreeErrors_ :: [ParseError] -> TreeBuilder TreeOutput+packTreeErrors_ [] = packTree_ []+packTreeErrors_ errs = packTree_ [ErrorList errs]++-- | Prepend an error to the first patch in the list, or add an 'ErrorList'+-- entry if it doesn't support them.+-- +-- If this is the only patchset which may be generated, use 'packTreeErrors_'+-- rather than passing a null @['Patch']@.+consTreeError_ :: ParseError -> [Patch] -> [Patch]+consTreeError_ err (ErrorList errs:ps) =+ ErrorList (err : errs) : ps+consTreeError_ err (InsertCharacter errs c:ps) =+ InsertCharacter (err : errs) c : ps+consTreeError_ err (InsertComment errs loc d:ps) =+ InsertComment (err : errs) loc d : ps+consTreeError_ err (InsertElement errs d:ps) =+ InsertElement (err : errs) d : ps+consTreeError_ err (InsertAndSetDocumentType errs d:ps) =+ InsertAndSetDocumentType (err : errs) d : ps+consTreeError_ err ps = ErrorList [err] : ps++-- | Prepend an error to the wrapped patchset, adding an 'ErrorList' entry if+-- the first patch doesn't support them.+-- +-- If this is the only patchset which may be generated, use 'packTreeErrors'+-- rather than passing a null @['Patch']@.+consTreeError :: ParseError -> TreeOutput -> TreeOutput+consTreeError err out = out { treePatches = consTreeError_ err $ treePatches out }+++-- | Prepend a plain patchset to the contents of a wrapped one.+(++|) :: [Patch] -> TreeOutput -> TreeOutput+ps ++| out = out { treePatches = ps ++ treePatches out }+infixr 4 ++|++-- | Append a plain patchset to the contents of a wrapped one.+(|++) :: TreeOutput -> [Patch] -> TreeOutput+out |++ ps = out { treePatches = treePatches out ++ ps }+infixr 4 |++++-- | Concatenate the payloads of two wrapped patchsets, retaining the+-- associated state of the right one.+(|++|) :: TreeOutput -> TreeOutput -> TreeOutput+ps |++| ps' = ps' { treePatches = treePatches ps ++ treePatches ps' }+infixr 4 |++|+++-- | Set the degree of backwards compatibility the document seems to be written+-- for.+setDocumentQuirks :: QuirksMode -> TreeBuilder [Patch]+setDocumentQuirks quirks = do+ N.S.modify $ \state -> state+ { quirksMode = quirks+ }+ return [SetDocumentQuirks quirks]+++-- | Pass the stack of open elements into the given function in order to+-- determine how many elements should be closed, then close them.+clear :: ([ElementParams] -> Word) -> TreeBuilder [Patch]+clear f = do+ state <- N.S.get+ clearCount . f . map snd $ openElements state++-- | Pop a known number of nodes from the stack of open elements.+clearCount :: Word -> TreeBuilder [Patch]+clearCount 0 = return []+clearCount l = closeAncestorNodes_ 0 l+++-- | Close all markup tags until the current node is one of the given elements+-- in the HTML namespace, close it. For the inverse (closing all tags which+-- /are/ listed), see 'generateEndTags'.+-- +-- 'tableContext', 'tableBodyContext', and 'tableRowContext' provide the+-- typical inputs.+clearToContext :: [ElementName] -> TreeBuilder [Patch]+clearToContext ns = clear countToContext+ where countToContext [] = 0+ countToContext (d:ds) = if elem (elementName d) ns && elementNamespace d == Just htmlNamespace+ then 0+ else succ $ countToContext ds++-- | __HTML:__+-- @[clear the stack back to a table context]+-- (https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-context)@+-- +-- The list of elements to pass 'clearToContext' when closing all tags until+-- the start of the most recent table.+tableContext :: [ElementName]+tableContext =+ [ "table"+ , "template"+ , "html"+ ]++-- | __HTML:__+-- @[clear the stack back to a table body context]+-- (https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-body-context)@+-- +-- The list of elements to pass 'clearToContext' when closing all tags until+-- the start of the most recent section of the current table.+tableBodyContext :: [ElementName]+tableBodyContext =+ [ "tbody"+ , "tfoot"+ , "thead"+ , "template"+ , "html"+ ]++-- | __HTML:__+-- @[clear the stack back to a table row context]+-- (https://html.spec.whatwg.org/multipage/parsing.html#clear-the-stack-back-to-a-table-row-context)@+-- +-- The list of elements to pass 'clearToContext' when closing all tags until+-- the start of the most recent row of the current table.+tableRowContext :: [ElementName]+tableRowContext =+ [ "tr"+ , "template"+ , "html"+ ]+++-- | "Pop elements from the stack of open elements until the given element in+-- the HTML namespace has been popped from the stack."+-- +-- If multiple elements may indicate an endpoint, use 'closeElements' instead.+closeElement :: ElementName -> TreeBuilder [Patch]+closeElement = closeElements . (: [])++-- | "Pop elements from the stack of open elements until one of the given+-- elements in the HTML namespace has been popped from the stack."+-- +-- If only a single element may indicate an endpoint, 'closeElement' could+-- provide a cleaner interface.+closeElements :: [ElementName] -> TreeBuilder [Patch]+closeElements names = clear countToElement+ where countToElement [] = 0+ countToElement (d:ds) = if elem (elementName d) names && elementNamespace d == Just htmlNamespace+ then 1+ else succ $ countToElement ds++-- | "Pop the current element from the stack of open elements."+-- +-- This produces a stateful output, and should only be used where the token+-- would otherwise be discarded; use 'closeCurrentNode_' elsewhere.+closeCurrentNode :: TreeInput -> TreeBuilder TreeOutput+closeCurrentNode t' = do+ close <- closeCurrentNode_+ state <- N.S.get+ return $ TreeOutput+ { treePatches = foldr consTreeError_ close $ tokenErrs t'+ , treeState = tokenState . mapTokenState t' $ resetNamespace state+ }+ where resetNamespace state tokState = tokState+ { currentNodeNamespace = Y.listToMaybe (openElements state) >>= elementNamespace . snd+ }++-- | "Pop the current element from the stack of open elements."+-- +-- This produces a stateless output, and should only be used where the token is+-- handled in another manner; use 'closeCurrentNode' elsewhere. This has+-- identical behaviour to @'closeAncestorNode_' 0@.+closeCurrentNode_ :: TreeBuilder [Patch]+closeCurrentNode_ = closeAncestorNode_ 0++-- | As 'closeCurrentNode_', but the closed node and its descendants are not+-- retained in the document tree.+dropCurrentNode :: TreeBuilder [Patch]+dropCurrentNode = closeCurrentNode_ $> [DropCurrentNode]++-- | Partially close the current node, such that 'insertElement' (and related+-- element instructions) will still be inserted as children, but /every other/+-- type of node will instead be inserted as siblings. This doesn't actually+-- change the internal state; a following call to 'closeCurrentNode' or similar+-- is still required.+softCloseCurrentNode_ :: [Patch]+softCloseCurrentNode_ = [SoftCloseCurrentNode]+++-- | Remove the node the specified number of ancestors up the tree from the+-- stack of open elements. See 'closeCurrentNode_' for the special case where+-- the argument is @0@.+closeAncestorNode_ :: TargetNode -> TreeBuilder [Patch]+closeAncestorNode_ = flip closeAncestorNodes_ 1++-- | Remove several nodes the specified number of ancestors up the tree from+-- the stack of open elements. See 'closeAncestorNode_' if only a single node+-- needs to be closed.+closeAncestorNodes_ :: TargetNode -> ReparentDepth -> TreeBuilder [Patch]+closeAncestorNodes_ l d = do+ state <- N.S.get+ let (es1, es2) = splitAt (fromIntegral l) $ openElements state+ d' = fromIntegral d+ N.S.put $ state+ { openElements = es1 ++ drop d' es2+ }+ return [CloseNodes . M.I.singleton (fromIntegral l) . fromIntegral . min d' $ length es2]+++-- | Close all markup tags up to and including the most recent @\<p\>@ element,+-- throwing an 'UnexpectedElementWithImpliedEndTag' if one of them does not+-- typically allow an implied end tag. If the error is not required,+-- 'closeElement' provides similar behaviour.+closePElement :: TreeBuilder [Patch]+closePElement = do+ generate <- generateEndTags $ L.delete "p" impliedEndTags+ current <- currentNode+ let errF = if maybe True (nodeIsElement "p") current+ then id+ else consTreeError_ UnexpectedElementWithImpliedEndTag+ p <- closeElement "p"+ return $ generate ++ errF p++-- | Close all markup tags in a given list, until reaching an element which is+-- not in that list. For the inverse (closing all tags /except/ what's+-- listed), see 'clearToContext'.+-- +-- 'impliedEndTags' and 'thoroughlyImpliedEndTags' provide the typical inputs.+generateEndTags :: [ElementName] -> TreeBuilder [Patch]+generateEndTags tags = clear countImpliable+ where countImpliable [] = 0+ countImpliable (d:ds) = if elem (elementName d) tags+ then succ $ countImpliable ds+ else 0++-- | __HTML:__+-- @[generate implied end tags]+-- (https://html.spec.whatwg.org/multipage/parsing.html#generate-implied-end-tags)@+-- +-- The list of elements to pass 'generateEndTags' when closing all markup tags+-- which typically allow their end tag to be omitted.+impliedEndTags :: [ElementName]+impliedEndTags =+ [ "dd"+ , "dt"+ , "li"+ , "optgroup"+ , "option"+ , "p"+ , "rb"+ , "rp"+ , "rt"+ , "rtc"+ ]++-- | __HTML:__+-- @[generate all implied end tags thoroughly]+-- (https://html.spec.whatwg.org/multipage/parsing.html#generate-all-implied-end-tags-thoroughly)@+-- +-- The list of elements to pass 'generateEndTags' when closing all markup tags,+-- including ones which may not usually allow their end tag to be omitted.+thoroughlyImpliedEndTags :: [ElementName]+thoroughlyImpliedEndTags =+ [ "caption"+ , "colgroup"+ , "tbody"+ , "td"+ , "tfoot"+ , "th"+ , "thead"+ , "tr"+ ] ++ impliedEndTags+++-- | __HTML:__+-- @[create an element for the token]+-- (https://html.spec.whatwg.org/multipage/parsing.html#create-an-element-for-the-token)@+-- +-- Reserve a place in the tree for a given node, but do not yet add it. Note+-- that this does not otherwise affect the parser state, so any modification of+-- open elements or updating of the 'currentNodeNamespace', for example, needs+-- to be performed manually. For that reason, 'insertElement_' is almost+-- always more desirable.+createElement :: ElementParams -> TreeBuilder (NodeIndex, ElementParams)+createElement d = do+ state <- N.S.get+ let index = elementIndex state+ N.S.put $ state+ { elementIndex = succ index+ }+ return (index, d)+++-- | __HTML:__+-- @[insert a character]+-- (https://html.spec.whatwg.org/multipage/parsing.html#insert-a-character)@+-- steps 1-3+-- +-- If the wrapped token is a 'Character', add it to the final tree; fails if+-- it's not. Note that the concatenation of character into 'Text' nodes occurs+-- in the patch folding logic instead.+insertCharacter :: TreeInput -> TreeBuilder TreeOutput+insertCharacter t' = case tokenOut t' of+ Character c -> return $ TreeOutput+ { treePatches = [InsertCharacter (tokenErrs t') c]+ , treeState = tokenState t'+ }+ _ -> A.empty++-- | __HTML:__+-- @[insert a comment]+-- (https://html.spec.whatwg.org/multipage/parsing.html#insert-a-comment)@+-- with no explicit insertion postion+-- +-- If the wrapped token is a 'Tokenize.Comment', add it to the final tree at the current+-- position (@'RelativeLocation' 0@); fails if it's not.+insertComment :: TreeInput -> TreeBuilder TreeOutput+insertComment = insertComment' $ RelativeLocation 0++-- | __HTML:__+-- @[insert a comment]+-- (https://html.spec.whatwg.org/multipage/parsing.html#insert-a-comment)@+-- +-- If the wrapped token is a 'Tokenize.Comment', add it to the final tree at+-- the position specified; fails if it's not.+insertComment' :: InsertAt -> TreeInput -> TreeBuilder TreeOutput+insertComment' at t' = case tokenOut t' of+ Tokenize.Comment c -> return $ TreeOutput+ { treePatches = [InsertComment (tokenErrs t') at c]+ , treeState = tokenState t'+ }+ _ -> A.empty++-- | If the wrapped token is a 'Tokenize.Comment', add it to the final tree;+-- fails if it's not.+insertDoctype :: TreeInput -> TreeBuilder TreeOutput+insertDoctype t' = case tokenOut t' of+ Doctype d ->+ let system = doctypeSystemId d+ legacy+ | doctypeName d /= Just "html" = True+ | Y.isJust $ doctypePublicId d = True+ | Y.isJust system && system /= Just "about:legacy-compat" = True+ | otherwise = False+ errs'+ | legacy = LegacyDoctype : tokenErrs t'+ | otherwise = tokenErrs t'+ in packTree t' [InsertAndSetDocumentType errs' $ tokenDocumentType t']+ _ -> A.empty++-- | __HTML:__+-- @[insert an HTML element]+-- (https://html.spec.whatwg.org/multipage/parsing.html#insert-an-html-element)@+-- +-- If the wrapped token is a 'StartTag', add it to the final tree as a markup+-- element in the HTML namespace; fails if it's not.+-- +-- For tag data generated /a priori/, use 'insertElement_' instead.+insertElement :: TreeInput -> TreeBuilder TreeOutput+insertElement = insertForeignElement htmlNamespace++-- | Add a markup element described by the input record to the tree in the HTML+-- namespace.+-- +-- If the tag data was obtained from the tokenizer, use 'insertElement' instead.+insertElement_ :: TagParams -> TreeBuilder [Patch]+insertElement_ d = treePatches <$> insertElement (dummyToken [] $ StartTag d)++-- | __HTML:__+-- @[insert a foreign element]+-- (https://html.spec.whatwg.org/multipage/parsing.html#insert-a-foreign-element)@+-- +-- If the wrapped token is a 'StartTag', add it to the final tree as a markup+-- element in the specified namespace; fails if it's not.+insertForeignElement :: Namespace -> TreeInput -> TreeBuilder TreeOutput+insertForeignElement ns t' = case tokenOut t' of+ StartTag d -> do+ let d' = packNodeData (Just ns) d+ errs'+ | tagIsSelfClosing d = NonVoidHtmlElementStartTagWithTrailingSolidus : tokenErrs t'+ | otherwise = tokenErrs t'+ e <- createElement d'+ N.S.modify $ \state -> state+ { openElements = e : openElements state+ }+ return $ TreeOutput+ { treePatches = [InsertElement errs' $ adjustAttributes d']+ , treeState = tokenState $ mapTokenState t' setNamespace+ }+ _ -> A.empty+ where adjustAttributes+ | ns == htmlNamespace = id+ | otherwise = adjustForeignAttributes+ setNamespace state = state+ { currentNodeNamespace = Just ns+ }++-- | If the wrapped token is a 'StartTag', add it to the final tree as a markup+-- element in the HTML namespace, and then immediately close it; fails if it's+-- not.+-- +-- For tag data generated /a priori/, use 'insertNullElement_' instead.+insertNullElement :: TreeInput -> TreeBuilder TreeOutput+insertNullElement = insertForeignNullElement htmlNamespace++-- | Add a markup element described by the input record to the tree in the HTML+-- namespace, and then immediately close it.+-- +-- If the tag data was obtained from the tokenizer, use 'insertNullElement'+-- instead.+insertNullElement_ :: TagParams -> TreeBuilder [Patch]+insertNullElement_ d = treePatches <$> insertNullElement (dummyToken [] $ StartTag d)++-- | If the wrapped token is a 'StartTag', add it to the final tree as a markup+-- element in the specified namespace, and then immediately close it; fails if+-- it's not.+insertForeignNullElement :: Namespace -> TreeInput -> TreeBuilder TreeOutput+insertForeignNullElement ns t' = case tokenOut t' of+ StartTag d -> return $ TreeOutput+ { treePatches =+ [ InsertElement (tokenErrs t') . adjustAttributes $ packNodeData (Just ns) d+ , CloseNodes $ M.I.singleton 0 1+ ]+ , treeState = tokenState t'+ }+ _ -> A.empty+ where adjustAttributes+ | ns == htmlNamespace = id+ | otherwise = adjustForeignAttributes+++-- | If the wrapped token is a 'StartTag', add it to the final tree as a markup+-- element in the HTML namespace and set it as the target of the head element+-- pointer; fails if it's not.+-- +-- For tag data generated /a priori/, use 'insertHeadElement_' instead.+insertHeadElement :: TreeInput -> TreeBuilder TreeOutput+insertHeadElement t' = do+ N.S.modify $ \state -> state+ { headElementPointer = Just $ elementIndex state+ }+ insertElement t'++-- | Add a markup element described by the input record to the tree in the HTML+-- namespace and set it as the target of the head element pointer.+-- +-- If the tag data was obtained from the tokenizer, use 'insertElement' instead.+insertHeadElement_ :: TagParams -> TreeBuilder [Patch]+insertHeadElement_ d = do+ N.S.modify $ \state -> state+ { headElementPointer = Just $ elementIndex state+ }+ insertElement_ d+++-- | Add an extra point of metadata to the indicated markup element, if that+-- element doesn't already have an attribute with that name.+addAttribute :: InsertAt -> NodeIndex -> BasicAttribute -> TreeBuilder [Patch]+addAttribute at i (name, value) = do+ state <- N.S.get+ let (es1, es2) = L.break ((==) i . fst) $ openElements state+ case es2 of+ ((_, e):es') | not (elem name . map attrName . toAttrList $ elementAttributes e) -> do+ N.S.put $ state+ { openElements = es1 ++ (i, addAttributeData e) : es'+ }+ return [AddAttribute at attr]+ _ -> return []+ where addAttributeData d = d+ { elementAttributes = insertAttribute attr $ elementAttributes d+ }+ attr = emptyAttributeParams+ { attrName = name+ , attrValue = value+ }+++-- | __HTML:__+-- @[push onto the list of active formatting elements]+-- (https://html.spec.whatwg.org/multipage/parsing.html#push-onto-the-list-of-active-formatting-elements)@+-- +-- Add a markup element described by the input record to the tree in the HTML+-- namespace. The element is also added to the set of elements which are+-- recreated on overlapping markup spans via 'reconstructFormattingElements'.+insertFormattingElement :: TreeInput -> TreeBuilder TreeOutput+insertFormattingElement t' = do+ insert <- insertElement t'+ state <- N.S.get+ case Y.listToMaybe $ openElements state of+ Just e -> case tokenOut t' of+ StartTag d -> N.S.put $ state+ { activeFormattingElements =+ pushFormattingElement (fst e, d) $ activeFormattingElements state+ }+ _ -> return ()+ Nothing -> return ()+ return insert+ where pushFormattingElement e [] = [[e]]+ pushFormattingElement e (fs:fss) = (e : fs') : fss+ where fs' = case L.findIndices (equalElement . snd) fs of+ (_:_:i:is) -> case splitAts (i : is) fs of+ [] -> []+ (ds:dss) -> ds ++ concatMap (drop 1) dss+ _ -> fs+ splitAts [] dss = [dss]+ splitAts (i:is) dss =+ let (ds', dss') = splitAt i dss+ in ds' : splitAts (map (`subtract` i) is) dss'+ equalElement e' =+ tagName (snd e) == tagName e'+ -- This is only ever invoked on HTML elements, so the+ -- namespaces shouldn't need to be checked for equality.+ && tagAttributes (snd e) == tagAttributes e'++-- | __HTML:__+-- @[reconstruct the active formatting elements]+-- (https://html.spec.whatwg.org/multipage/parsing.html#reconstruct-the-active-formatting-elements)@+-- +-- Create new tokens for each of the yet-unclosed elements created by+-- 'insertFormattingElement', within the scope defined by+-- 'insertFormattingMarker'.+reconstructFormattingElements :: TreeBuilder [Patch]+reconstructFormattingElements = do+ state <- N.S.get+ let open = map fst $ openElements state+ (es, ess) = case activeFormattingElements state of+ [] -> ([], [])+ (es':ess') -> (es', ess')+ (toRebuild, alreadyOpened) = span (\(i, _) -> notElem i open) es+ N.S.put $ state+ { activeFormattingElements = alreadyOpened : ess+ }+ N.foldM reconstruct [] $ reverse toRebuild+ where reconstruct pss (_, d) = do+ ps <- treePatches <$> insertFormattingElement (dummyToken [] $ StartTag d)+ return $ pss ++ ps+++-- | __HTML:__+-- @[change the encoding]+-- (https://html.spec.whatwg.org/multipage/parsing.html#stop-parsing)@,+-- step 6+-- +-- Restore the tracked state to pristine condition, issue an instruction to+-- throw out any 'Tree' generated thus far, and set the given binary stream to+-- be used when the parser next resumes.+restartParsing :: BS.L.ByteString -> TreeBuilder TreeOutput+restartParsing initial = do+ N.S.put $ treeParserState defState+ return $ TreeOutput+ { treePatches = [RestartParsing]+ , treeState = Just (tokState, BS.L.toStrict initial)+ }+ where updateDecoder state = tokenizerState state &+ case decoderState_ $ tokenizerState state of+ Left initialize -> tokenizerEncoding $ fmap Just initialize+ Right decState -> tokenizerEncoding . Right $ fmap decoderEncoding decState+ defState = defaultTreeState+ tokState = updateDecoder defState++-- | __HTML:__+-- @[stop parsing]+-- (https://html.spec.whatwg.org/multipage/parsing.html#stop-parsing)@+-- +-- Close all remaining open elements, and perform other cleanup functions to+-- finalize the document tree.+stopParsing :: TreeInput -> TreeBuilder TreeOutput+stopParsing t' = clear (fromIntegral . length) >>= packTree t'
+ src/Web/Mangrove/Parse/Tree/Patch/Fold.hs view
@@ -0,0 +1,274 @@+{-|+Description: Functions to collapse a state-free instruction list into a document tree.++Copyright: (c) 2020 Sam May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: provisional+Portability: portable++This module provides the logic powering the second half of this+implementation's split+__[HTML](https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)__+tree construction algorithm. Namely, the functions in this module operate over+the single-dimensional stream of static tree-building instructions generated by+the rest of the "Web.Mangrove.Parse.Tree" hierarchy, folding them into a+simplified DOM tree. For a more detailed discussion of the design behind this,+see the documentation of "Web.Mangrove.Parse.Tree.Patch".+-}+module Web.Mangrove.Parse.Tree.Patch.Fold+ ( buildTree+ ) where+++import qualified Data.Bifunctor as F.B+import qualified Data.Either as E+import qualified Data.HashMap.Strict as M+import qualified Data.IntMap.Strict as M.I+import qualified Data.List as L+import qualified Data.Maybe as Y+import qualified Data.Text as T++import Web.Willow.DOM++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tree.Common hiding ( Token ( .. ) )+import Web.Mangrove.Parse.Tree.Patch+++-- | Fold a series of instructions describing how to build a document tree+-- (without reference to any persistent state) into the tree they describe.+buildTree :: [Patch] -> Tree+buildTree = buildTree' . foldPatchset+ where buildTree' (ts, []) = emptyTree+ { node = Document mode'+ , children = ts'+ }+ where (ms, ts') = L.partition (\t -> nodeType (node t) == Just DocumentNode) ts+ mode' = foldr max NoQuirks $ Y.mapMaybe (getQuirksMode . node) ms+ getQuirksMode (Document mode) = Just mode+ getQuirksMode _ = Nothing+ buildTree' (_, RestartParsing : ps) = buildTree' $ foldPatchset ps+ buildTree' (ts, ps) = buildTree' . F.B.first (ts ++) $ foldPatchset ps++-- | Consume the next minimal sequence of folding instructions which would+-- result in at least one complete tree. The input patchset is read according+-- to a sensible behaviour for trees providing a root for the document+-- (typically the /only/ root).+foldPatchset :: [Patch] -> ([Tree], [Patch])+foldPatchset ps = F.B.first (joinTexts . Y.mapMaybe filterNull) $ foldPatchset' False ps+ where -- Preprocess the placeholder tuples, where the 'InsertAt' carries the+ -- semantic data rather than the 'Tree'.+ filterNull (Right (_, t)) = Just $ Right t+ filterNull (Left c) = Just $ Left c++-- | Consume the next sequence of folding instructions which would result in a+-- complete tree. Any intervening instructions which are destined for other+-- parts of the final tree are extracted for re-direction upward. Any textual+-- data to be inserted directly into the tree is returned as a 'Left' value for+-- later packing into a single 'Text' node, to avoid the exponential complexity+-- of, e.g., @'Data.Text'.'T.cons'@.+foldPatchset'+ :: Bool+ -- ^ Whether this function call originates from somewhere higher up the+ -- document tree; any explicit calls will almost always use 'False'.+ -> [Patch]+ -> ([Either ([ParseError], Char) (InsertAt, Tree)], [Patch])+foldPatchset' _ [] = ([], [])+foldPatchset' isInner (ErrorList _:ps) = foldPatchset' isInner ps+-- Break infinite loop on unmatched node-closing patches.+foldPatchset' False (CloseNodes _:ps) = foldPatchset' False ps+foldPatchset' False (SoftCloseCurrentNode:ps) = foldPatchset' False ps+foldPatchset' False (DropCurrentNode:ps) = foldPatchset' False ps+foldPatchset' False (RestartParsing:ps) = foldPatchset' False ps+-- Stop processing on node-closing patches to enable recursive consumption of+-- all patches until the end of the node.+foldPatchset' True ps@(DropCurrentNode:_) = ([], ps)+foldPatchset' True ps@(RestartParsing:_) = ([], ps)+-- Push a "close ancestors" along until it reaches one of the node-closing+-- patches; at that point, drop the payload destined for the current location.+foldPatchset' True (p@(CloseNodes ls):ps) = case decrementReparenting ls of+ _ | M.I.null ls -> foldPatchset' True ps+ (0, toFloat) -> case ps of+ [] -> ([], [p])+ (CloseNodes ls':ps') ->+ foldPatchset' True $ CloseNodes (M.I.unionWith (+) ls ls') : ps'+ (DropCurrentNode:ps') ->+ ([], DropCurrentNode : CloseNodes toFloat : ps')+ (RestartParsing:_) ->+ ([], ps)+ (p'@InsertElement{}:ps') ->+ foldPatchset' True $ p' : CloseNodes (incrementReparenting ls) : ps'+ (p':ps') ->+ foldPatchset' True $ p' : p : ps'+ _ -> ([], p : ps)+-- Simple patch -> node translation.+foldPatchset' isInner (SetDocumentQuirks mode:ps) =+ F.B.first (Right (InDocument, packQuirks mode) :) $ foldPatchset' isInner ps+foldPatchset' isInner (InsertAndSetDocumentType _ d:ps) =+ F.B.first (Right (InDocument, packDoctype d) :) $ foldPatchset' isInner ps+foldPatchset' isInner (InsertComment _ loc txt:ps) =+ F.B.first (Right (loc, packComment txt) :) $ foldPatchset' isInner ps+foldPatchset' isInner (AddAttribute loc attr:ps) =+ F.B.first (Right (loc, packAttribute attr) :) $ foldPatchset' isInner ps+-- Prepends to the following text rather than the spec's appending to the+-- previous. That should still have an identical result.+foldPatchset' isInner (InsertCharacter errs h:ps) =+ F.B.first (Left (errs, h) :) $ foldPatchset' isInner ps+-- Float non-element nodes up one level.+foldPatchset' True (SoftCloseCurrentNode:ps) =+ F.B.first (map floatTrees) $ foldPatchset' True ps+ where floatTrees t'@(Right (RelativeLocation l, t))+ | nodeType (node t) == Just ElementNode = t'+ | otherwise = Right (RelativeLocation $ succ l, t)+ -- Text nodes here don't take advantage of ahead-of-time gathering+ -- (thus need more complex concatenation), but shouldn't be a large+ -- portion of the document.+ floatTrees (Left (_, c)) = Right (RelativeLocation 1, emptyTree+ { node = Text $ T.singleton c+ })+ floatTrees t' = t'+-- Consume patches until and including the (matching) node-closing patch.+foldPatchset' isInner (InsertElement _ tag:ps) =+ let (ts, ps') = foldPatchset' True ps+ (ts', bubble) = filterFloaters ts $ floatsElement isInner tag+ (attrs, ts'') = L.partition isAttribute ts'+ tag' = tag+ { elementAttributes = M.union (elementAttributes tag) .+ fromAttrList $ Y.mapMaybe toAttribute attrs+ }+ this = Right (RelativeLocation 0, packElement tag' $ joinTexts ts'')+ in case ps' of+ [] -> (this : bubble, [])+ (DropCurrentNode:ps'') -> F.B.first (++ bubble) $ foldPatchset' True ps''+ (CloseNodes ls:ps'') ->+ let (here, toFloat) = decrementReparenting ls+ ls' = case here of+ l | l >= 2 -> M.I.insertWith (+) 0 (pred l) toFloat+ _ -> toFloat+ (out, trail) = foldPatchset' True $ CloseNodes ls' : ps''+ in (this : bubble ++ out, trail)+ (RestartParsing:_) -> ([], ps')+ _ ->+ let (out, trail) = foldPatchset' True ps'+ cleaned = map (fmap $ \c -> (RelativeLocation 0, c)) ts''+ in (cleaned ++ out ++ bubble, trail)+ where isAttribute (Right tok) = nodeType (node tok) == Just AttributeNode+ isAttribute (Left _) = False+ toAttribute (Right tok) = case node tok of+ Attribute attr -> Just attr+ _ -> Nothing+ toAttribute (Left _) = Nothing+++-- | Given a heterogeneous set of patches potentially destined for multiple+-- places within the document hierarchy, and a known set of addresses for the+-- current location, partition out the relevant patches from those continuing+-- onward.+filterFloaters+ :: [Either ([ParseError], Char) (InsertAt, Tree)]+ -> [InsertAt]+ -> ([Either ([ParseError], Char) Tree], [Either ([ParseError], Char) (InsertAt, Tree)])+filterFloaters ts here = F.B.bimap+ (map $ F.B.second snd)+ (map . fmap $ F.B.first decrementTarget) $+ L.partition isHere ts+ where decrementTarget loc = case loc of+ RelativeLocation 0 -> loc+ RelativeLocation i -> RelativeLocation $ pred i+ _ -> loc+ isHere (Right (l, _)) = elem l here+ isHere (Left _) = True++-- | Calculate the accepted addresses for the element at the current location;+-- usually just @'RelativeLocation' 0@, but a root @\<html\>@ node also accepts+-- 'InHtmlElement'.+floatsElement+ :: Bool+ -- ^ Whether this node is a descendant node of the root.+ -> ElementParams+ -> [InsertAt]+floatsElement isInner d+ | elementName d == T.pack "html" && not isInner = [RelativeLocation 0, InHtmlElement]+ | otherwise = [RelativeLocation 0]+++-- | Given the list of reparenting directives used by a 'CloseNodes'+-- instruction, increase the distance to the node-to-reparent to account for+-- the patch being pushed into a child node.+incrementReparenting :: M.I.IntMap ReparentDepth -> M.I.IntMap ReparentDepth+incrementReparenting = M.I.mapKeys succ++-- | Given the list of reparenting directives used by a 'CloseNodes'+-- instruction, partition out the accumulated directives intended for the+-- current node, and decrease the distance to the node-to-reparent to account+-- for the child node being closed.+decrementReparenting :: M.I.IntMap ReparentDepth -> (ReparentDepth, M.I.IntMap ReparentDepth)+decrementReparenting ls = (Y.fromMaybe 0 $ M.I.lookup 0 ls, M.I.mapKeys pred $ M.I.delete 0 ls)+++-- | Collapse all sequential 'Char' sequences in the list and pack them into a+-- single 'Text' node, preserving the interspersed complete subtrees.+joinTexts :: [Either ([ParseError], Char) Tree] -> [Tree]+joinTexts [] = []+joinTexts (Right t : xs) = t : joinTexts xs+joinTexts (Left c : xs) = txt' : joinTexts xs'+ where (cs', xs') = span E.isLeft xs+ cs = map (either id $ error "unexpected 'Right' after @span isLeft@") cs'+ (_, txt) = unzip $ c : cs+ txt' = emptyTree+ { node = Text $ T.pack txt+ }+++-- | Wrap the desired level of backwards compatibility into a 'Document' node+-- as a placeholder.+packQuirks :: QuirksMode -> Tree+packQuirks mode = emptyTree+ { node = Document mode+ }++-- | Wrap a string of characters in the data types expected for a 'Comment'+-- node in the output of 'buildTree'.+packComment :: T.Text -> Tree+packComment txt = emptyTree+ { node = Comment txt+ }++-- | Wrap the metadata contained in a document type declaration in the data+-- types expected for a 'DocumentType' node in the output of 'buildTree'.+packDoctype :: DocumentTypeParams -> Tree+packDoctype dtd = emptyTree+ { node = DocumentType dtd+ }++-- | Wrap the metadata contained in a markup tag in the data types expected for+-- an 'Element' node in the output of 'buildTree'. If it represents an HTML+-- @\<template\>@ element, additionally wrap the children in a+-- 'DocumentFragment' node to indicate the @[template+-- contents](https://html.spec.whatwg.org/scripting.html#template-contents)@+-- parameter.+packElement :: ElementParams -> [Tree] -> Tree+packElement tag childTrees+ | nodeIsElement (T.pack "template") tag = emptyTree+ { node = e+ , children = [emptyTree+ { node = DocumentFragment+ , children = childTrees+ }]+ }+ | otherwise = emptyTree+ { node = e+ , children = childTrees+ }+ where e = Element tag++-- | Wrap the metadata contained in a metadata tag's attribute in the data+-- types expected for an 'Attribute' node in the output of 'buildTree'; this+-- will then be subsumed into the parent 'Element' node, and does not remain in+-- the final document tree.+packAttribute :: AttributeParams -> Tree+packAttribute attr = emptyTree+ { node = Attribute attr+ }
+ test/Test/Mangrove/Html5Lib.hs view
@@ -0,0 +1,31 @@+{-|+Description: ++Copyright: (c) 2020 Samuel May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: experimental+Portability: portable+-}+module Main where+++import qualified Control.Monad as M+import qualified System.Exit as E+import qualified Test.HUnit as U++import qualified Test.Mangrove.Html5Lib.Tokenizer as Tokenizer+import qualified Test.Mangrove.Html5Lib.TreeConstruction as TreeConstruction+++main :: IO ()+main = do+ tokenizer <- Tokenizer.tests+ tree <- TreeConstruction.tests+ results <- U.runTestTT $ U.TestList+ [ tokenizer+ , tree+ ]+ + M.unless (U.errors results == 0 && U.failures results == 0) E.exitFailure
+ test/Test/Mangrove/Html5Lib/Common.hs view
@@ -0,0 +1,13 @@+module Test.Mangrove.Html5Lib.Common+ ( dataFile+ ) where+++import Paths_mangrove++import System.FilePath ( (</>) )+++dataFile :: FilePath -> IO FilePath+dataFile f = getDataFileName $ dataDir </> f+ where dataDir = "test" </> "html5lib-tests"
+ test/Test/Mangrove/Html5Lib/Tokenizer.hs view
@@ -0,0 +1,96 @@+module Test.Mangrove.Html5Lib.Tokenizer+ ( tests+ ) where+++import qualified Data.ByteString.Short as BS.S+import qualified Data.List as L+import qualified Data.Text as T++import qualified Test.HUnit as U++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize+import Web.Willow.DOM++import Test.Mangrove.Html5Lib.Tokenizer.JSON++import Test.HUnit ( (~:), (@?=) )+++tests :: IO U.Test+tests = U.TestLabel "tokenizer" . U.TestList <$> mapM runTestFile+ [ "test1"+ , "test2"+ , "test3"+ , "test4"+ , "entities"+ , "namedEntities"+ , "numericEntities"+ , "unicodeChars"+ -- , "unicodeCharsProblematic" -- Seems to contradict the standard in allowing surrogates to be decoded+ , "contentModelFlags"+ , "domjs"+ , "escapeFlag"+ , "pendingSpecChanges"+ -- , "xmlViolation"+ ]+++runTestFile :: FilePath -> IO U.Test+runTestFile p = U.TestLabel (p ++ ".test") . U.TestList . map run <$> parseTestFile p++run :: JsonTest -> U.Test+run test = T.unpack (description test) ~: U.TestList $ map (runState test)+ [ (show state, tokenizerMode state tokState)+ | state <- initialStates test+ ]+ where tokState = case lastStartTag test of+ Just n -> tokenizerStartTag Nothing n $ defaultTokenizerState+ Nothing -> defaultTokenizerState++runState :: JsonTest -> (String, TokenizerState) -> U.Test+runState test (mode, state) = mode ~: U.TestCase $ do+ let (out, state') = tokenize state $ input test+ final = finalizeTokenizer state'+ out' = out ++ final+ (L.sortOn show . concat $ map mapError out', trimEof $ map mapToken out') @?=+ (map errorCode $ errors test, concat $ output test)+ where normalizeToken (EndTag d) = EndTag $ emptyTagParams+ { tagName = tagName d+ }+ normalizeToken tok = tok+ mapToken = normalizeToken . snd+ normalizeError (CharacterReferenceOutsideUnicodeRange _) =+ CharacterReferenceOutsideUnicodeRange 0+ normalizeError (DuplicateAttribute _) =+ DuplicateAttribute (T.empty, T.empty)+ normalizeError (DuplicateSingletonElement _) =+ DuplicateSingletonElement emptyElementParams+ normalizeError (FramesetInBody _) =+ FramesetInBody emptyElementParams+ normalizeError (MalformedTableStructure _) =+ MalformedTableStructure emptyElementParams+ normalizeError (InvalidByteSequence _) =+ InvalidByteSequence $ BS.S.pack []+ normalizeError (NoncharacterCharacterReference _) =+ NoncharacterCharacterReference '\NUL'+ normalizeError (ObsoleteTagName _) =+ ObsoleteTagName T.empty+ normalizeError (SurrogateCharacterReference _) =+ SurrogateCharacterReference '\NUL'+ normalizeError (UnexpectedCharacterAfterDoctypeSystemIdentifier _) =+ UnexpectedCharacterAfterDoctypeSystemIdentifier '\NUL'+ normalizeError (UnexpectedDoctype _) =+ UnexpectedDoctype emptyDocumentTypeParams+ normalizeError (UnexpectedDescendantElement _) =+ UnexpectedDescendantElement emptyElementParams+ normalizeError (UnexpectedEndTag _) =+ UnexpectedEndTag emptyElementParams+ normalizeError (UnmatchedEndTag _) =+ UnmatchedEndTag emptyElementParams+ normalizeError err = err+ mapError = map normalizeError . fst+ trimEof ts = case reverse ts of+ (EndOfStream:ts') -> reverse ts'+ _ -> ts
+ test/Test/Mangrove/Html5Lib/Tokenizer/JSON.hs view
@@ -0,0 +1,179 @@+{-# LANGUAGE OverloadedStrings #-}++{-# OPTIONS_GHC -Wno-orphans #-}++module Test.Mangrove.Html5Lib.Tokenizer.JSON+ ( JsonTest ( .. )+ , TestError ( .. )+ , parseTestFile+ ) where+++import qualified Data.Aeson as J+import qualified Data.Aeson.Types as J+import qualified Data.ByteString as BS+import qualified Data.ByteString.Builder as BS.B+import qualified Data.ByteString.Lazy as BS.L+import qualified Data.ByteString.Short as BS.S+import qualified Data.HashMap.Strict as M+import qualified Data.List as L+import qualified Data.Maybe as Y+import qualified Data.Text as T++import qualified Numeric as N++import qualified Text.Read as R++import Web.Mangrove.Parse.Common.Error+import Web.Mangrove.Parse.Tokenize+import Web.Willow.DOM hiding ( Node ( .. ) )++import Test.Mangrove.Html5Lib.Common++import Data.Aeson ( (.:), (.:?), (.!=) )+import Data.Vector ( (!?) )+import System.FilePath ( (</>), (<.>) )+++testFile :: FilePath -> IO FilePath+testFile f = (</> f) <$> dataFile "tokenizer"++parseTestFile :: FilePath -> IO [JsonTest]+parseTestFile p = testFile (p <.> "test") >>= J.eitherDecodeFileStrict >>= either fail rewrap+ where rewrap (TestFile ts) = return ts+++data JsonTest = JsonTest+ { description :: T.Text+ , input :: BS.ByteString+ , output :: [[Token]]+ , initialStates :: [CurrentTokenizerState]+ , lastStartTag :: Maybe T.Text+ , errors :: [TestError]+ }+ deriving ( Eq, Show, Read )++instance J.FromJSON JsonTest where+ parseJSON = J.withObject "test" $ \v -> do+ doubleEscaped <- v .:? "doubleEscaped" .!= False+ JsonTest+ <$> v .: "description"+ <*> fmap (BS.L.toStrict . BS.B.toLazyByteString . BS.B.stringUtf8 . unescape doubleEscaped) (v .: "input")+ <*> fmap (map $ unpackTokens doubleEscaped) (v .: "output")+ <*> v .:? "initialStates" .!= [DataState]+ <*> v .:? "lastStartTag"+ <*> fmap (L.sortOn (show . errorCode)) (v .:? "errors" .!= [])+ where unpackTokens doubleEscaped (TestToken (Right (Comment str))) =+ [Comment . T.pack $ unescape doubleEscaped str]+ unpackTokens _ (TestToken (Right tok)) = [tok]+ unpackTokens doubleEscaped (TestToken (Left str)) =+ map Character . unescape doubleEscaped $ T.pack str+ unescape False = T.unpack+ unescape True = unescape' . T.splitOn "\\u"+ unescape' [] = ""+ unescape' (t:ts) = T.unpack t ++ concatMap unescapeCode ts+ unescapeCode t = maybe '\NUL' (toEnum . fst) (Y.listToMaybe $ N.readHex code) : ts+ where (code, ts) = splitAt 4 $ T.unpack t+++data TestError = TestError+ { errorCode :: ParseError+ , line :: Word+ , column :: Word+ }+ deriving ( Eq, Show, Read )++instance J.FromJSON TestError where+ parseJSON = J.withObject "error" $ \v -> TestError+ <$> v .: "code"+ <*> v .: "line"+ <*> v .: "col"+++instance J.FromJSON CurrentTokenizerState where+ parseJSON = J.withText "initialState" $ \v -> case v of+ "Data state" -> return DataState+ "PLAINTEXT state" -> return PlainTextState+ "RCDATA state" -> return RCDataState+ "RAWTEXT state" -> return RawTextState+ "Script data state" -> return ScriptDataState+ "CDATA section state" -> return CDataState+ _ -> fail $ "Unknown tokenizer state '" ++ T.unpack v ++ "'"+++newtype TestToken = TestToken (Either String Token)+ deriving ( Eq, Show, Read )++instance J.FromJSON TestToken where+ parseJSON = J.withArray "output" $ \v -> case v !? 0 of+ Just "DOCTYPE" -> do+ name <- maybe (return Nothing) parseTextOrNull $ v !? 1+ public <- maybe (return Nothing) parseTextOrNull $ v !? 2+ system <- maybe (return Nothing) parseTextOrNull $ v !? 3+ correctness <- maybe (return True) parseBool $ v !? 4+ return . TestToken . Right . Doctype $ emptyDoctypeParams+ { doctypeName = name+ , doctypePublicId = public+ , doctypeSystemId = system+ , doctypeQuirks = not correctness+ }+ Just "StartTag" -> do+ name <- maybe (return T.empty) parseText $ v !? 1+ attrs <- maybe (return []) parseAttributes $ v !? 2+ selfClosing <- maybe (return False) parseBool $ v !? 3+ return . TestToken . Right . StartTag $ emptyTagParams+ { tagName = name+ , tagIsSelfClosing = selfClosing+ , tagAttributes = M.fromList attrs+ }+ Just "EndTag" -> J.withText "EndTag" (return . TestToken . Right . packEndTag) $+ Y.fromMaybe (J.String T.empty) (v !? 1)+ Just "Comment" -> J.withText "Comment" (return . TestToken . Right . Comment) $+ Y.fromMaybe (J.String T.empty) (v !? 1)+ Just "Character" -> J.withText "Character" (return . TestToken . Left . T.unpack) $+ Y.fromMaybe (J.String T.empty) (v !? 1)+ Just _ -> fail $ "Unknown output token"+ Nothing -> fail $ "Empty output token"+ where packEndTag name = EndTag $ emptyTagParams+ { tagName = name+ }+ parseText = J.withText "text" return+ parseTextOrNull (J.String t) = return $ Just t+ parseTextOrNull J.Null = return Nothing+ parseTextOrNull v = J.typeMismatch "String or Null" v+ parseBool = J.withBool "bool" return+ parseAttributes = J.withObject "attributes" $ fmap M.toList . mapM parseText+++instance J.FromJSON ParseError where+ parseJSON = J.withText "code" $ \v -> case v of+ "character-reference-outside-unicode-range" ->+ return $ CharacterReferenceOutsideUnicodeRange 0+ "duplicate-attribute" -> return $ DuplicateAttribute (T.empty, T.empty)+ "duplicate-singleton-element" -> return $ DuplicateSingletonElement emptyElementParams+ "frameset-in-body" -> return $ FramesetInBody emptyElementParams+ "malformed-table-structure" -> return $ MalformedTableStructure emptyElementParams+ "invalid-byte-sequence" -> return . InvalidByteSequence $ BS.S.pack []+ "noncharacter-character-reference" -> return $ NoncharacterCharacterReference '\NUL'+ "obsolete-tag-name" -> return $ ObsoleteTagName T.empty+ "surrogate-character-reference" -> return $ SurrogateCharacterReference '\NUL'+ "unexpected-character-after-doctype-system-identifier" ->+ return $ UnexpectedCharacterAfterDoctypeSystemIdentifier '\NUL'+ "unexpected-descendant-element" -> return $ UnexpectedDescendantElement emptyElementParams+ "unexpected-doctype" -> return $ UnexpectedDoctype emptyDocumentTypeParams+ "unexpected-end-tag" -> return $ UnexpectedEndTag emptyElementParams+ "unmatched-end-tag" -> return $ UnmatchedEndTag emptyElementParams+ _ -> maybe (fail $ err v) return . R.readMaybe . concat .+ map (T.unpack . conCase) $ T.split (== '-') v+ where conCase "cdata" = "CData"+ conCase "eof" = "EOF"+ conCase t = T.toTitle t+ err v = "Could not parse error code '" ++ T.unpack v ++ "'"+++newtype TestFile = TestFile [JsonTest]+ deriving ( Eq, Show, Read )++instance J.FromJSON TestFile where+ parseJSON = J.withObject "file" $ \v -> TestFile+ <$> v .: "tests"
+ test/Test/Mangrove/Html5Lib/TreeConstruction.hs view
@@ -0,0 +1,68 @@+module Test.Mangrove.Html5Lib.TreeConstruction+ ( tests+ ) where+++import qualified Control.Exception as E++import qualified Test.HUnit as U++import Web.Mangrove.Parse.Tree++import Test.Mangrove.Html5Lib.TreeConstruction.Parser++import Test.HUnit ( (@=?) )+++tests :: IO U.Test+tests = U.TestLabel "tree-construction" . U.TestList <$> mapM runTestFile files+ where files =+ [ "tests" ++ show (i :: Word)+ | i <- [1..12]+ -- "tests13" doesn't exist+ ++ [14..26]+ ] +++ [ "adoption01"+ , "adoption02"+ , "blocks"+ , "comments01"+ , "doctype01"+ , "domjs-unsafe"+ , "entities01"+ , "entities02"+ , "foreign-fragment"+ , "html5test-com"+ , "inbody01"+ , "isindex"+ , "main-element"+ , "math"+ , "menuitem-element"+ , "namespace-sensitivity"+ , "plain-text-unsafe"+ , "ruby"+ , "scriptdata01"+ , "tables01"+ , "template"+ , "tests_innerHTML_1"+ , "tricky01"+ , "webkit01"+ , "webkit02"+ ]++runTestFile :: FilePath -> IO U.Test+runTestFile p = U.TestLabel (p ++ ".dat") . U.TestList . map run <$> parseTestFile p+++run :: TreeTest -> U.Test+run t = U.TestCase $ do+ result <- E.tryJust filterErrors . E.evaluate .+ uncurry finalizeTree . tree (state t) $ input t+ either return (output t @=?) $ normalizeOutput <$> result+ where filterErrors (E.ErrorCall "Adoption agency not yet implemented") = Just ()+ filterErrors (E.ErrorCall "Foster parenting not yet implemented") = Just ()+ filterErrors _ = Nothing+ normalizeOutput n = n+ { node = normalizeQuirks $ node n+ }+ normalizeQuirks (Document _) = Document NoQuirks+ normalizeQuirks n = n
+ test/Test/Mangrove/Html5Lib/TreeConstruction/Parser.hs view
@@ -0,0 +1,208 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}++module Test.Mangrove.Html5Lib.TreeConstruction.Parser+ ( parseTestFile+ , TreeTest ( .. )+ ) where+++import qualified Data.ByteString.Char8 as BS.C+import qualified Data.HashMap.Strict as M+import qualified Data.List as L+import qualified Data.Maybe as Y+import qualified Data.Text as T+import qualified Data.Text.Encoding as T++import Web.Mangrove.Parse.Tree+import Web.Willow.DOM++import Test.Mangrove.Html5Lib.Common++#if MIN_VERSION_base(4,11,0)+#else+import Data.Semigroup ( (<>) )+#endif++import System.FilePath ( (</>), (<.>) )+++data TreeTest = TreeTest+ { input :: BS.C.ByteString+ , errors :: [(Word, Word, T.Text)]+ , output :: Tree+ , state :: TreeState+ }+ deriving ( Eq, Show, Read )++data RawTest = RawTest+ { input' :: [BS.C.ByteString]+ , errors' :: [BS.C.ByteString]+ , output' :: [BS.C.ByteString]+ , state' :: Maybe BS.C.ByteString+ , scripting' :: Maybe Bool+ }+ deriving ( Eq, Show, Read )+++testFile :: FilePath -> IO FilePath+testFile f = (</> f) <$> dataFile "tree-construction"++parseTestFile :: FilePath -> IO [TreeTest]+parseTestFile p = fmap parse $ testFile (p <.> "dat") >>= BS.C.readFile++parse :: BS.C.ByteString -> [TreeTest]+parse = Y.mapMaybe (parseTest . rawTest) . breakTests+ where breakTests = breakTests' . BS.C.split '\n'+ breakTests' [] = []+ breakTests' bss = case break BS.C.null bss of+ (bs, []) -> [bs]+ (bs, bss') -> bs : breakTests' (drop 1 bss')++rawTest :: [BS.C.ByteString] -> RawTest+rawTest ("#data":bss) = (rawTest bss')+ { input' = d+ }+ where (d, bss') = break isInstruction bss+rawTest ("#errors":bss) = (rawTest bss')+ { errors' = e+ }+ where (e, bss') = break isInstruction bss+rawTest ("#document-fragment":bss) = (rawTest bss')+ { state' = Y.listToMaybe x+ }+ where (x, bss') = break isInstruction bss+rawTest ("#document":bss) = (rawTest bss')+ { output' = t+ }+ where (t, bss') = break isInstruction bss+rawTest ("#script-off":bss) = (rawTest bss')+ { scripting' = Just False+ }+ where (_, bss') = break isInstruction bss+rawTest ("#script-on":bss) = (rawTest bss')+ { scripting' = Just True+ }+ where (_, bss') = break isInstruction bss+rawTest (_:bss) = rawTest $ dropWhile (not . isInstruction) bss+rawTest [] = RawTest+ { input' = []+ , errors' = []+ , output' = []+ , state' = Nothing+ , scripting' = Nothing+ }++isInstruction :: BS.C.ByteString -> Bool+isInstruction bs = fmap fst (BS.C.uncons bs) == Just '#'+++parseTest :: RawTest -> Maybe TreeTest+parseTest t+ | null $ input' t = Nothing+ | null $ output' t = Nothing+ | scripting' t == Just True = Nothing+ | otherwise = Just $ TreeTest+ { input = BS.C.intercalate "\n" $ input' t+ , errors = Y.mapMaybe parseError $ errors' t+ , output = parseOutput . map (BS.C.drop 2) . foldr joinMultiline [] $ output' t+ , state = case state' t of+ Just bs -> treeFragment (packElement bs) [] Nothing Nothing defaultTreeState+ Nothing -> defaultTreeState+ }+ where joinMultiline bs [] = [bs]+ joinMultiline bs (bs':bss) = if BS.C.take 2 bs' == "| "+ then bs : bs' : bss+ else bs <> "\n" <> bs' : bss+ htmlElement = emptyElementParams+ { elementName = "html"+ , elementNamespace = Just htmlNamespace+ }+ packElement = packElement' . parseNode . BS.C.cons '<' . flip BS.C.snoc '>'+ packElement' (Element d) = d+ packElement' _ = htmlElement++parseError :: BS.C.ByteString -> Maybe (Word, Word, T.Text)+parseError _ = Nothing --TODO++parseOutput :: [BS.C.ByteString] -> Tree+parseOutput bss = Tree+ { node = Document NoQuirks+ , children = map snd . foldr encapsulate [] $ map dropParents bss+ }+ where dropParents bs =+ let (ws, n) = BS.C.span (== ' ') bs+ in (BS.C.length ws, parseNode n)+ encapsulate (d, e) es =+ let (cs, ss) = span ((d <) . fst) es+ (attrs, nonAttrs) = L.partition (\c -> nodeType (node $ snd c) == Just AttributeNode) cs+ (e', cs') = if nodeType e == Just ElementNode+ then let Element t = e+ t' = t+ { elementAttributes =+ M.union (elementAttributes t) . fromAttrList $ Y.mapMaybe (unpack . node . snd) attrs+ }+ unpack (Attribute a) = Just a+ unpack _ = Nothing+ in (Element t', nonAttrs)+ else (e, cs)+ in (d, Tree+ { node = e'+ , children = map snd cs'+ }) : ss++parseNode :: BS.C.ByteString -> Node+parseNode bs+ | bs == "content" = DocumentFragment+ | BS.C.take 10 bs == "<!DOCTYPE " =+ let (n', bs') = BS.C.break (\c -> c == ' ' || c == '>') $ BS.C.drop 10 bs+ (n, p, s) = if BS.C.take 1 bs' == ">"+ then (repack 0 0 n', T.empty, T.empty)+ else let (p', s') = BS.C.break (== '"') $ BS.C.drop 2 bs'+ in (repack 0 0 n', repack 0 0 p', repack 3 2 s')+ in DocumentType $ emptyDocumentTypeParams+ { documentTypeName = n+ , documentTypePublicId = p+ , documentTypeSystemId = s+ }+ | BS.C.take 5 bs == "<!-- " = Comment $ repack 5 4 bs+ | BS.C.take 1 bs == "<" && fmap snd (BS.C.unsnoc bs) == Just '>' =+ let (ns, n) = trimTag $ repack 1 1 bs+ in Element $ emptyElementParams+ { elementNamespace = Just ns+ , elementName = n+ }+ | BS.C.take 1 bs == "\"" = Text $ repack 1 1 bs+ | BS.C.isInfixOf "=\"" bs && BS.C.last bs == '"' =+ let (n', v) = BS.C.break (== '=') bs+ in Attribute $ (trimAttribute $ repack 0 0 n')+ { attrValue = repack 2 1 v+ }+ | otherwise = Document NoQuirks+ where repack h f bs' = T.decodeUtf8 . BS.C.drop h $ BS.C.take (BS.C.length bs' - f) bs'+ trimTag t+ | T.take 4 t == "svg " = (svgNamespace, T.drop 4 t)+ | T.take 5 t == "math " = (mathMLNamespace, T.drop 5 t)+ | otherwise = (htmlNamespace, t)+ trimAttribute t+ | T.take 6 t == "xlink " = emptyAttributeParams+ { attrNamespace = Just xlinkNamespace+ , attrPrefix = Just "xlink"+ , attrName = T.drop 6 t+ }+ | T.take 4 t == "xml " = emptyAttributeParams+ { attrNamespace = Just xmlNamespace+ , attrPrefix = Just "xml"+ , attrName = T.drop 4 t+ }+ | T.take 6 t == "xmlns " = emptyAttributeParams+ { attrNamespace = Just xmlnsNamespace+ , attrPrefix = Just "xmlns"+ , attrName = T.drop 6 t+ }+ | otherwise = emptyAttributeParams+ { attrNamespace = Nothing+ , attrPrefix = Nothing+ , attrName = t+ }
+ test/Test/Mangrove/Unit.hs view
@@ -0,0 +1,28 @@+{-|+Description: ++Copyright: (c) 2020 Samuel May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: experimental+Portability: portable+-}+module Main where+++import qualified Control.Monad as N+import qualified System.Exit as E+import qualified Test.HUnit as U++import qualified Test.Mangrove.Unit.Tokenize as Tokenize+++main :: IO ()+main = do+ tokenize <- Tokenize.tests+ results <- U.runTestTT $ U.TestList+ [ tokenize+ ]+ + N.unless (U.errors results == 0 && U.failures results == 0) E.exitFailure
+ test/Test/Mangrove/Unit/Common.hs view
@@ -0,0 +1,23 @@+{-|+Description: ++Copyright: (c) 2020 Samuel May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: experimental+Portability: portable+-}+module Test.Mangrove.Unit.Common+ ( dataFile+ ) where+++import Paths_mangrove++import System.FilePath ( (</>) )+++dataFile :: FilePath -> IO FilePath+dataFile f = getDataFileName $ dataDir </> f+ where dataDir = "test" </> "unit"
+ test/Test/Mangrove/Unit/Tokenize.hs view
@@ -0,0 +1,26 @@+{-|+Description: ++Copyright: (c) 2020 Samuel May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: experimental+Portability: portable+-}+module Test.Mangrove.Unit.Tokenize+ ( tests+ ) where+++import qualified Test.HUnit as U++import Test.HUnit ( (~:) )++import qualified Test.Mangrove.Unit.Tokenize.Tag as Tag+++tests :: IO U.Test+tests = return $ "tokenization" ~: U.TestList+ [ Tag.tests+ ]
+ test/Test/Mangrove/Unit/Tokenize/Tag.hs view
@@ -0,0 +1,49 @@+{-|+Description: ++Copyright: (c) 2020 Samuel May+License: MPL-2.0+Maintainer: ag.eitilt@gmail.com++Stability: experimental+Portability: portable+-}+module Test.Mangrove.Unit.Tokenize.Tag+ ( tests+ ) where+++import qualified Data.ByteString.Char8 as BS.C+import qualified Data.Text as T+import qualified Data.Tuple.HT as U.HT++import qualified Test.HUnit as U++import Test.HUnit ( (~:), (~?=) )++import Web.Mangrove.Parse.Tokenize+++tests :: U.Test+tests = "UTF-8 encoding" ~: U.TestList+ [ "appropriate end tags allow re-entry" ~: U.TestList $ map stepBreakpoint+ [ RawTextState+ , RCDataState+ , ScriptDataState+ , ScriptDataEscapedState+ ]+ ]+++stepBreakpoint :: CurrentTokenizerState -> U.Test+stepBreakpoint state = show state ~:+ step' ~?= ([([], EndTag tag)], tokenState', BS.C.pack "</xmp>")+ where step = tokenizeStep tokenState $ BS.C.pack "</script></xmp>"+ step' = U.HT.mapSnd3 (tokenizerEncoding (Right $ Just Utf8)) $ step+ script = T.pack "script"+ tag = emptyTagParams+ { tagName = script+ }+ tokenState = tokenizerStartTag Nothing script $+ tokenizerMode state defaultTokenizerState+ tokenState' = tokenizerMode DataState tokenState