diff --git a/App/Localization/ar.lproj/Credits.rtf b/App/Localization/ar.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/ar.lproj/Credits.rtf
+++ b/App/Localization/ar.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/de.lproj/Credits.rtf b/App/Localization/de.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/de.lproj/Credits.rtf
+++ b/App/Localization/de.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/en.lproj/Credits.rtf b/App/Localization/en.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/en.lproj/Credits.rtf
+++ b/App/Localization/en.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/es.lproj/Credits.rtf b/App/Localization/es.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/es.lproj/Credits.rtf
+++ b/App/Localization/es.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/fr.lproj/Credits.rtf b/App/Localization/fr.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/fr.lproj/Credits.rtf
+++ b/App/Localization/fr.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/is.lproj/Credits.rtf b/App/Localization/is.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/is.lproj/Credits.rtf
+++ b/App/Localization/is.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/it-IT.lproj/Credits.rtf b/App/Localization/it-IT.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/it-IT.lproj/Credits.rtf
+++ b/App/Localization/it-IT.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/ja.lproj/Credits.rtf b/App/Localization/ja.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/ja.lproj/Credits.rtf
+++ b/App/Localization/ja.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/ko-KR.lproj/Credits.rtf b/App/Localization/ko-KR.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/ko-KR.lproj/Credits.rtf
+++ b/App/Localization/ko-KR.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/pt-BR.lproj/Credits.rtf b/App/Localization/pt-BR.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/pt-BR.lproj/Credits.rtf
+++ b/App/Localization/pt-BR.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/sk.lproj/Credits.rtf b/App/Localization/sk.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/sk.lproj/Credits.rtf
+++ b/App/Localization/sk.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/sv.lproj/Credits.rtf b/App/Localization/sv.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/sv.lproj/Credits.rtf
+++ b/App/Localization/sv.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/zh-Hans.lproj/Credits.rtf b/App/Localization/zh-Hans.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/zh-Hans.lproj/Credits.rtf
+++ b/App/Localization/zh-Hans.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/App/Localization/zh-Hant.lproj/Credits.rtf b/App/Localization/zh-Hant.lproj/Credits.rtf
index 9ace71f..0e2f464 100644
--- a/App/Localization/zh-Hant.lproj/Credits.rtf
+++ b/App/Localization/zh-Hant.lproj/Credits.rtf
@@ -93,6 +93,8 @@ Open source Markdown editor for OS X.
{\field{\*\fldinst{HYPERLINK "https://github.com/tinglou"}}{\fldrslt \cf0 Ting Lou}}\
\pard\pardeftab720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\fldrslt \cf0 Remi Cattiau}}\
+\pard\pardeftab720\partightenfactor0
+{\field{\*\fldinst{HYPERLINK "https://github.com/levous"}}{\fldrslt \cf0 LeVous (Rusty Zarse)}}\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
@@ -160,28 +162,18 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 \cf0 Hoedown}}\
-Copyright \'a9 2008, Natacha Port\'e9\
-Copyright \'a9 2011, Vicent Mart\'ed\
-Copyright \'a9 2013, Devin Torres and the Hoedown authors\
-\
-Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.\
-\
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\
-\
-{\field{\*\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\fldrslt
-\b\fs28 Hoextdown}}
-\b\fs28 \
-\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
-
-\b0\fs24 \cf0 Copyright \'a9 2014 kjdev\
+{\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\fldrslt
+\b\fs28 \cf0 cmark-gfm}}\
+Copyright \'a9 2014, John MacFarlane\
+All rights reserved.\
\
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.\
\
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\
\
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\
+cmark-gfm includes code from other projects under their own notices: see {\field{\*\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}{\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\
\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}{\fldrslt
@@ -249,6 +241,8 @@ Copyright 2011 Ali Rantakari -- <{\field{\*\fldinst{HYPERLINK "http://hasseg.org
\
PEG-Markdown-Highlight is licensed under the same terms as peg-markdown (see below for more information).\
\
+MacDown's editor highlighter and theme parser are Swift ports of PEG Markdown Highlight's code, so that editor themes keep working; the library itself is no longer used.\
+\
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\partightenfactor0
{\field{\*\fldinst{HYPERLINK "https://github.com/jgm/peg-markdown"}}{\fldrslt
\b\fs28 \cf0 peg-markdown}}\
diff --git a/CLAUDE.md b/CLAUDE.md
index 982863a..35fe6ce 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -28,7 +28,7 @@ There is no linter configured.
## Build structure
-- `Package.swift` (SwiftPM) holds everything except the app's `@main`: the C targets `CHoedown` and `CPegMarkdown`, `MacDownShared`, `MacDownKit`, the `macdown-cmd` executable, and the tests. Almost all code lives in `MacDownKit`. `App/MacDownApp.swift` only wires `MacDownScene` and `MacDownAppDelegate`.
+- `Package.swift` (SwiftPM) holds everything except the app's `@main`: `MacDownShared`, `MacDownKit`, the `macdown-cmd` executable, and the tests. The Markdown parser is swift-cmark (cmark-gfm), pinned exactly. Almost all code lives in `MacDownKit`. `App/MacDownApp.swift` only wires `MacDownScene` and `MacDownAppDelegate`.
- `project.yml` (XcodeGen) defines the `MacDown` app target, which depends on the local package, and embeds the `macdown` tool in `Contents/SharedSupport/bin`. Edit `project.yml`, not the generated `.xcodeproj`.
- Resources load through `MPPaths.resourceBundle` (`Bundle.module`). New resource directories must be added to the `resources:` list in `Package.swift`.
- Strict concurrency is on (`swiftLanguageModes: [.v6]`, `SWIFT_STRICT_CONCURRENCY: complete`). UI types are `@MainActor`, and parsing runs off the main actor.
@@ -36,12 +36,12 @@ There is no linter configured.
## Render pipeline
1. `DocumentController` (one per window, the central `@MainActor` coordinator that owns the editor, preview, renderer and the formatting/export actions) observes editor text and preference changes.
-2. `Preferences.renderSettings` turns preferences into `RenderSettings` = `ParseSettings` (Markdown options, stored as hoedown's flags, and the hidden `markdownEngine`) + `PageSettings` (styles, Prism, MathJax, Mermaid, Graphviz, template).
-3. `Renderer.parse` builds a `MarkdownDocumentModel` in a detached task (a generation counter discards superseded parses). The model (`Document/Model`, the only code that calls cmark-gfm) protects math and front matter (`ProtectedSource`), parses once with cmark-gfm (`CMarkTree`), and walks the tree there: `HTMLRenderer` writes the preview body (hoedown's markup, Prism languages, `[TOC]`, front matter table, math for MathJax, `data-source-line` for scroll sync) and `HighlightMapper` the editor's highlight spans and blocks. Only `Sendable` results leave the task. With `markdownEngine = hoedown` (selectable for one release, then removed) `MarkdownParser` renders with the original's patched hoedown instead.
+2. `Preferences.renderSettings` turns preferences into `RenderSettings` = `ParseSettings` (the opt-in Markdown options; standard Markdown is always on) + `PageSettings` (styles, Prism, MathJax, Mermaid, Graphviz, template).
+3. `Renderer.parse` builds a `MarkdownDocumentModel` in a detached task (a generation counter discards superseded parses). The model (`Document/Model`, the only code that calls cmark-gfm) protects math and front matter (`ProtectedSource`), parses once with cmark-gfm (`CMarkTree`), and walks the tree there: `HTMLRenderer` writes the preview body (hoedown's markup, Prism languages, `[TOC]`, front matter table, math for MathJax, `data-source-line` for scroll sync) and `HighlightMapper` the editor's highlight spans and blocks. Only `Sendable` results leave the task.
4. `PageBuilder` (pure static functions) builds the full HTML page from the parse result using `HTMLTemplate` (a minimal Handlebars subset) and `Asset` (linked or embedded CSS/JS). The same builder makes the preview HTML and the HTML/PDF export, with different embedding options.
5. `PreviewController` loads the page into a `WKWebView`. File URLs are rewritten to `x-macdown-preview://local/` (`PreviewURL` / `LocalFileSchemeHandler`) because WKWebView pages loaded from a string can't read local files. Metrics for scroll sync, word count and background color come back via JS evaluation and script message handlers. When a new page differs from the loaded one only between the `` markers (added by `previewHTML` only, not exports), `load` swaps the body in place and re-runs the page scripts instead of reloading; anything that changes styles, scripts or the base URL reloads the page.
-The output keeps hoedown's markup where CommonMark agrees; `HTMLRendererTests` compare it with hoedown byte for byte, and every remaining difference over the test corpus is reviewed in `Tests/MacDownKitTests/Resources/reviewed-html-diffs.json` and summarized in docs/MACDOWN-PORT.md. Don't add C: renderer changes go in `HTMLRenderer`.
+The output keeps the markup the original's hoedown wrote where CommonMark agrees (classes, code blocks, footnotes), so its styles and scripts apply; `HTMLRendererTests` compare it byte for byte with hoedown's output, recorded before hoedown was removed (`Tests/MacDownKitTests/Resources/engine-snapshots.json`), and the differences are summarized in docs/MACDOWN-PORT.md. Don't add C: renderer changes go in `HTMLRenderer`.
## Saving
@@ -49,7 +49,7 @@ The editor edits a draft (its text); `MarkdownDocument.text` is the persisted st
## Editor
-`EditorTextView` (an `NSTextView` subclass) is hosted in SwiftUI through `Representables`. The editing helpers (list/blockquote continuation, auto-pairing, toggling markup, indenting) are in `NSTextView+Autocomplete.swift` and are called from `DocumentController`'s `NSTextViewDelegate` methods. Pasting formatted text (`EditorTextView+Paste.swift`) converts the clipboard's HTML or rich text with `HTMLToMarkdown` (`Document/HTMLToMarkdown`) in `readSelection(from:type:)`, which Paste, Services and drops all go through; see docs/ARCHITECTURE.md, "Pasting". Syntax highlighting is `MarkdownHighlighter`, which styles the visible range from spans keyed by PEG Markdown Highlight's element types, so the original `.style` themes (parsed by `ThemeStyle`) apply. The spans come from the document model's `HighlightMapper` (handed over by `DocumentController` after each parse); with the hoedown engine, from the PEG C parser, whose `pmh_parser.c` is generated code (see docs/ARCHITECTURE.md). Don't hand-edit it.
+`EditorTextView` (an `NSTextView` subclass) is hosted in SwiftUI through `Representables`. The editing helpers (list/blockquote continuation, auto-pairing, toggling markup, indenting) are in `NSTextView+Autocomplete.swift` and are called from `DocumentController`'s `NSTextViewDelegate` methods. Pasting formatted text (`EditorTextView+Paste.swift`) converts the clipboard's HTML or rich text with `HTMLToMarkdown` (`Document/HTMLToMarkdown`) in `readSelection(from:type:)`, which Paste, Services and drops all go through; see docs/ARCHITECTURE.md, "Pasting". Syntax highlighting is `MarkdownHighlighter`, which styles the visible range from spans keyed by `HighlightElement` (PEG Markdown Highlight's element types), so the original `.style` themes (parsed by `ThemeStyle`) apply. The spans come from the document model's `HighlightMapper`, handed over by `DocumentController` after each parse. `MarkdownHighlighter` and `ThemeStyle` are Swift ports of PEG Markdown Highlight's code (its license stays in `Licenses/`); the C library itself is gone.
## App ↔ shell utility
@@ -68,4 +68,4 @@ Debug builds write a JSON report of each preview render (Prism tokens, TOC links
The bundled help (`Resources/help.md`) has a live example of every Markdown feature and setting, and `HelpDocumentTests` renders it, checks the editor highlighting over it, and loads it in the real preview (Prism, Mermaid, Graphviz, images, task lists). When you add or change a feature, update its section in `help.md` and the matching expectations. Live examples that need a setting use text distinct from the "Inline Formatting" table, which shows results as literal HTML.
-The tests use Swift Testing (`@Suite`/`@Test`), not XCTest, which runs suites in parallel. Suites that drive a real `DocumentController` with a live WKWebView or change `Preferences.shared` (`DocumentControllerTests`, `ScrollSyncIntegrationTests`) are nested in the serialized `LiveDocumentTests` suite so they don't interfere; put new ones there too. WebKit doesn't run animation frames in the (off-screen) test windows, so the page's scroll and layout reports don't fire; tests call `PreviewController.pageDidScroll(to:)` / `pageLayoutDidChange()` instead. Test fixtures are in `Tests/MacDownKitTests/Resources`. The migration corpus (`Resources/Corpus`) is generated test Markdown; never add real user documents or other docs to it. `HTMLDiffHarnessTests` and `HighlightDiffHarnessTests` compare the engines over it and fail on any difference not reviewed (`Resources/reviewed-html-diffs.json`, `HighlightDiffHarnessTests.reviewed`, `Resources/expected-html-diffs.json`); `MACDOWN_DIFF_REPORT=` writes reports (see docs/ARCHITECTURE.md, "Tests"). Live tests that depend on the engine set `Preferences.shared.markdownEngine` themselves.
+The tests use Swift Testing (`@Suite`/`@Test`), not XCTest, which runs suites in parallel. Suites that drive a real `DocumentController` with a live WKWebView or change `Preferences.shared` (`DocumentControllerTests`, `ScrollSyncIntegrationTests`) are nested in the serialized `LiveDocumentTests` suite so they don't interfere; put new ones there too. WebKit doesn't run animation frames in the (off-screen) test windows, so the page's scroll and layout reports don't fire; tests call `PreviewController.pageDidScroll(to:)` / `pageLayoutDidChange()` instead. Test fixtures are in `Tests/MacDownKitTests/Resources`. The migration corpus (`Resources/Corpus`) is generated test Markdown; never add real user documents or other docs to it. `PasteRoundTripTests` converts it back from HTML and fails on any difference not reviewed (`Resources/reviewed-paste-diffs.json`); `MACDOWN_DIFF_REPORT=` writes a report (see docs/ARCHITECTURE.md, "Tests").
diff --git a/Licenses/README.md b/Licenses/README.md
new file mode 100644
index 0000000..98d0e14
--- /dev/null
+++ b/Licenses/README.md
@@ -0,0 +1,17 @@
+# Third-party licenses
+
+What each license here covers in MacDown:
+
+| File | Covers |
+|---|---|
+| `cmark-gfm.txt` | [swift-cmark](https://github.com/swiftlang/swift-cmark) (cmark-gfm and its GFM extensions), the Markdown parser |
+| `peg-markdown-highlight.txt` | The Swift ports of PEG Markdown Highlight's editor highlighter (`MarkdownHighlighter.swift`) and theme parser (`ThemeStyleParser.swift`). The C library itself is no longer used; the ports keep the `.style` themes working |
+| `libyaml.txt` | libyaml, through [Yams](https://github.com/jpsim/Yams), for front matter |
+| `prism.txt` | Prism, syntax highlighting in the preview |
+| `mermaid.txt` | Mermaid diagrams |
+| `graphviz.txt` | Graphviz (viz.js) diagrams |
+| `markdown-mark.txt` | The Markdown mark image in the help |
+| `macdown.txt` | The original MacDown, which this app ports |
+
+hoedown and PEG Markdown Highlight's C parser were removed on 2026-10-22,
+when the move to cmark-gfm finished (docs/MACDOWN-PORT.md).
diff --git a/Licenses/hoedown.txt b/Licenses/hoedown.txt
deleted file mode 100644
index 72ceb93..0000000
--- a/Licenses/hoedown.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-Copyright (c) 2008, Natacha Porté
-Copyright (c) 2011, Vicent Martí
-Copyright (c) 2013, Devin Torres and the Hoedown authors
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Licenses/hoextdown.txt b/Licenses/hoextdown.txt
deleted file mode 100644
index 190e257..0000000
--- a/Licenses/hoextdown.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright (c) 2014 kjdev
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-'Software'), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/Licenses/peg-markdown-highlight.txt b/Licenses/peg-markdown-highlight.txt
index 077df54..2a31bfd 100644
--- a/Licenses/peg-markdown-highlight.txt
+++ b/Licenses/peg-markdown-highlight.txt
@@ -1,3 +1,12 @@
+Note for MacDown: the PEG Markdown Highlight library itself is no longer
+used or shipped. MacDown's editor highlighter
+(Sources/MacDownKit/Editor/MarkdownHighlighter.swift, from
+HGMarkdownHighlighter.m and HGMarkdownHighlightingStyle.m) and its theme
+parser (Sources/MacDownKit/Editor/ThemeStyleParser.swift, from
+pmh_styleparser.c) are Swift ports of its code, kept so that the bundled and
+users' `.style` editor themes keep working after the move to cmark-gfm. This
+license covers those ports.
+
PEG Markdown Highlight
======================
diff --git a/Package.swift b/Package.swift
index 4bfdfba..456c713 100644
--- a/Package.swift
+++ b/Package.swift
@@ -21,30 +21,12 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/jpsim/Yams.git", from: "5.1.0"),
- // The Markdown parser (docs/intents/swift-markdown-migration.md,
- // Decision 10), pinned exactly: the diff harnesses compare its output.
+ // The Markdown parser (see docs/ARCHITECTURE.md, "Parse with cmark-gfm
+ // directly"), pinned exactly: the renderer tests and the paste round
+ // trip compare its output byte for byte.
.package(url: "https://github.com/swiftlang/swift-cmark.git", exact: "0.9.0"),
],
targets: [
- // Hoedown 3.0.7 plus MacDown's renderer patches (task lists, code block
- // information, Prism-compatible code blocks, TOC class).
- .target(
- name: "CHoedown",
- path: "Sources/CHoedown",
- exclude: ["LICENSE"],
- cSettings: [
- .unsafeFlags(["-Wno-everything"]),
- ]
- ),
- // PEG Markdown Highlight. pmh_parser.c is generated from
- // pmh_grammar.leg with greg; see Vendor/README.md.
- .target(
- name: "CPegMarkdown",
- path: "Sources/CPegMarkdown",
- cSettings: [
- .unsafeFlags(["-Wno-everything"]),
- ]
- ),
// Constants shared by the app and the `macdown` command line utility.
.target(
name: "MacDownShared",
@@ -53,8 +35,6 @@ let package = Package(
.target(
name: "MacDownKit",
dependencies: [
- "CHoedown",
- "CPegMarkdown",
"MacDownShared",
.product(name: "Yams", package: "Yams"),
.product(name: "cmark-gfm", package: "swift-cmark"),
diff --git a/Sources/CHoedown/LICENSE b/Sources/CHoedown/LICENSE
deleted file mode 100644
index 4e75de4..0000000
--- a/Sources/CHoedown/LICENSE
+++ /dev/null
@@ -1,15 +0,0 @@
-Copyright (c) 2008, Natacha Porté
-Copyright (c) 2011, Vicent Martí
-Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors
-
-Permission to use, copy, modify, and distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Sources/CHoedown/autolink.c b/Sources/CHoedown/autolink.c
deleted file mode 100644
index e7019fd..0000000
--- a/Sources/CHoedown/autolink.c
+++ /dev/null
@@ -1,281 +0,0 @@
-#include "autolink.h"
-
-#include
-#include
-#include
-#include
-
-#ifndef _MSC_VER
-#include
-#else
-#define strncasecmp _strnicmp
-#endif
-
-int
-hoedown_autolink_is_safe(const uint8_t *data, size_t size)
-{
- static const size_t valid_uris_count = 6;
- static const char *valid_uris[] = {
- "http://", "https://", "/", "#", "ftp://", "mailto:"
- };
- static const size_t valid_uris_size[] = { 7, 8, 1, 1, 6, 7 };
- size_t i;
-
- for (i = 0; i < valid_uris_count; ++i) {
- size_t len = valid_uris_size[i];
-
- if (size > len &&
- strncasecmp((char *)data, valid_uris[i], len) == 0 &&
- isalnum(data[len]))
- return 1;
- }
-
- return 0;
-}
-
-static size_t
-autolink_delim(uint8_t *data, size_t link_end, size_t max_rewind, size_t size)
-{
- uint8_t cclose, copen = 0;
- size_t i;
-
- for (i = 0; i < link_end; ++i)
- if (data[i] == '<') {
- link_end = i;
- break;
- }
-
- while (link_end > 0) {
- if (strchr("?!.,:", data[link_end - 1]) != NULL)
- link_end--;
-
- else if (data[link_end - 1] == ';') {
- size_t new_end = link_end - 2;
-
- while (new_end > 0 && isalpha(data[new_end]))
- new_end--;
-
- if (new_end < link_end - 2 && data[new_end] == '&')
- link_end = new_end;
- else
- link_end--;
- }
- else break;
- }
-
- if (link_end == 0)
- return 0;
-
- cclose = data[link_end - 1];
-
- switch (cclose) {
- case '"': copen = '"'; break;
- case '\'': copen = '\''; break;
- case ')': copen = '('; break;
- case ']': copen = '['; break;
- case '}': copen = '{'; break;
- }
-
- if (copen != 0) {
- size_t closing = 0;
- size_t opening = 0;
- size_t i = 0;
-
- /* Try to close the final punctuation sign in this same line;
- * if we managed to close it outside of the URL, that means that it's
- * not part of the URL. If it closes inside the URL, that means it
- * is part of the URL.
- *
- * Examples:
- *
- * foo http://www.pokemon.com/Pikachu_(Electric) bar
- * => http://www.pokemon.com/Pikachu_(Electric)
- *
- * foo (http://www.pokemon.com/Pikachu_(Electric)) bar
- * => http://www.pokemon.com/Pikachu_(Electric)
- *
- * foo http://www.pokemon.com/Pikachu_(Electric)) bar
- * => http://www.pokemon.com/Pikachu_(Electric))
- *
- * (foo http://www.pokemon.com/Pikachu_(Electric)) bar
- * => foo http://www.pokemon.com/Pikachu_(Electric)
- */
-
- while (i < link_end) {
- if (data[i] == copen)
- opening++;
- else if (data[i] == cclose)
- closing++;
-
- i++;
- }
-
- if (closing != opening)
- link_end--;
- }
-
- return link_end;
-}
-
-static size_t
-check_domain(uint8_t *data, size_t size, int allow_short)
-{
- size_t i, np = 0;
-
- if (!isalnum(data[0]))
- return 0;
-
- for (i = 1; i < size - 1; ++i) {
- if (strchr(".:", data[i]) != NULL) np++;
- else if (!isalnum(data[i]) && data[i] != '-') break;
- }
-
- if (allow_short) {
- /* We don't need a valid domain in the strict sense (with
- * least one dot; so just make sure it's composed of valid
- * domain characters and return the length of the the valid
- * sequence. */
- return i;
- } else {
- /* a valid domain needs to have at least a dot.
- * that's as far as we get */
- return np ? i : 0;
- }
-}
-
-size_t
-hoedown_autolink__www(
- size_t *rewind_p,
- hoedown_buffer *link,
- uint8_t *data,
- size_t max_rewind,
- size_t size,
- unsigned int flags)
-{
- size_t link_end;
-
- if (max_rewind > 0 && !ispunct(data[-1]) && !isspace(data[-1]))
- return 0;
-
- if (size < 4 || memcmp(data, "www.", strlen("www.")) != 0)
- return 0;
-
- link_end = check_domain(data, size, 0);
-
- if (link_end == 0)
- return 0;
-
- while (link_end < size && !isspace(data[link_end]))
- link_end++;
-
- link_end = autolink_delim(data, link_end, max_rewind, size);
-
- if (link_end == 0)
- return 0;
-
- hoedown_buffer_put(link, data, link_end);
- *rewind_p = 0;
-
- return (int)link_end;
-}
-
-size_t
-hoedown_autolink__email(
- size_t *rewind_p,
- hoedown_buffer *link,
- uint8_t *data,
- size_t max_rewind,
- size_t size,
- unsigned int flags)
-{
- size_t link_end, rewind;
- int nb = 0, np = 0;
-
- for (rewind = 0; rewind < max_rewind; ++rewind) {
- uint8_t c = data[-1 - rewind];
-
- if (isalnum(c))
- continue;
-
- if (strchr(".+-_", c) != NULL)
- continue;
-
- break;
- }
-
- if (rewind == 0)
- return 0;
-
- for (link_end = 0; link_end < size; ++link_end) {
- uint8_t c = data[link_end];
-
- if (isalnum(c))
- continue;
-
- if (c == '@')
- nb++;
- else if (c == '.' && link_end < size - 1)
- np++;
- else if (c != '-' && c != '_')
- break;
- }
-
- if (link_end < 2 || nb != 1 || np == 0 ||
- !isalpha(data[link_end - 1]))
- return 0;
-
- link_end = autolink_delim(data, link_end, max_rewind, size);
-
- if (link_end == 0)
- return 0;
-
- hoedown_buffer_put(link, data - rewind, link_end + rewind);
- *rewind_p = rewind;
-
- return link_end;
-}
-
-size_t
-hoedown_autolink__url(
- size_t *rewind_p,
- hoedown_buffer *link,
- uint8_t *data,
- size_t max_rewind,
- size_t size,
- unsigned int flags)
-{
- size_t link_end, rewind = 0, domain_len;
-
- if (size < 4 || data[1] != '/' || data[2] != '/')
- return 0;
-
- while (rewind < max_rewind && isalpha(data[-1 - rewind]))
- rewind++;
-
- if (!hoedown_autolink_is_safe(data - rewind, size + rewind))
- return 0;
-
- link_end = strlen("://");
-
- domain_len = check_domain(
- data + link_end,
- size - link_end,
- flags & HOEDOWN_AUTOLINK_SHORT_DOMAINS);
-
- if (domain_len == 0)
- return 0;
-
- link_end += domain_len;
- while (link_end < size && !isspace(data[link_end]))
- link_end++;
-
- link_end = autolink_delim(data, link_end, max_rewind, size);
-
- if (link_end == 0)
- return 0;
-
- hoedown_buffer_put(link, data - rewind, link_end + rewind);
- *rewind_p = rewind;
-
- return link_end;
-}
diff --git a/Sources/CHoedown/buffer.c b/Sources/CHoedown/buffer.c
deleted file mode 100644
index af77b70..0000000
--- a/Sources/CHoedown/buffer.c
+++ /dev/null
@@ -1,308 +0,0 @@
-#include "buffer.h"
-
-#include
-#include
-#include
-#include
-
-void *
-hoedown_malloc(size_t size)
-{
- void *ret = malloc(size);
-
- if (!ret) {
- fprintf(stderr, "Allocation failed.\n");
- abort();
- }
-
- return ret;
-}
-
-void *
-hoedown_calloc(size_t nmemb, size_t size)
-{
- void *ret = calloc(nmemb, size);
-
- if (!ret) {
- fprintf(stderr, "Allocation failed.\n");
- abort();
- }
-
- return ret;
-}
-
-void *
-hoedown_realloc(void *ptr, size_t size)
-{
- void *ret = realloc(ptr, size);
-
- if (!ret) {
- fprintf(stderr, "Allocation failed.\n");
- abort();
- }
-
- return ret;
-}
-
-void
-hoedown_buffer_init(
- hoedown_buffer *buf,
- size_t unit,
- hoedown_realloc_callback data_realloc,
- hoedown_free_callback data_free,
- hoedown_free_callback buffer_free)
-{
- assert(buf);
-
- buf->data = NULL;
- buf->size = buf->asize = 0;
- buf->unit = unit;
- buf->data_realloc = data_realloc;
- buf->data_free = data_free;
- buf->buffer_free = buffer_free;
-}
-
-void
-hoedown_buffer_uninit(hoedown_buffer *buf)
-{
- assert(buf && buf->unit);
- buf->data_free(buf->data);
-}
-
-hoedown_buffer *
-hoedown_buffer_new(size_t unit)
-{
- hoedown_buffer *ret = hoedown_malloc(sizeof (hoedown_buffer));
- hoedown_buffer_init(ret, unit, hoedown_realloc, free, free);
- return ret;
-}
-
-void
-hoedown_buffer_free(hoedown_buffer *buf)
-{
- if (!buf) return;
- assert(buf && buf->unit);
-
- buf->data_free(buf->data);
-
- if (buf->buffer_free)
- buf->buffer_free(buf);
-}
-
-void
-hoedown_buffer_reset(hoedown_buffer *buf)
-{
- assert(buf && buf->unit);
-
- buf->data_free(buf->data);
- buf->data = NULL;
- buf->size = buf->asize = 0;
-}
-
-void
-hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz)
-{
- size_t neoasz;
- assert(buf && buf->unit);
-
- if (buf->asize >= neosz)
- return;
-
- neoasz = buf->asize + buf->unit;
- while (neoasz < neosz)
- neoasz += buf->unit;
-
- buf->data = buf->data_realloc(buf->data, neoasz);
- buf->asize = neoasz;
-}
-
-void
-hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size)
-{
- assert(buf && buf->unit);
-
- if (buf->size + size > buf->asize)
- hoedown_buffer_grow(buf, buf->size + size);
-
- memcpy(buf->data + buf->size, data, size);
- buf->size += size;
-}
-
-void
-hoedown_buffer_puts(hoedown_buffer *buf, const char *str)
-{
- hoedown_buffer_put(buf, (const uint8_t *)str, strlen(str));
-}
-
-void
-hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c)
-{
- assert(buf && buf->unit);
-
- if (buf->size >= buf->asize)
- hoedown_buffer_grow(buf, buf->size + 1);
-
- buf->data[buf->size] = c;
- buf->size += 1;
-}
-
-int
-hoedown_buffer_putf(hoedown_buffer *buf, FILE *file)
-{
- assert(buf && buf->unit);
-
- while (!(feof(file) || ferror(file))) {
- hoedown_buffer_grow(buf, buf->size + buf->unit);
- buf->size += fread(buf->data + buf->size, 1, buf->unit, file);
- }
-
- return ferror(file);
-}
-
-void
-hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size)
-{
- assert(buf && buf->unit);
-
- if (size > buf->asize)
- hoedown_buffer_grow(buf, size);
-
- memcpy(buf->data, data, size);
- buf->size = size;
-}
-
-void
-hoedown_buffer_sets(hoedown_buffer *buf, const char *str)
-{
- hoedown_buffer_set(buf, (const uint8_t *)str, strlen(str));
-}
-
-int
-hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size)
-{
- if (buf->size != size) return 0;
- return memcmp(buf->data, data, size) == 0;
-}
-
-int
-hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str)
-{
- return hoedown_buffer_eq(buf, (const uint8_t *)str, strlen(str));
-}
-
-int
-hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix)
-{
- size_t i;
-
- for (i = 0; i < buf->size; ++i) {
- if (prefix[i] == 0)
- return 0;
-
- if (buf->data[i] != prefix[i])
- return buf->data[i] - prefix[i];
- }
-
- return 0;
-}
-
-void
-hoedown_buffer_slurp(hoedown_buffer *buf, size_t size)
-{
- assert(buf && buf->unit);
-
- if (size >= buf->size) {
- buf->size = 0;
- return;
- }
-
- buf->size -= size;
- memmove(buf->data, buf->data + size, buf->size);
-}
-
-const char *
-hoedown_buffer_cstr(hoedown_buffer *buf)
-{
- assert(buf && buf->unit);
-
- if (buf->size < buf->asize && buf->data[buf->size] == 0)
- return (char *)buf->data;
-
- hoedown_buffer_grow(buf, buf->size + 1);
- buf->data[buf->size] = 0;
-
- return (char *)buf->data;
-}
-
-void
-hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...)
-{
- va_list ap;
- int n;
-
- assert(buf && buf->unit);
-
- if (buf->size >= buf->asize)
- hoedown_buffer_grow(buf, buf->size + 1);
-
- va_start(ap, fmt);
- n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap);
- va_end(ap);
-
- if (n < 0) {
-#ifndef _MSC_VER
- return;
-#else
- va_start(ap, fmt);
- n = _vscprintf(fmt, ap);
- va_end(ap);
-#endif
- }
-
- if ((size_t)n >= buf->asize - buf->size) {
- hoedown_buffer_grow(buf, buf->size + n + 1);
-
- va_start(ap, fmt);
- n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap);
- va_end(ap);
- }
-
- if (n < 0)
- return;
-
- buf->size += n;
-}
-
-void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int c) {
- unsigned char unichar[4];
-
- assert(buf && buf->unit);
-
- if (c < 0x80) {
- hoedown_buffer_putc(buf, c);
- }
- else if (c < 0x800) {
- unichar[0] = 192 + (c / 64);
- unichar[1] = 128 + (c % 64);
- hoedown_buffer_put(buf, unichar, 2);
- }
- else if (c - 0xd800u < 0x800) {
- HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd");
- }
- else if (c < 0x10000) {
- unichar[0] = 224 + (c / 4096);
- unichar[1] = 128 + (c / 64) % 64;
- unichar[2] = 128 + (c % 64);
- hoedown_buffer_put(buf, unichar, 3);
- }
- else if (c < 0x110000) {
- unichar[0] = 240 + (c / 262144);
- unichar[1] = 128 + (c / 4096) % 64;
- unichar[2] = 128 + (c / 64) % 64;
- unichar[3] = 128 + (c % 64);
- hoedown_buffer_put(buf, unichar, 4);
- }
- else {
- HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd");
- }
-}
diff --git a/Sources/CHoedown/document.c b/Sources/CHoedown/document.c
deleted file mode 100644
index c088615..0000000
--- a/Sources/CHoedown/document.c
+++ /dev/null
@@ -1,2994 +0,0 @@
-#include "document.h"
-
-#include
-#include
-#include
-#include
-
-#include "stack.h"
-
-#ifndef _MSC_VER
-#include
-#else
-#define strncasecmp _strnicmp
-#endif
-
-#define REF_TABLE_SIZE 8
-
-#define BUFFER_BLOCK 0
-#define BUFFER_SPAN 1
-
-#define HOEDOWN_LI_END 8 /* internal list flag */
-
-const char *hoedown_find_block_tag(const char *str, unsigned int len);
-
-/***************
- * LOCAL TYPES *
- ***************/
-
-/* link_ref: reference to a link */
-struct link_ref {
- unsigned int id;
-
- hoedown_buffer *link;
- hoedown_buffer *title;
-
- struct link_ref *next;
-};
-
-/* footnote_ref: reference to a footnote */
-struct footnote_ref {
- unsigned int id;
-
- int is_used;
- unsigned int num;
-
- hoedown_buffer *contents;
-};
-
-/* footnote_item: an item in a footnote_list */
-struct footnote_item {
- struct footnote_ref *ref;
- struct footnote_item *next;
-};
-
-/* footnote_list: linked list of footnote_item */
-struct footnote_list {
- unsigned int count;
- struct footnote_item *head;
- struct footnote_item *tail;
-};
-
-/* char_trigger: function pointer to render active chars */
-/* returns the number of chars taken care of */
-/* data is the pointer of the beginning of the span */
-/* offset is the number of valid chars before data */
-typedef size_t
-(*char_trigger)(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-
-static size_t char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-static size_t char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size);
-
-enum markdown_char_t {
- MD_CHAR_NONE = 0,
- MD_CHAR_EMPHASIS,
- MD_CHAR_CODESPAN,
- MD_CHAR_LINEBREAK,
- MD_CHAR_LINK,
- MD_CHAR_LANGLE,
- MD_CHAR_ESCAPE,
- MD_CHAR_ENTITY,
- MD_CHAR_AUTOLINK_URL,
- MD_CHAR_AUTOLINK_EMAIL,
- MD_CHAR_AUTOLINK_WWW,
- MD_CHAR_SUPERSCRIPT,
- MD_CHAR_QUOTE,
- MD_CHAR_MATH
-};
-
-static char_trigger markdown_char_ptrs[] = {
- NULL,
- &char_emphasis,
- &char_codespan,
- &char_linebreak,
- &char_link,
- &char_langle_tag,
- &char_escape,
- &char_entity,
- &char_autolink_url,
- &char_autolink_email,
- &char_autolink_www,
- &char_superscript,
- &char_quote,
- &char_math
-};
-
-struct hoedown_document {
- hoedown_renderer md;
- hoedown_renderer_data data;
-
- struct link_ref *refs[REF_TABLE_SIZE];
- struct footnote_list footnotes_found;
- struct footnote_list footnotes_used;
- uint8_t active_char[256];
- hoedown_stack work_bufs[2];
- hoedown_extensions ext_flags;
- size_t max_nesting;
- int in_link_body;
-};
-
-/***************************
- * HELPER FUNCTIONS *
- ***************************/
-
-static hoedown_buffer *
-newbuf(hoedown_document *doc, int type)
-{
- static const size_t buf_size[2] = {256, 64};
- hoedown_buffer *work = NULL;
- hoedown_stack *pool = &doc->work_bufs[type];
-
- if (pool->size < pool->asize &&
- pool->item[pool->size] != NULL) {
- work = pool->item[pool->size++];
- work->size = 0;
- } else {
- work = hoedown_buffer_new(buf_size[type]);
- hoedown_stack_push(pool, work);
- }
-
- return work;
-}
-
-static void
-popbuf(hoedown_document *doc, int type)
-{
- doc->work_bufs[type].size--;
-}
-
-static void
-unscape_text(hoedown_buffer *ob, hoedown_buffer *src)
-{
- size_t i = 0, org;
- while (i < src->size) {
- org = i;
- while (i < src->size && src->data[i] != '\\')
- i++;
-
- if (i > org)
- hoedown_buffer_put(ob, src->data + org, i - org);
-
- if (i + 1 >= src->size)
- break;
-
- hoedown_buffer_putc(ob, src->data[i + 1]);
- i += 2;
- }
-}
-
-static unsigned int
-hash_link_ref(const uint8_t *link_ref, size_t length)
-{
- size_t i;
- unsigned int hash = 0;
-
- for (i = 0; i < length; ++i)
- hash = tolower(link_ref[i]) + (hash << 6) + (hash << 16) - hash;
-
- return hash;
-}
-
-static struct link_ref *
-add_link_ref(
- struct link_ref **references,
- const uint8_t *name, size_t name_size)
-{
- struct link_ref *ref = hoedown_calloc(1, sizeof(struct link_ref));
-
- ref->id = hash_link_ref(name, name_size);
- ref->next = references[ref->id % REF_TABLE_SIZE];
-
- references[ref->id % REF_TABLE_SIZE] = ref;
- return ref;
-}
-
-static struct link_ref *
-find_link_ref(struct link_ref **references, uint8_t *name, size_t length)
-{
- unsigned int hash = hash_link_ref(name, length);
- struct link_ref *ref = NULL;
-
- ref = references[hash % REF_TABLE_SIZE];
-
- while (ref != NULL) {
- if (ref->id == hash)
- return ref;
-
- ref = ref->next;
- }
-
- return NULL;
-}
-
-static void
-free_link_refs(struct link_ref **references)
-{
- size_t i;
-
- for (i = 0; i < REF_TABLE_SIZE; ++i) {
- struct link_ref *r = references[i];
- struct link_ref *next;
-
- while (r) {
- next = r->next;
- hoedown_buffer_free(r->link);
- hoedown_buffer_free(r->title);
- free(r);
- r = next;
- }
- }
-}
-
-static struct footnote_ref *
-create_footnote_ref(struct footnote_list *list, const uint8_t *name, size_t name_size)
-{
- struct footnote_ref *ref = hoedown_calloc(1, sizeof(struct footnote_ref));
-
- ref->id = hash_link_ref(name, name_size);
-
- return ref;
-}
-
-static int
-add_footnote_ref(struct footnote_list *list, struct footnote_ref *ref)
-{
- struct footnote_item *item = hoedown_calloc(1, sizeof(struct footnote_item));
- if (!item)
- return 0;
- item->ref = ref;
-
- if (list->head == NULL) {
- list->head = list->tail = item;
- } else {
- list->tail->next = item;
- list->tail = item;
- }
- list->count++;
-
- return 1;
-}
-
-static struct footnote_ref *
-find_footnote_ref(struct footnote_list *list, uint8_t *name, size_t length)
-{
- unsigned int hash = hash_link_ref(name, length);
- struct footnote_item *item = NULL;
-
- item = list->head;
-
- while (item != NULL) {
- if (item->ref->id == hash)
- return item->ref;
- item = item->next;
- }
-
- return NULL;
-}
-
-static void
-free_footnote_ref(struct footnote_ref *ref)
-{
- hoedown_buffer_free(ref->contents);
- free(ref);
-}
-
-static void
-free_footnote_list(struct footnote_list *list, int free_refs)
-{
- struct footnote_item *item = list->head;
- struct footnote_item *next;
-
- while (item) {
- next = item->next;
- if (free_refs)
- free_footnote_ref(item->ref);
- free(item);
- item = next;
- }
-}
-
-
-/*
- * Check whether a char is a Markdown spacing char.
-
- * Right now we only consider spaces the actual
- * space and a newline: tabs and carriage returns
- * are filtered out during the preprocessing phase.
- *
- * If we wanted to actually be UTF-8 compliant, we
- * should instead extract an Unicode codepoint from
- * this character and check for space properties.
- */
-static int
-_isspace(int c)
-{
- return c == ' ' || c == '\n';
-}
-
-/* is_empty_all: verify that all the data is spacing */
-static int
-is_empty_all(const uint8_t *data, size_t size)
-{
- size_t i = 0;
- while (i < size && _isspace(data[i])) i++;
- return i == size;
-}
-
-/*
- * Replace all spacing characters in data with spaces. As a special
- * case, this collapses a newline with the previous space, if possible.
- */
-static void
-replace_spacing(hoedown_buffer *ob, const uint8_t *data, size_t size)
-{
- size_t i = 0, mark;
- hoedown_buffer_grow(ob, size);
- while (1) {
- mark = i;
- while (i < size && data[i] != '\n') i++;
- hoedown_buffer_put(ob, data + mark, i - mark);
-
- if (i >= size) break;
-
- if (!(i > 0 && data[i-1] == ' '))
- hoedown_buffer_putc(ob, ' ');
- i++;
- }
-}
-
-/****************************
- * INLINE PARSING FUNCTIONS *
- ****************************/
-
-/* is_mail_autolink • looks for the address part of a mail autolink and '>' */
-/* this is less strict than the original markdown e-mail address matching */
-static size_t
-is_mail_autolink(uint8_t *data, size_t size)
-{
- size_t i = 0, nb = 0;
-
- /* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */
- for (i = 0; i < size; ++i) {
- if (isalnum(data[i]))
- continue;
-
- switch (data[i]) {
- case '@':
- nb++;
-
- case '-':
- case '.':
- case '_':
- break;
-
- case '>':
- return (nb == 1) ? i + 1 : 0;
-
- default:
- return 0;
- }
- }
-
- return 0;
-}
-
-/* tag_length • returns the length of the given tag, or 0 is it's not valid */
-static size_t
-tag_length(uint8_t *data, size_t size, hoedown_autolink_type *autolink)
-{
- size_t i, j;
-
- /* a valid tag can't be shorter than 3 chars */
- if (size < 3) return 0;
-
- if (data[0] != '<') return 0;
-
- /* HTML comment, laxist form */
- if (size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') {
- i = 5;
-
- while (i < size && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>'))
- i++;
-
- i++;
-
- if (i <= size)
- return i;
- }
-
- /* begins with a '<' optionally followed by '/', followed by letter or number */
- i = (data[1] == '/') ? 2 : 1;
-
- if (!isalnum(data[i]))
- return 0;
-
- /* scheme test */
- *autolink = HOEDOWN_AUTOLINK_NONE;
-
- /* try to find the beginning of an URI */
- while (i < size && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-'))
- i++;
-
- if (i > 1 && data[i] == '@') {
- if ((j = is_mail_autolink(data + i, size - i)) != 0) {
- *autolink = HOEDOWN_AUTOLINK_EMAIL;
- return i + j;
- }
- }
-
- if (i > 2 && data[i] == ':') {
- *autolink = HOEDOWN_AUTOLINK_NORMAL;
- i++;
- }
-
- /* completing autolink test: no spacing or ' or " */
- if (i >= size)
- *autolink = HOEDOWN_AUTOLINK_NONE;
-
- else if (*autolink) {
- j = i;
-
- while (i < size) {
- if (data[i] == '\\') i += 2;
- else if (data[i] == '>' || data[i] == '\'' ||
- data[i] == '"' || data[i] == ' ' || data[i] == '\n')
- break;
- else i++;
- }
-
- if (i >= size) return 0;
- if (i > j && data[i] == '>') return i + 1;
- /* one of the forbidden chars has been found */
- *autolink = HOEDOWN_AUTOLINK_NONE;
- }
-
- /* looking for something looking like a tag end */
- while (i < size && data[i] != '>') i++;
- if (i >= size) return 0;
- return i + 1;
-}
-
-/* parse_inline • parses inline markdown elements */
-static void
-parse_inline(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t i = 0, end = 0, consumed = 0;
- hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL };
- uint8_t *active_char = doc->active_char;
-
- if (doc->work_bufs[BUFFER_SPAN].size +
- doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting)
- return;
-
- while (i < size) {
- /* copying inactive chars into the output */
- while (end < size && active_char[data[end]] == 0)
- end++;
-
- if (doc->md.normal_text) {
- work.data = data + i;
- work.size = end - i;
- doc->md.normal_text(ob, &work, &doc->data);
- }
- else
- hoedown_buffer_put(ob, data + i, end - i);
-
- if (end >= size) break;
- i = end;
-
- end = markdown_char_ptrs[ (int)active_char[data[end]] ](ob, doc, data + i, i - consumed, size - i);
- if (!end) /* no action from the callback */
- end = i + 1;
- else {
- i += end;
- end = i;
- consumed = i;
- }
- }
-}
-
-/* is_escaped • returns whether special char at data[loc] is escaped by '\\' */
-static int
-is_escaped(uint8_t *data, size_t loc)
-{
- size_t i = loc;
- while (i >= 1 && data[i - 1] == '\\')
- i--;
-
- /* odd numbers of backslashes escapes data[loc] */
- return (loc - i) % 2;
-}
-
-/* find_emph_char • looks for the next emph uint8_t, skipping other constructs */
-static size_t
-find_emph_char(uint8_t *data, size_t size, uint8_t c)
-{
- size_t i = 0;
-
- while (i < size) {
- while (i < size && data[i] != c && data[i] != '[' && data[i] != '`')
- i++;
-
- if (i == size)
- return 0;
-
- /* not counting escaped chars */
- if (is_escaped(data, i)) {
- i++; continue;
- }
-
- if (data[i] == c)
- return i;
-
- /* skipping a codespan */
- if (data[i] == '`') {
- size_t span_nb = 0, bt;
- size_t tmp_i = 0;
-
- /* counting the number of opening backticks */
- while (i < size && data[i] == '`') {
- i++; span_nb++;
- }
-
- if (i >= size) return 0;
-
- /* finding the matching closing sequence */
- bt = 0;
- while (i < size && bt < span_nb) {
- if (!tmp_i && data[i] == c) tmp_i = i;
- if (data[i] == '`') bt++;
- else bt = 0;
- i++;
- }
-
- /* not a well-formed codespan; use found matching emph char */
- if (bt < span_nb && i >= size) return tmp_i;
- }
- /* skipping a link */
- else if (data[i] == '[') {
- size_t tmp_i = 0;
- uint8_t cc;
-
- i++;
- while (i < size && data[i] != ']') {
- if (!tmp_i && data[i] == c) tmp_i = i;
- i++;
- }
-
- i++;
- while (i < size && _isspace(data[i]))
- i++;
-
- if (i >= size)
- return tmp_i;
-
- switch (data[i]) {
- case '[':
- cc = ']'; break;
-
- case '(':
- cc = ')'; break;
-
- default:
- if (tmp_i)
- return tmp_i;
- else
- continue;
- }
-
- i++;
- while (i < size && data[i] != cc) {
- if (!tmp_i && data[i] == c) tmp_i = i;
- i++;
- }
-
- if (i >= size)
- return tmp_i;
-
- i++;
- }
- }
-
- return 0;
-}
-
-/* parse_emph1 • parsing single emphase */
-/* closed by a symbol not preceded by spacing and not followed by symbol */
-static size_t
-parse_emph1(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c)
-{
- size_t i = 0, len;
- hoedown_buffer *work = 0;
- int r;
-
- /* skipping one symbol if coming from emph3 */
- if (size > 1 && data[0] == c && data[1] == c) i = 1;
-
- while (i < size) {
- len = find_emph_char(data + i, size - i, c);
- if (!len) return 0;
- i += len;
- if (i >= size) return 0;
-
- if (data[i] == c && !_isspace(data[i - 1])) {
-
- if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) {
- if (i + 1 < size && isalnum(data[i + 1]))
- continue;
- }
-
- work = newbuf(doc, BUFFER_SPAN);
- parse_inline(work, doc, data, i);
-
- if (doc->ext_flags & HOEDOWN_EXT_UNDERLINE && c == '_')
- r = doc->md.underline(ob, work, &doc->data);
- else
- r = doc->md.emphasis(ob, work, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- return r ? i + 1 : 0;
- }
- }
-
- return 0;
-}
-
-/* parse_emph2 • parsing single emphase */
-static size_t
-parse_emph2(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c)
-{
- size_t i = 0, len;
- hoedown_buffer *work = 0;
- int r;
-
- while (i < size) {
- len = find_emph_char(data + i, size - i, c);
- if (!len) return 0;
- i += len;
-
- if (i + 1 < size && data[i] == c && data[i + 1] == c && i && !_isspace(data[i - 1])) {
- work = newbuf(doc, BUFFER_SPAN);
- parse_inline(work, doc, data, i);
-
- if (c == '~')
- r = doc->md.strikethrough(ob, work, &doc->data);
- else if (c == '=')
- r = doc->md.highlight(ob, work, &doc->data);
- else
- r = doc->md.double_emphasis(ob, work, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- return r ? i + 2 : 0;
- }
- i++;
- }
- return 0;
-}
-
-/* parse_emph3 • parsing single emphase */
-/* finds the first closing tag, and delegates to the other emph */
-static size_t
-parse_emph3(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c)
-{
- size_t i = 0, len;
- int r;
-
- while (i < size) {
- len = find_emph_char(data + i, size - i, c);
- if (!len) return 0;
- i += len;
-
- /* skip spacing preceded symbols */
- if (data[i] != c || _isspace(data[i - 1]))
- continue;
-
- if (i + 2 < size && data[i + 1] == c && data[i + 2] == c && doc->md.triple_emphasis) {
- /* triple symbol found */
- hoedown_buffer *work = newbuf(doc, BUFFER_SPAN);
-
- parse_inline(work, doc, data, i);
- r = doc->md.triple_emphasis(ob, work, &doc->data);
- popbuf(doc, BUFFER_SPAN);
- return r ? i + 3 : 0;
-
- } else if (i + 1 < size && data[i + 1] == c) {
- /* double symbol found, handing over to emph1 */
- len = parse_emph1(ob, doc, data - 2, size + 2, c);
- if (!len) return 0;
- else return len - 2;
-
- } else {
- /* single symbol found, handing over to emph2 */
- len = parse_emph2(ob, doc, data - 1, size + 1, c);
- if (!len) return 0;
- else return len - 1;
- }
- }
- return 0;
-}
-
-/* parse_math • parses a math span until the given ending delimiter */
-static size_t
-parse_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size, const char *end, size_t delimsz, int displaymode)
-{
- hoedown_buffer text = { NULL, 0, 0, 0, NULL, NULL, NULL };
- size_t i = delimsz;
-
- if (!doc->md.math)
- return 0;
-
- /* find ending delimiter */
- while (1) {
- while (i < size && data[i] != (uint8_t)end[0])
- i++;
-
- if (i >= size)
- return 0;
-
- if (!is_escaped(data, i) && !(i + delimsz > size)
- && memcmp(data + i, end, delimsz) == 0)
- break;
-
- i++;
- }
-
- /* prepare buffers */
- text.data = data + delimsz;
- text.size = i - delimsz;
-
- /* if this is a $$ and MATH_EXPLICIT is not active,
- * guess whether displaymode should be enabled from the context */
- i += delimsz;
- if (delimsz == 2 && !(doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT))
- displaymode = is_empty_all(data - offset, offset) && is_empty_all(data + i, size - i);
-
- /* call callback */
- if (doc->md.math(ob, &text, displaymode, &doc->data))
- return i;
-
- return 0;
-}
-
-/* char_emphasis • single and double emphasis parsing */
-static size_t
-char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- uint8_t c = data[0];
- size_t ret;
-
- if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) {
- if (offset > 0 && !_isspace(data[-1]) && data[-1] != '>' && data[-1] != '(')
- return 0;
- }
-
- if (size > 2 && data[1] != c) {
- /* spacing cannot follow an opening emphasis;
- * strikethrough and highlight only takes two characters '~~' */
- if (c == '~' || c == '=' || _isspace(data[1]) || (ret = parse_emph1(ob, doc, data + 1, size - 1, c)) == 0)
- return 0;
-
- return ret + 1;
- }
-
- if (size > 3 && data[1] == c && data[2] != c) {
- if (_isspace(data[2]) || (ret = parse_emph2(ob, doc, data + 2, size - 2, c)) == 0)
- return 0;
-
- return ret + 2;
- }
-
- if (size > 4 && data[1] == c && data[2] == c && data[3] != c) {
- if (c == '~' || c == '=' || _isspace(data[3]) || (ret = parse_emph3(ob, doc, data + 3, size - 3, c)) == 0)
- return 0;
-
- return ret + 3;
- }
-
- return 0;
-}
-
-
-/* char_linebreak • '\n' preceded by two spaces (assuming linebreak != 0) */
-static size_t
-char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- if (offset < 2 || data[-1] != ' ' || data[-2] != ' ')
- return 0;
-
- /* removing the last space from ob and rendering */
- while (ob->size && ob->data[ob->size - 1] == ' ')
- ob->size--;
-
- return doc->md.linebreak(ob, &doc->data) ? 1 : 0;
-}
-
-
-/* char_codespan • '`' parsing a code span (assuming codespan != 0) */
-static size_t
-char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL };
- size_t end, nb = 0, i, f_begin, f_end;
-
- /* counting the number of backticks in the delimiter */
- while (nb < size && data[nb] == '`')
- nb++;
-
- /* finding the next delimiter */
- i = 0;
- for (end = nb; end < size && i < nb; end++) {
- if (data[end] == '`') i++;
- else i = 0;
- }
-
- if (i < nb && end >= size)
- return 0; /* no matching delimiter */
-
- /* trimming outside spaces */
- f_begin = nb;
- while (f_begin < end && data[f_begin] == ' ')
- f_begin++;
-
- f_end = end - nb;
- while (f_end > nb && data[f_end-1] == ' ')
- f_end--;
-
- /* real code span */
- if (f_begin < f_end) {
- work.data = data + f_begin;
- work.size = f_end - f_begin;
-
- if (!doc->md.codespan(ob, &work, &doc->data))
- end = 0;
- } else {
- if (!doc->md.codespan(ob, 0, &doc->data))
- end = 0;
- }
-
- return end;
-}
-
-/* char_quote • '"' parsing a quote */
-static size_t
-char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- size_t end, nq = 0, i, f_begin, f_end;
-
- /* counting the number of quotes in the delimiter */
- while (nq < size && data[nq] == '"')
- nq++;
-
- /* finding the next delimiter */
- end = nq;
- while (1) {
- i = end;
- end += find_emph_char(data + end, size - end, '"');
- if (end == i) return 0; /* no matching delimiter */
- i = end;
- while (end < size && data[end] == '"' && end - i < nq) end++;
- if (end - i >= nq) break;
- }
-
- /* trimming outside spaces */
- f_begin = nq;
- while (f_begin < end && data[f_begin] == ' ')
- f_begin++;
-
- f_end = end - nq;
- while (f_end > nq && data[f_end-1] == ' ')
- f_end--;
-
- /* real quote */
- if (f_begin < f_end) {
- hoedown_buffer *work = newbuf(doc, BUFFER_SPAN);
- parse_inline(work, doc, data + f_begin, f_end - f_begin);
-
- if (!doc->md.quote(ob, work, &doc->data))
- end = 0;
- popbuf(doc, BUFFER_SPAN);
- } else {
- if (!doc->md.quote(ob, 0, &doc->data))
- end = 0;
- }
-
- return end;
-}
-
-
-/* char_escape • '\\' backslash escape */
-static size_t
-char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- static const char *escape_chars = "\\`*_{}[]()#+-.!:|&<>^~=\"$";
- hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL };
- size_t w;
-
- if (size > 1) {
- if (data[1] == '\\' && (doc->ext_flags & HOEDOWN_EXT_MATH) &&
- size > 2 && (data[2] == '(' || data[2] == '[')) {
- const char *end = (data[2] == '[') ? "\\\\]" : "\\\\)";
- w = parse_math(ob, doc, data, offset, size, end, 3, data[2] == '[');
- if (w) return w;
- }
-
- if (strchr(escape_chars, data[1]) == NULL)
- return 0;
-
- if (doc->md.normal_text) {
- work.data = data + 1;
- work.size = 1;
- doc->md.normal_text(ob, &work, &doc->data);
- }
- else hoedown_buffer_putc(ob, data[1]);
- } else if (size == 1) {
- hoedown_buffer_putc(ob, data[0]);
- }
-
- return 2;
-}
-
-/* char_entity • '&' escaped when it doesn't belong to an entity */
-/* valid entities are assumed to be anything matching ?[A-Za-z0-9]+; */
-static size_t
-char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- size_t end = 1;
- hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL };
-
- if (end < size && data[end] == '#')
- end++;
-
- while (end < size && isalnum(data[end]))
- end++;
-
- if (end < size && data[end] == ';')
- end++; /* real entity */
- else
- return 0; /* lone '&' */
-
- if (doc->md.entity) {
- work.data = data;
- work.size = end;
- doc->md.entity(ob, &work, &doc->data);
- }
- else hoedown_buffer_put(ob, data, end);
-
- return end;
-}
-
-/* char_langle_tag • '<' when tags or autolinks are allowed */
-static size_t
-char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL };
- hoedown_autolink_type altype = HOEDOWN_AUTOLINK_NONE;
- size_t end = tag_length(data, size, &altype);
- int ret = 0;
-
- work.data = data;
- work.size = end;
-
- if (end > 2) {
- if (doc->md.autolink && altype != HOEDOWN_AUTOLINK_NONE) {
- hoedown_buffer *u_link = newbuf(doc, BUFFER_SPAN);
- work.data = data + 1;
- work.size = end - 2;
- unscape_text(u_link, &work);
- ret = doc->md.autolink(ob, u_link, altype, &doc->data);
- popbuf(doc, BUFFER_SPAN);
- }
- else if (doc->md.raw_html)
- ret = doc->md.raw_html(ob, &work, &doc->data);
- }
-
- if (!ret) return 0;
- else return end;
-}
-
-static size_t
-char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer *link, *link_url, *link_text;
- size_t link_len, rewind;
-
- if (!doc->md.link || doc->in_link_body)
- return 0;
-
- link = newbuf(doc, BUFFER_SPAN);
-
- if ((link_len = hoedown_autolink__www(&rewind, link, data, offset, size, HOEDOWN_AUTOLINK_SHORT_DOMAINS)) > 0) {
- link_url = newbuf(doc, BUFFER_SPAN);
- HOEDOWN_BUFPUTSL(link_url, "http://");
- hoedown_buffer_put(link_url, link->data, link->size);
-
- if (ob->size > rewind)
- ob->size -= rewind;
- else
- ob->size = 0;
-
- if (doc->md.normal_text) {
- link_text = newbuf(doc, BUFFER_SPAN);
- doc->md.normal_text(link_text, link, &doc->data);
- doc->md.link(ob, link_text, link_url, NULL, &doc->data);
- popbuf(doc, BUFFER_SPAN);
- } else {
- doc->md.link(ob, link, link_url, NULL, &doc->data);
- }
- popbuf(doc, BUFFER_SPAN);
- }
-
- popbuf(doc, BUFFER_SPAN);
- return link_len;
-}
-
-static size_t
-char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer *link;
- size_t link_len, rewind;
-
- if (!doc->md.autolink || doc->in_link_body)
- return 0;
-
- link = newbuf(doc, BUFFER_SPAN);
-
- if ((link_len = hoedown_autolink__email(&rewind, link, data, offset, size, 0)) > 0) {
- if (ob->size > rewind)
- ob->size -= rewind;
- else
- ob->size = 0;
-
- doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_EMAIL, &doc->data);
- }
-
- popbuf(doc, BUFFER_SPAN);
- return link_len;
-}
-
-static size_t
-char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- hoedown_buffer *link;
- size_t link_len, rewind;
-
- if (!doc->md.autolink || doc->in_link_body)
- return 0;
-
- link = newbuf(doc, BUFFER_SPAN);
-
- if ((link_len = hoedown_autolink__url(&rewind, link, data, offset, size, 0)) > 0) {
- if (ob->size > rewind)
- ob->size -= rewind;
- else
- ob->size = 0;
-
- doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_NORMAL, &doc->data);
- }
-
- popbuf(doc, BUFFER_SPAN);
- return link_len;
-}
-
-/* char_link • '[': parsing a link, a footnote or an image */
-static size_t
-char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- int is_img = (offset && data[-1] == '!' && !is_escaped(data - offset, offset - 1));
- int is_footnote = (doc->ext_flags & HOEDOWN_EXT_FOOTNOTES && data[1] == '^');
- size_t i = 1, txt_e, link_b = 0, link_e = 0, title_b = 0, title_e = 0;
- hoedown_buffer *content = NULL;
- hoedown_buffer *link = NULL;
- hoedown_buffer *title = NULL;
- hoedown_buffer *u_link = NULL;
- size_t org_work_size = doc->work_bufs[BUFFER_SPAN].size;
- int ret = 0, in_title = 0, qtype = 0;
-
- /* checking whether the correct renderer exists */
- if ((is_footnote && !doc->md.footnote_ref) || (is_img && !doc->md.image)
- || (!is_img && !is_footnote && !doc->md.link))
- goto cleanup;
-
- /* looking for the matching closing bracket */
- i += find_emph_char(data + i, size - i, ']');
- txt_e = i;
-
- if (i < size && data[i] == ']') i++;
- else goto cleanup;
-
- /* footnote link */
- if (is_footnote) {
- hoedown_buffer id = { NULL, 0, 0, 0, NULL, NULL, NULL };
- struct footnote_ref *fr;
-
- if (txt_e < 3)
- goto cleanup;
-
- id.data = data + 2;
- id.size = txt_e - 2;
-
- fr = find_footnote_ref(&doc->footnotes_found, id.data, id.size);
-
- /* mark footnote used */
- if (fr && !fr->is_used) {
- if(!add_footnote_ref(&doc->footnotes_used, fr))
- goto cleanup;
- fr->is_used = 1;
- fr->num = doc->footnotes_used.count;
-
- /* render */
- if (doc->md.footnote_ref)
- ret = doc->md.footnote_ref(ob, fr->num, &doc->data);
- }
-
- goto cleanup;
- }
-
- /* skip any amount of spacing */
- /* (this is much more laxist than original markdown syntax) */
- while (i < size && _isspace(data[i]))
- i++;
-
- /* inline style link */
- if (i < size && data[i] == '(') {
- size_t nb_p;
-
- /* skipping initial spacing */
- i++;
-
- while (i < size && _isspace(data[i]))
- i++;
-
- link_b = i;
-
- /* looking for link end: ' " ) */
- /* Count the number of open parenthesis */
- nb_p = 0;
-
- while (i < size) {
- if (data[i] == '\\') i += 2;
- else if (data[i] == '(' && i != 0) {
- nb_p++; i++;
- }
- else if (data[i] == ')') {
- if (nb_p == 0) break;
- else nb_p--; i++;
- } else if (i >= 1 && _isspace(data[i-1]) && (data[i] == '\'' || data[i] == '"')) break;
- else i++;
- }
-
- if (i >= size) goto cleanup;
- link_e = i;
-
- /* looking for title end if present */
- if (data[i] == '\'' || data[i] == '"') {
- qtype = data[i];
- in_title = 1;
- i++;
- title_b = i;
-
- while (i < size) {
- if (data[i] == '\\') i += 2;
- else if (data[i] == qtype) {in_title = 0; i++;}
- else if ((data[i] == ')') && !in_title) break;
- else i++;
- }
-
- if (i >= size) goto cleanup;
-
- /* skipping spacing after title */
- title_e = i - 1;
- while (title_e > title_b && _isspace(data[title_e]))
- title_e--;
-
- /* checking for closing quote presence */
- if (data[title_e] != '\'' && data[title_e] != '"') {
- title_b = title_e = 0;
- link_e = i;
- }
- }
-
- /* remove spacing at the end of the link */
- while (link_e > link_b && _isspace(data[link_e - 1]))
- link_e--;
-
- /* remove optional angle brackets around the link */
- if (data[link_b] == '<' && data[link_e - 1] == '>') {
- link_b++;
- link_e--;
- }
-
- /* building escaped link and title */
- if (link_e > link_b) {
- link = newbuf(doc, BUFFER_SPAN);
- hoedown_buffer_put(link, data + link_b, link_e - link_b);
- }
-
- if (title_e > title_b) {
- title = newbuf(doc, BUFFER_SPAN);
- hoedown_buffer_put(title, data + title_b, title_e - title_b);
- }
-
- i++;
- }
-
- /* reference style link */
- else if (i < size && data[i] == '[') {
- hoedown_buffer *id = newbuf(doc, BUFFER_SPAN);
- struct link_ref *lr;
-
- /* looking for the id */
- i++;
- link_b = i;
- while (i < size && data[i] != ']') i++;
- if (i >= size) goto cleanup;
- link_e = i;
-
- /* finding the link_ref */
- if (link_b == link_e)
- replace_spacing(id, data + 1, txt_e - 1);
- else
- hoedown_buffer_put(id, data + link_b, link_e - link_b);
-
- lr = find_link_ref(doc->refs, id->data, id->size);
- if (!lr)
- goto cleanup;
-
- /* keeping link and title from link_ref */
- link = lr->link;
- title = lr->title;
- i++;
- }
-
- /* shortcut reference style link */
- else {
- hoedown_buffer *id = newbuf(doc, BUFFER_SPAN);
- struct link_ref *lr;
-
- /* crafting the id */
- replace_spacing(id, data + 1, txt_e - 1);
-
- /* finding the link_ref */
- lr = find_link_ref(doc->refs, id->data, id->size);
- if (!lr)
- goto cleanup;
-
- /* keeping link and title from link_ref */
- link = lr->link;
- title = lr->title;
-
- /* rewinding the spacing */
- i = txt_e + 1;
- }
-
- /* building content: img alt is kept, only link content is parsed */
- if (txt_e > 1) {
- content = newbuf(doc, BUFFER_SPAN);
- if (is_img) {
- hoedown_buffer_put(content, data + 1, txt_e - 1);
- } else {
- /* disable autolinking when parsing inline the
- * content of a link */
- doc->in_link_body = 1;
- parse_inline(content, doc, data + 1, txt_e - 1);
- doc->in_link_body = 0;
- }
- }
-
- if (link) {
- u_link = newbuf(doc, BUFFER_SPAN);
- unscape_text(u_link, link);
- }
-
- /* calling the relevant rendering function */
- if (is_img) {
- if (ob->size && ob->data[ob->size - 1] == '!')
- ob->size -= 1;
-
- ret = doc->md.image(ob, u_link, title, content, &doc->data);
- } else {
- ret = doc->md.link(ob, content, u_link, title, &doc->data);
- }
-
- /* cleanup */
-cleanup:
- doc->work_bufs[BUFFER_SPAN].size = (int)org_work_size;
- return ret ? i : 0;
-}
-
-static size_t
-char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- size_t sup_start, sup_len;
- hoedown_buffer *sup;
-
- if (!doc->md.superscript)
- return 0;
-
- if (size < 2)
- return 0;
-
- if (data[1] == '(') {
- sup_start = 2;
- sup_len = find_emph_char(data + 2, size - 2, ')') + 2;
-
- if (sup_len == size)
- return 0;
- } else {
- sup_start = sup_len = 1;
-
- while (sup_len < size && !_isspace(data[sup_len]))
- sup_len++;
- }
-
- if (sup_len - sup_start == 0)
- return (sup_start == 2) ? 3 : 0;
-
- sup = newbuf(doc, BUFFER_SPAN);
- parse_inline(sup, doc, data + sup_start, sup_len - sup_start);
- doc->md.superscript(ob, sup, &doc->data);
- popbuf(doc, BUFFER_SPAN);
-
- return (sup_start == 2) ? sup_len + 1 : sup_len;
-}
-
-static size_t
-char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size)
-{
- /* double dollar */
- if (size > 1 && data[1] == '$')
- return parse_math(ob, doc, data, offset, size, "$$", 2, 1);
-
- /* single dollar allowed only with MATH_EXPLICIT flag */
- if (doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT)
- return parse_math(ob, doc, data, offset, size, "$", 1, 0);
-
- return 0;
-}
-
-/*********************************
- * BLOCK-LEVEL PARSING FUNCTIONS *
- *********************************/
-
-/* is_empty • returns the line length when it is empty, 0 otherwise */
-static size_t
-is_empty(const uint8_t *data, size_t size)
-{
- size_t i;
-
- for (i = 0; i < size && data[i] != '\n'; i++)
- if (data[i] != ' ')
- return 0;
-
- return i + 1;
-}
-
-/* is_hrule • returns whether a line is a horizontal rule */
-static int
-is_hrule(uint8_t *data, size_t size)
-{
- size_t i = 0, n = 0;
- uint8_t c;
-
- /* skipping initial spaces */
- if (size < 3) return 0;
- if (data[0] == ' ') { i++;
- if (data[1] == ' ') { i++;
- if (data[2] == ' ') { i++; } } }
-
- /* looking at the hrule uint8_t */
- if (i + 2 >= size
- || (data[i] != '*' && data[i] != '-' && data[i] != '_'))
- return 0;
- c = data[i];
-
- /* the whole line must be the char or space */
- while (i < size && data[i] != '\n') {
- if (data[i] == c) n++;
- else if (data[i] != ' ')
- return 0;
-
- i++;
- }
-
- return n >= 3;
-}
-
-/* check if a line is a code fence; return the
- * end of the code fence. if passed, width of
- * the fence rule and character will be returned */
-static size_t
-is_codefence(uint8_t *data, size_t size, size_t *width, uint8_t *chr)
-{
- size_t i = 0, n = 1;
- uint8_t c;
-
- /* skipping initial spaces */
- if (size < 3)
- return 0;
-
- if (data[0] == ' ') { i++;
- if (data[1] == ' ') { i++;
- if (data[2] == ' ') { i++; } } }
-
- /* looking at the hrule uint8_t */
- c = data[i];
- if (i + 2 >= size || !(c=='~' || c=='`'))
- return 0;
-
- /* the fence must be that same character */
- while (++i < size && data[i] == c)
- ++n;
-
- if (n < 3)
- return 0;
-
- if (width) *width = n;
- if (chr) *chr = c;
- return i;
-}
-
-/* expects single line, checks if it's a codefence and extracts language */
-static size_t
-parse_codefence(uint8_t *data, size_t size, hoedown_buffer *lang, size_t *width, uint8_t *chr)
-{
- size_t i, w, lang_start;
-
- i = w = is_codefence(data, size, width, chr);
- if (i == 0)
- return 0;
-
- while (i < size && _isspace(data[i]))
- i++;
-
- lang_start = i;
-
- while (i < size && !_isspace(data[i]))
- i++;
-
- lang->data = data + lang_start;
- lang->size = i - lang_start;
-
- /* Avoid parsing a codespan as a fence */
- i = lang_start + 2;
- while (i < size && !(data[i] == *chr && data[i-1] == *chr && data[i-2] == *chr)) i++;
- if (i < size) return 0;
-
- return w;
-}
-
-/* is_atxheader • returns whether the line is a hash-prefixed header */
-static int
-is_atxheader(hoedown_document *doc, uint8_t *data, size_t size)
-{
- if (data[0] != '#')
- return 0;
-
- if (doc->ext_flags & HOEDOWN_EXT_SPACE_HEADERS) {
- size_t level = 0;
-
- while (level < size && level < 6 && data[level] == '#')
- level++;
-
- if (level < size && data[level] != ' ')
- return 0;
- }
-
- return 1;
-}
-
-/* is_headerline • returns whether the line is a setext-style hdr underline */
-static int
-is_headerline(uint8_t *data, size_t size)
-{
- size_t i = 0;
-
- /* test of level 1 header */
- if (data[i] == '=') {
- for (i = 1; i < size && data[i] == '='; i++);
- while (i < size && data[i] == ' ') i++;
- return (i >= size || data[i] == '\n') ? 1 : 0; }
-
- /* test of level 2 header */
- if (data[i] == '-') {
- for (i = 1; i < size && data[i] == '-'; i++);
- while (i < size && data[i] == ' ') i++;
- return (i >= size || data[i] == '\n') ? 2 : 0; }
-
- return 0;
-}
-
-static int
-is_next_headerline(uint8_t *data, size_t size)
-{
- size_t i = 0;
-
- while (i < size && data[i] != '\n')
- i++;
-
- if (++i >= size)
- return 0;
-
- return is_headerline(data + i, size - i);
-}
-
-/* prefix_quote • returns blockquote prefix length */
-static size_t
-prefix_quote(uint8_t *data, size_t size)
-{
- size_t i = 0;
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
-
- if (i < size && data[i] == '>') {
- if (i + 1 < size && data[i + 1] == ' ')
- return i + 2;
-
- return i + 1;
- }
-
- return 0;
-}
-
-/* prefix_code • returns prefix length for block code*/
-static size_t
-prefix_code(uint8_t *data, size_t size)
-{
- if (size > 3 && data[0] == ' ' && data[1] == ' '
- && data[2] == ' ' && data[3] == ' ') return 4;
-
- return 0;
-}
-
-/* prefix_oli • returns ordered list item prefix */
-static size_t
-prefix_oli(uint8_t *data, size_t size)
-{
- size_t i = 0;
-
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
-
- if (i >= size || data[i] < '0' || data[i] > '9')
- return 0;
-
- while (i < size && data[i] >= '0' && data[i] <= '9')
- i++;
-
- if (i + 1 >= size || data[i] != '.' || data[i + 1] != ' ')
- return 0;
-
- if (is_next_headerline(data + i, size - i))
- return 0;
-
- return i + 2;
-}
-
-/* prefix_uli • returns ordered list item prefix */
-static size_t
-prefix_uli(uint8_t *data, size_t size)
-{
- size_t i = 0;
-
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
- if (i < size && data[i] == ' ') i++;
-
- if (i + 1 >= size ||
- (data[i] != '*' && data[i] != '+' && data[i] != '-') ||
- data[i + 1] != ' ')
- return 0;
-
- if (is_next_headerline(data + i, size - i))
- return 0;
-
- return i + 2;
-}
-
-
-/* parse_block • parsing of one block, returning next uint8_t to parse */
-static void parse_block(hoedown_buffer *ob, hoedown_document *doc,
- uint8_t *data, size_t size);
-
-
-/* parse_blockquote • handles parsing of a blockquote fragment */
-static size_t
-parse_blockquote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t beg, end = 0, pre, work_size = 0;
- uint8_t *work_data = 0;
- hoedown_buffer *out = 0;
-
- out = newbuf(doc, BUFFER_BLOCK);
- beg = 0;
- while (beg < size) {
- for (end = beg + 1; end < size && data[end - 1] != '\n'; end++);
-
- pre = prefix_quote(data + beg, end - beg);
-
- if (pre)
- beg += pre; /* skipping prefix */
-
- /* empty line followed by non-quote line */
- else if (is_empty(data + beg, end - beg) &&
- (end >= size || (prefix_quote(data + end, size - end) == 0 &&
- !is_empty(data + end, size - end))))
- break;
-
- if (beg < end) { /* copy into the in-place working buffer */
- /* hoedown_buffer_put(work, data + beg, end - beg); */
- if (!work_data)
- work_data = data + beg;
- else if (data + beg != work_data + work_size)
- memmove(work_data + work_size, data + beg, end - beg);
- work_size += end - beg;
- }
- beg = end;
- }
-
- parse_block(out, doc, work_data, work_size);
- if (doc->md.blockquote)
- doc->md.blockquote(ob, out, &doc->data);
- popbuf(doc, BUFFER_BLOCK);
- return end;
-}
-
-static size_t
-parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render);
-
-/* parse_blockquote • handles parsing of a regular paragraph */
-static size_t
-parse_paragraph(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL };
- size_t i = 0, end = 0;
- int level = 0;
-
- work.data = data;
-
- while (i < size) {
- for (end = i + 1; end < size && data[end - 1] != '\n'; end++) /* empty */;
-
- if (is_empty(data + i, size - i))
- break;
-
- if ((level = is_headerline(data + i, size - i)) != 0)
- break;
-
- if (is_atxheader(doc, data + i, size - i) ||
- is_hrule(data + i, size - i) ||
- prefix_quote(data + i, size - i)) {
- end = i;
- break;
- }
-
- i = end;
- }
-
- work.size = i;
- while (work.size && data[work.size - 1] == '\n')
- work.size--;
-
- if (!level) {
- hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK);
- parse_inline(tmp, doc, work.data, work.size);
- if (doc->md.paragraph)
- doc->md.paragraph(ob, tmp, &doc->data);
- popbuf(doc, BUFFER_BLOCK);
- } else {
- hoedown_buffer *header_work;
-
- if (work.size) {
- size_t beg;
- i = work.size;
- work.size -= 1;
-
- while (work.size && data[work.size] != '\n')
- work.size -= 1;
-
- beg = work.size + 1;
- while (work.size && data[work.size - 1] == '\n')
- work.size -= 1;
-
- if (work.size > 0) {
- hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK);
- parse_inline(tmp, doc, work.data, work.size);
-
- if (doc->md.paragraph)
- doc->md.paragraph(ob, tmp, &doc->data);
-
- popbuf(doc, BUFFER_BLOCK);
- work.data += beg;
- work.size = i - beg;
- }
- else work.size = i;
- }
-
- header_work = newbuf(doc, BUFFER_SPAN);
- parse_inline(header_work, doc, work.data, work.size);
-
- if (doc->md.header)
- doc->md.header(ob, header_work, (int)level, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- }
-
- return end;
-}
-
-/* parse_fencedcode • handles parsing of a block-level code fragment */
-static size_t
-parse_fencedcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- hoedown_buffer text = { 0, 0, 0, 0, NULL, NULL, NULL };
- hoedown_buffer lang = { 0, 0, 0, 0, NULL, NULL, NULL };
- size_t i = 0, text_start, line_start;
- size_t w, w2;
- size_t width, width2;
- uint8_t chr, chr2;
-
- /* parse codefence line */
- while (i < size && data[i] != '\n')
- i++;
-
- w = parse_codefence(data, i, &lang, &width, &chr);
- if (!w)
- return 0;
-
- /* search for end */
- i++;
- text_start = i;
- while ((line_start = i) < size) {
- while (i < size && data[i] != '\n')
- i++;
-
- w2 = is_codefence(data + line_start, i - line_start, &width2, &chr2);
- if (w == w2 && width == width2 && chr == chr2 &&
- is_empty(data + (line_start+w), i - (line_start+w)))
- break;
-
- i++;
- }
-
- text.data = data + text_start;
- text.size = line_start - text_start;
-
- if (doc->md.blockcode)
- doc->md.blockcode(ob, text.size ? &text : NULL, lang.size ? &lang : NULL, &doc->data);
-
- return i;
-}
-
-static size_t
-parse_blockcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t beg, end, pre;
- hoedown_buffer *work = 0;
-
- work = newbuf(doc, BUFFER_BLOCK);
-
- beg = 0;
- while (beg < size) {
- for (end = beg + 1; end < size && data[end - 1] != '\n'; end++) {};
- pre = prefix_code(data + beg, end - beg);
-
- if (pre)
- beg += pre; /* skipping prefix */
- else if (!is_empty(data + beg, end - beg))
- /* non-empty non-prefixed line breaks the pre */
- break;
-
- if (beg < end) {
- /* verbatim copy to the working buffer,
- escaping entities */
- if (is_empty(data + beg, end - beg))
- hoedown_buffer_putc(work, '\n');
- else hoedown_buffer_put(work, data + beg, end - beg);
- }
- beg = end;
- }
-
- while (work->size && work->data[work->size - 1] == '\n')
- work->size -= 1;
-
- hoedown_buffer_putc(work, '\n');
-
- if (doc->md.blockcode)
- doc->md.blockcode(ob, work, NULL, &doc->data);
-
- popbuf(doc, BUFFER_BLOCK);
- return beg;
-}
-
-/* parse_listitem • parsing of a single list item */
-/* assuming initial prefix is already removed */
-static size_t
-parse_listitem(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags *flags)
-{
- hoedown_buffer *work = 0, *inter = 0;
- size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i;
- int in_empty = 0, has_inside_empty = 0, in_fence = 0;
-
- /* keeping track of the first indentation prefix */
- while (orgpre < 3 && orgpre < size && data[orgpre] == ' ')
- orgpre++;
-
- beg = prefix_uli(data, size);
- if (!beg)
- beg = prefix_oli(data, size);
-
- if (!beg)
- return 0;
-
- /* skipping to the beginning of the following line */
- end = beg;
- while (end < size && data[end - 1] != '\n')
- end++;
-
- /* getting working buffers */
- work = newbuf(doc, BUFFER_SPAN);
- inter = newbuf(doc, BUFFER_SPAN);
-
- /* putting the first line into the working buffer */
- hoedown_buffer_put(work, data + beg, end - beg);
- beg = end;
-
- /* process the following lines */
- while (beg < size) {
- size_t has_next_uli = 0, has_next_oli = 0;
-
- end++;
-
- while (end < size && data[end - 1] != '\n')
- end++;
-
- /* process an empty line */
- if (is_empty(data + beg, end - beg)) {
- in_empty = 1;
- beg = end;
- continue;
- }
-
- /* calculating the indentation */
- i = 0;
- while (i < 4 && beg + i < end && data[beg + i] == ' ')
- i++;
-
- pre = i;
-
- if (doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) {
- if (is_codefence(data + beg + i, end - beg - i, NULL, NULL))
- in_fence = !in_fence;
- }
-
- /* Only check for new list items if we are **not** inside
- * a fenced code block */
- if (!in_fence) {
- has_next_uli = prefix_uli(data + beg + i, end - beg - i);
- has_next_oli = prefix_oli(data + beg + i, end - beg - i);
- }
-
- /* checking for a new item */
- if ((has_next_uli && !is_hrule(data + beg + i, end - beg - i)) || has_next_oli) {
- if (in_empty)
- has_inside_empty = 1;
-
- /* the following item must have the same (or less) indentation */
- if (pre <= orgpre) {
- /* if the following item has different list type, we end this list */
- if (in_empty && (
- ((*flags & HOEDOWN_LIST_ORDERED) && has_next_uli) ||
- (!(*flags & HOEDOWN_LIST_ORDERED) && has_next_oli)))
- *flags |= HOEDOWN_LI_END;
-
- break;
- }
-
- if (!sublist)
- sublist = work->size;
- }
- /* joining only indented stuff after empty lines;
- * note that now we only require 1 space of indentation
- * to continue a list */
- else if (in_empty && pre == 0) {
- *flags |= HOEDOWN_LI_END;
- break;
- }
-
- if (in_empty) {
- hoedown_buffer_putc(work, '\n');
- has_inside_empty = 1;
- in_empty = 0;
- }
-
- /* adding the line without prefix into the working buffer */
- hoedown_buffer_put(work, data + beg + i, end - beg - i);
- beg = end;
- }
-
- /* render of li contents */
- if (has_inside_empty)
- *flags |= HOEDOWN_LI_BLOCK;
-
- if (*flags & HOEDOWN_LI_BLOCK) {
- /* intermediate render of block li */
- if (sublist && sublist < work->size) {
- parse_block(inter, doc, work->data, sublist);
- parse_block(inter, doc, work->data + sublist, work->size - sublist);
- }
- else
- parse_block(inter, doc, work->data, work->size);
- } else {
- /* intermediate render of inline li */
- if (sublist && sublist < work->size) {
- parse_inline(inter, doc, work->data, sublist);
- parse_block(inter, doc, work->data + sublist, work->size - sublist);
- }
- else
- parse_inline(inter, doc, work->data, work->size);
- }
-
- /* render of li itself */
- if (doc->md.listitem)
- doc->md.listitem(ob, inter, *flags, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- popbuf(doc, BUFFER_SPAN);
- return beg;
-}
-
-
-/* parse_list • parsing ordered or unordered list block */
-static size_t
-parse_list(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags flags)
-{
- hoedown_buffer *work = 0;
- size_t i = 0, j;
-
- work = newbuf(doc, BUFFER_BLOCK);
-
- while (i < size) {
- j = parse_listitem(work, doc, data + i, size - i, &flags);
- i += j;
-
- if (!j || (flags & HOEDOWN_LI_END))
- break;
- }
-
- if (doc->md.list)
- doc->md.list(ob, work, flags, &doc->data);
- popbuf(doc, BUFFER_BLOCK);
- return i;
-}
-
-/* parse_atxheader • parsing of atx-style headers */
-static size_t
-parse_atxheader(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t level = 0;
- size_t i, end, skip;
-
- while (level < size && level < 6 && data[level] == '#')
- level++;
-
- for (i = level; i < size && data[i] == ' '; i++);
-
- for (end = i; end < size && data[end] != '\n'; end++);
- skip = end;
-
- while (end && data[end - 1] == '#')
- end--;
-
- while (end && data[end - 1] == ' ')
- end--;
-
- if (end > i) {
- hoedown_buffer *work = newbuf(doc, BUFFER_SPAN);
-
- parse_inline(work, doc, data + i, end - i);
-
- if (doc->md.header)
- doc->md.header(ob, work, (int)level, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- }
-
- return skip;
-}
-
-/* parse_footnote_def • parse a single footnote definition */
-static void
-parse_footnote_def(hoedown_buffer *ob, hoedown_document *doc, unsigned int num, uint8_t *data, size_t size)
-{
- hoedown_buffer *work = 0;
- work = newbuf(doc, BUFFER_SPAN);
-
- parse_block(work, doc, data, size);
-
- if (doc->md.footnote_def)
- doc->md.footnote_def(ob, work, num, &doc->data);
- popbuf(doc, BUFFER_SPAN);
-}
-
-/* parse_footnote_list • render the contents of the footnotes */
-static void
-parse_footnote_list(hoedown_buffer *ob, hoedown_document *doc, struct footnote_list *footnotes)
-{
- hoedown_buffer *work = 0;
- struct footnote_item *item;
- struct footnote_ref *ref;
-
- if (footnotes->count == 0)
- return;
-
- work = newbuf(doc, BUFFER_BLOCK);
-
- item = footnotes->head;
- while (item) {
- ref = item->ref;
- parse_footnote_def(work, doc, ref->num, ref->contents->data, ref->contents->size);
- item = item->next;
- }
-
- if (doc->md.footnotes)
- doc->md.footnotes(ob, work, &doc->data);
- popbuf(doc, BUFFER_BLOCK);
-}
-
-/* htmlblock_is_end • check for end of HTML block : ( *)\n */
-/* returns tag length on match, 0 otherwise */
-/* assumes data starts with "<" */
-static size_t
-htmlblock_is_end(
- const char *tag,
- size_t tag_len,
- hoedown_document *doc,
- uint8_t *data,
- size_t size)
-{
- size_t i = tag_len + 3, w;
-
- /* try to match the end tag */
- /* note: we're not considering tags like "" which are still valid */
- if (i > size ||
- data[1] != '/' ||
- strncasecmp((char *)data + 2, tag, tag_len) != 0 ||
- data[tag_len + 2] != '>')
- return 0;
-
- /* rest of the line must be empty */
- if ((w = is_empty(data + i, size - i)) == 0 && i < size)
- return 0;
-
- return i + w;
-}
-
-/* htmlblock_find_end • try to find HTML block ending tag */
-/* returns the length on match, 0 otherwise */
-static size_t
-htmlblock_find_end(
- const char *tag,
- size_t tag_len,
- hoedown_document *doc,
- uint8_t *data,
- size_t size)
-{
- size_t i = 0, w;
-
- while (1) {
- while (i < size && data[i] != '<') i++;
- if (i >= size) return 0;
-
- w = htmlblock_is_end(tag, tag_len, doc, data + i, size - i);
- if (w) return i + w;
- i++;
- }
-}
-
-/* htmlblock_find_end_strict • try to find end of HTML block in strict mode */
-/* (it must be an unindented line, and have a blank line afterwads) */
-/* returns the length on match, 0 otherwise */
-static size_t
-htmlblock_find_end_strict(
- const char *tag,
- size_t tag_len,
- hoedown_document *doc,
- uint8_t *data,
- size_t size)
-{
- size_t i = 0, mark;
-
- while (1) {
- mark = i;
- while (i < size && data[i] != '\n') i++;
- if (i < size) i++;
- if (i == mark) return 0;
-
- if (data[mark] == ' ' && mark > 0) continue;
- mark += htmlblock_find_end(tag, tag_len, doc, data + mark, i - mark);
- if (mark == i && (is_empty(data + i, size - i) || i >= size)) break;
- }
-
- return i;
-}
-
-/* parse_htmlblock • parsing of inline HTML block */
-static size_t
-parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render)
-{
- hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL };
- size_t i, j = 0, tag_len, tag_end;
- const char *curtag = NULL;
-
- work.data = data;
-
- /* identification of the opening tag */
- if (size < 2 || data[0] != '<')
- return 0;
-
- i = 1;
- while (i < size && data[i] != '>' && data[i] != ' ')
- i++;
-
- if (i < size)
- curtag = hoedown_find_block_tag((char *)data + 1, (int)i - 1);
-
- /* handling of special cases */
- if (!curtag) {
-
- /* HTML comment, laxist form */
- if (size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') {
- i = 5;
-
- while (i < size && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>'))
- i++;
-
- i++;
-
- if (i < size)
- j = is_empty(data + i, size - i);
-
- if (j) {
- work.size = i + j;
- if (do_render && doc->md.blockhtml)
- doc->md.blockhtml(ob, &work, &doc->data);
- return work.size;
- }
- }
-
- /* HR, which is the only self-closing block tag considered */
- if (size > 4 && (data[1] == 'h' || data[1] == 'H') && (data[2] == 'r' || data[2] == 'R')) {
- i = 3;
- while (i < size && data[i] != '>')
- i++;
-
- if (i + 1 < size) {
- i++;
- j = is_empty(data + i, size - i);
- if (j) {
- work.size = i + j;
- if (do_render && doc->md.blockhtml)
- doc->md.blockhtml(ob, &work, &doc->data);
- return work.size;
- }
- }
- }
-
- /* no special case recognised */
- return 0;
- }
-
- /* looking for a matching closing tag in strict mode */
- tag_len = strlen(curtag);
- tag_end = htmlblock_find_end_strict(curtag, tag_len, doc, data, size);
-
- /* if not found, trying a second pass looking for indented match */
- /* but not if tag is "ins" or "del" (following original Markdown.pl) */
- if (!tag_end && strcmp(curtag, "ins") != 0 && strcmp(curtag, "del") != 0)
- tag_end = htmlblock_find_end(curtag, tag_len, doc, data, size);
-
- if (!tag_end)
- return 0;
-
- /* the end of the block has been found */
- work.size = tag_end;
- if (do_render && doc->md.blockhtml)
- doc->md.blockhtml(ob, &work, &doc->data);
-
- return tag_end;
-}
-
-static void
-parse_table_row(
- hoedown_buffer *ob,
- hoedown_document *doc,
- uint8_t *data,
- size_t size,
- size_t columns,
- hoedown_table_flags *col_data,
- hoedown_table_flags header_flag)
-{
- size_t i = 0, col, len;
- hoedown_buffer *row_work = 0;
-
- if (!doc->md.table_cell || !doc->md.table_row)
- return;
-
- row_work = newbuf(doc, BUFFER_SPAN);
-
- if (i < size && data[i] == '|')
- i++;
-
- for (col = 0; col < columns && i < size; ++col) {
- size_t cell_start, cell_end;
- hoedown_buffer *cell_work;
-
- cell_work = newbuf(doc, BUFFER_SPAN);
-
- while (i < size && _isspace(data[i]))
- i++;
-
- cell_start = i;
-
- len = find_emph_char(data + i, size - i, '|');
-
- /* Two possibilities for len == 0:
- 1) No more pipe char found in the current line.
- 2) The next pipe is right after the current one, i.e. empty cell.
- For case 1, we skip to the end of line; for case 2 we just continue.
- */
- if (len == 0 && i < size && data[i] != '|')
- len = size - i;
- i += len;
-
- cell_end = i - 1;
-
- while (cell_end > cell_start && _isspace(data[cell_end]))
- cell_end--;
-
- parse_inline(cell_work, doc, data + cell_start, 1 + cell_end - cell_start);
- doc->md.table_cell(row_work, cell_work, col_data[col] | header_flag, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
- i++;
- }
-
- for (; col < columns; ++col) {
- hoedown_buffer empty_cell = { 0, 0, 0, 0, NULL, NULL, NULL };
- doc->md.table_cell(row_work, &empty_cell, col_data[col] | header_flag, &doc->data);
- }
-
- doc->md.table_row(ob, row_work, &doc->data);
-
- popbuf(doc, BUFFER_SPAN);
-}
-
-static size_t
-parse_table_header(
- hoedown_buffer *ob,
- hoedown_document *doc,
- uint8_t *data,
- size_t size,
- size_t *columns,
- hoedown_table_flags **column_data)
-{
- int pipes;
- size_t i = 0, col, header_end, under_end;
-
- pipes = 0;
- while (i < size && data[i] != '\n')
- if (data[i++] == '|')
- pipes++;
-
- if (i == size || pipes == 0)
- return 0;
-
- header_end = i;
-
- while (header_end > 0 && _isspace(data[header_end - 1]))
- header_end--;
-
- if (data[0] == '|')
- pipes--;
-
- if (header_end && data[header_end - 1] == '|')
- pipes--;
-
- if (pipes < 0)
- return 0;
-
- *columns = pipes + 1;
- *column_data = hoedown_calloc(*columns, sizeof(hoedown_table_flags));
-
- /* Parse the header underline */
- i++;
- if (i < size && data[i] == '|')
- i++;
-
- under_end = i;
- while (under_end < size && data[under_end] != '\n')
- under_end++;
-
- for (col = 0; col < *columns && i < under_end; ++col) {
- size_t dashes = 0;
-
- while (i < under_end && data[i] == ' ')
- i++;
-
- if (data[i] == ':') {
- i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_LEFT;
- dashes++;
- }
-
- while (i < under_end && data[i] == '-') {
- i++; dashes++;
- }
-
- if (i < under_end && data[i] == ':') {
- i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_RIGHT;
- dashes++;
- }
-
- while (i < under_end && data[i] == ' ')
- i++;
-
- if (i < under_end && data[i] != '|' && data[i] != '+')
- break;
-
- if (dashes < 3)
- break;
-
- i++;
- }
-
- if (col < *columns)
- return 0;
-
- parse_table_row(
- ob, doc, data,
- header_end,
- *columns,
- *column_data,
- HOEDOWN_TABLE_HEADER
- );
-
- return under_end + 1;
-}
-
-static size_t
-parse_table(
- hoedown_buffer *ob,
- hoedown_document *doc,
- uint8_t *data,
- size_t size)
-{
- size_t i;
-
- hoedown_buffer *work = 0;
- hoedown_buffer *header_work = 0;
- hoedown_buffer *body_work = 0;
-
- size_t columns;
- hoedown_table_flags *col_data = NULL;
-
- work = newbuf(doc, BUFFER_BLOCK);
- header_work = newbuf(doc, BUFFER_SPAN);
- body_work = newbuf(doc, BUFFER_BLOCK);
-
- i = parse_table_header(header_work, doc, data, size, &columns, &col_data);
- if (i > 0) {
-
- while (i < size) {
- size_t row_start;
- int pipes = 0;
-
- row_start = i;
-
- while (i < size && data[i] != '\n')
- if (data[i++] == '|')
- pipes++;
-
- if (pipes == 0 || i == size) {
- i = row_start;
- break;
- }
-
- parse_table_row(
- body_work,
- doc,
- data + row_start,
- i - row_start,
- columns,
- col_data, 0
- );
-
- i++;
- }
-
- if (doc->md.table_header)
- doc->md.table_header(work, header_work, &doc->data);
-
- if (doc->md.table_body)
- doc->md.table_body(work, body_work, &doc->data);
-
- if (doc->md.table)
- doc->md.table(ob, work, &doc->data);
- }
-
- free(col_data);
- popbuf(doc, BUFFER_SPAN);
- popbuf(doc, BUFFER_BLOCK);
- popbuf(doc, BUFFER_BLOCK);
- return i;
-}
-
-/* parse_block • parsing of one block, returning next uint8_t to parse */
-static void
-parse_block(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size)
-{
- size_t beg, end, i;
- uint8_t *txt_data;
- beg = 0;
-
- if (doc->work_bufs[BUFFER_SPAN].size +
- doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting)
- return;
-
- while (beg < size) {
- txt_data = data + beg;
- end = size - beg;
-
- if (is_atxheader(doc, txt_data, end))
- beg += parse_atxheader(ob, doc, txt_data, end);
-
- else if (data[beg] == '<' && doc->md.blockhtml &&
- (i = parse_htmlblock(ob, doc, txt_data, end, 1)) != 0)
- beg += i;
-
- else if ((i = is_empty(txt_data, end)) != 0)
- beg += i;
-
- else if (is_hrule(txt_data, end)) {
- if (doc->md.hrule)
- doc->md.hrule(ob, &doc->data);
-
- while (beg < size && data[beg] != '\n')
- beg++;
-
- beg++;
- }
-
- else if ((doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) != 0 &&
- (i = parse_fencedcode(ob, doc, txt_data, end)) != 0)
- beg += i;
-
- else if ((doc->ext_flags & HOEDOWN_EXT_TABLES) != 0 &&
- (i = parse_table(ob, doc, txt_data, end)) != 0)
- beg += i;
-
- else if (prefix_quote(txt_data, end))
- beg += parse_blockquote(ob, doc, txt_data, end);
-
- else if (!(doc->ext_flags & HOEDOWN_EXT_DISABLE_INDENTED_CODE) && prefix_code(txt_data, end))
- beg += parse_blockcode(ob, doc, txt_data, end);
-
- else if (prefix_uli(txt_data, end))
- beg += parse_list(ob, doc, txt_data, end, 0);
-
- else if (prefix_oli(txt_data, end))
- beg += parse_list(ob, doc, txt_data, end, HOEDOWN_LIST_ORDERED);
-
- else
- beg += parse_paragraph(ob, doc, txt_data, end);
- }
-}
-
-
-
-/*********************
- * REFERENCE PARSING *
- *********************/
-
-/* is_footnote • returns whether a line is a footnote definition or not */
-static int
-is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct footnote_list *list)
-{
- size_t i = 0;
- hoedown_buffer *contents = 0;
- size_t ind = 0;
- int in_empty = 0;
- size_t start = 0;
-
- size_t id_offset, id_end;
-
- /* up to 3 optional leading spaces */
- if (beg + 3 >= end) return 0;
- if (data[beg] == ' ') { i = 1;
- if (data[beg + 1] == ' ') { i = 2;
- if (data[beg + 2] == ' ') { i = 3;
- if (data[beg + 3] == ' ') return 0; } } }
- i += beg;
-
- /* id part: caret followed by anything between brackets */
- if (data[i] != '[') return 0;
- i++;
- if (i >= end || data[i] != '^') return 0;
- i++;
- id_offset = i;
- while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']')
- i++;
- if (i >= end || data[i] != ']') return 0;
- id_end = i;
-
- /* spacer: colon (space | tab)* newline? (space | tab)* */
- i++;
- if (i >= end || data[i] != ':') return 0;
- i++;
-
- /* getting content buffer */
- contents = hoedown_buffer_new(64);
-
- start = i;
-
- /* process lines similar to a list item */
- while (i < end) {
- while (i < end && data[i] != '\n' && data[i] != '\r') i++;
-
- /* process an empty line */
- if (is_empty(data + start, i - start)) {
- in_empty = 1;
- if (i < end && (data[i] == '\n' || data[i] == '\r')) {
- i++;
- if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++;
- }
- start = i;
- continue;
- }
-
- /* calculating the indentation */
- ind = 0;
- while (ind < 4 && start + ind < end && data[start + ind] == ' ')
- ind++;
-
- /* joining only indented stuff after empty lines;
- * note that now we only require 1 space of indentation
- * to continue, just like lists */
- if (ind == 0) {
- if (start == id_end + 2 && data[start] == '\t') {}
- else break;
- }
- else if (in_empty) {
- hoedown_buffer_putc(contents, '\n');
- }
-
- in_empty = 0;
-
- /* adding the line into the content buffer */
- hoedown_buffer_put(contents, data + start + ind, i - start - ind);
- /* add carriage return */
- if (i < end) {
- hoedown_buffer_putc(contents, '\n');
- if (i < end && (data[i] == '\n' || data[i] == '\r')) {
- i++;
- if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++;
- }
- }
- start = i;
- }
-
- if (last)
- *last = start;
-
- if (list) {
- struct footnote_ref *ref;
- ref = create_footnote_ref(list, data + id_offset, id_end - id_offset);
- if (!ref)
- return 0;
- if (!add_footnote_ref(list, ref)) {
- free_footnote_ref(ref);
- return 0;
- }
- ref->contents = contents;
- }
-
- return 1;
-}
-
-/* is_ref • returns whether a line is a reference or not */
-static int
-is_ref(const uint8_t *data, size_t beg, size_t end, size_t *last, struct link_ref **refs)
-{
-/* int n; */
- size_t i = 0;
- size_t id_offset, id_end;
- size_t link_offset, link_end;
- size_t title_offset, title_end;
- size_t line_end;
-
- /* up to 3 optional leading spaces */
- if (beg + 3 >= end) return 0;
- if (data[beg] == ' ') { i = 1;
- if (data[beg + 1] == ' ') { i = 2;
- if (data[beg + 2] == ' ') { i = 3;
- if (data[beg + 3] == ' ') return 0; } } }
- i += beg;
-
- /* id part: anything but a newline between brackets */
- if (data[i] != '[') return 0;
- i++;
- id_offset = i;
- while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']')
- i++;
- if (i >= end || data[i] != ']') return 0;
- id_end = i;
-
- /* spacer: colon (space | tab)* newline? (space | tab)* */
- i++;
- if (i >= end || data[i] != ':') return 0;
- i++;
- while (i < end && data[i] == ' ') i++;
- if (i < end && (data[i] == '\n' || data[i] == '\r')) {
- i++;
- if (i < end && data[i] == '\r' && data[i - 1] == '\n') i++; }
- while (i < end && data[i] == ' ') i++;
- if (i >= end) return 0;
-
- /* link: spacing-free sequence, optionally between angle brackets */
- if (data[i] == '<')
- i++;
-
- link_offset = i;
-
- while (i < end && data[i] != ' ' && data[i] != '\n' && data[i] != '\r')
- i++;
-
- if (data[i - 1] == '>') link_end = i - 1;
- else link_end = i;
-
- /* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */
- while (i < end && data[i] == ' ') i++;
- if (i < end && data[i] != '\n' && data[i] != '\r'
- && data[i] != '\'' && data[i] != '"' && data[i] != '(')
- return 0;
- line_end = 0;
- /* computing end-of-line */
- if (i >= end || data[i] == '\r' || data[i] == '\n') line_end = i;
- if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
- line_end = i + 1;
-
- /* optional (space|tab)* spacer after a newline */
- if (line_end) {
- i = line_end + 1;
- while (i < end && data[i] == ' ') i++; }
-
- /* optional title: any non-newline sequence enclosed in '"()
- alone on its line */
- title_offset = title_end = 0;
- if (i + 1 < end
- && (data[i] == '\'' || data[i] == '"' || data[i] == '(')) {
- i++;
- title_offset = i;
- /* looking for EOL */
- while (i < end && data[i] != '\n' && data[i] != '\r') i++;
- if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r')
- title_end = i + 1;
- else title_end = i;
- /* stepping back */
- i -= 1;
- while (i > title_offset && data[i] == ' ')
- i -= 1;
- if (i > title_offset
- && (data[i] == '\'' || data[i] == '"' || data[i] == ')')) {
- line_end = title_end;
- title_end = i; } }
-
- if (!line_end || link_end == link_offset)
- return 0; /* garbage after the link empty link */
-
- /* a valid ref has been found, filling-in return structures */
- if (last)
- *last = line_end;
-
- if (refs) {
- struct link_ref *ref;
-
- ref = add_link_ref(refs, data + id_offset, id_end - id_offset);
- if (!ref)
- return 0;
-
- ref->link = hoedown_buffer_new(link_end - link_offset);
- hoedown_buffer_put(ref->link, data + link_offset, link_end - link_offset);
-
- if (title_end > title_offset) {
- ref->title = hoedown_buffer_new(title_end - title_offset);
- hoedown_buffer_put(ref->title, data + title_offset, title_end - title_offset);
- }
- }
-
- return 1;
-}
-
-static void expand_tabs(hoedown_buffer *ob, const uint8_t *line, size_t size)
-{
- /* This code makes two assumptions:
- * - Input is valid UTF-8. (Any byte with top two bits 10 is skipped,
- * whether or not it is a valid UTF-8 continuation byte.)
- * - Input contains no combining characters. (Combining characters
- * should be skipped but are not.)
- */
- size_t i = 0, tab = 0;
-
- while (i < size) {
- size_t org = i;
-
- while (i < size && line[i] != '\t') {
- /* ignore UTF-8 continuation bytes */
- if ((line[i] & 0xc0) != 0x80)
- tab++;
- i++;
- }
-
- if (i > org)
- hoedown_buffer_put(ob, line + org, i - org);
-
- if (i >= size)
- break;
-
- do {
- hoedown_buffer_putc(ob, ' '); tab++;
- } while (tab % 4);
-
- i++;
- }
-}
-
-/**********************
- * EXPORTED FUNCTIONS *
- **********************/
-
-hoedown_document *
-hoedown_document_new(
- const hoedown_renderer *renderer,
- hoedown_extensions extensions,
- size_t max_nesting)
-{
- hoedown_document *doc = NULL;
-
- assert(max_nesting > 0 && renderer);
-
- doc = hoedown_malloc(sizeof(hoedown_document));
- memcpy(&doc->md, renderer, sizeof(hoedown_renderer));
-
- doc->data.opaque = renderer->opaque;
-
- hoedown_stack_init(&doc->work_bufs[BUFFER_BLOCK], 4);
- hoedown_stack_init(&doc->work_bufs[BUFFER_SPAN], 8);
-
- memset(doc->active_char, 0x0, 256);
-
- if (extensions & HOEDOWN_EXT_UNDERLINE && doc->md.underline) {
- doc->active_char['_'] = MD_CHAR_EMPHASIS;
- }
-
- if (doc->md.emphasis || doc->md.double_emphasis || doc->md.triple_emphasis) {
- doc->active_char['*'] = MD_CHAR_EMPHASIS;
- doc->active_char['_'] = MD_CHAR_EMPHASIS;
- if (extensions & HOEDOWN_EXT_STRIKETHROUGH)
- doc->active_char['~'] = MD_CHAR_EMPHASIS;
- if (extensions & HOEDOWN_EXT_HIGHLIGHT)
- doc->active_char['='] = MD_CHAR_EMPHASIS;
- }
-
- if (doc->md.codespan)
- doc->active_char['`'] = MD_CHAR_CODESPAN;
-
- if (doc->md.linebreak)
- doc->active_char['\n'] = MD_CHAR_LINEBREAK;
-
- if (doc->md.image || doc->md.link || doc->md.footnotes || doc->md.footnote_ref)
- doc->active_char['['] = MD_CHAR_LINK;
-
- doc->active_char['<'] = MD_CHAR_LANGLE;
- doc->active_char['\\'] = MD_CHAR_ESCAPE;
- doc->active_char['&'] = MD_CHAR_ENTITY;
-
- if (extensions & HOEDOWN_EXT_AUTOLINK) {
- doc->active_char[':'] = MD_CHAR_AUTOLINK_URL;
- doc->active_char['@'] = MD_CHAR_AUTOLINK_EMAIL;
- doc->active_char['w'] = MD_CHAR_AUTOLINK_WWW;
- }
-
- if (extensions & HOEDOWN_EXT_SUPERSCRIPT)
- doc->active_char['^'] = MD_CHAR_SUPERSCRIPT;
-
- if (extensions & HOEDOWN_EXT_QUOTE)
- doc->active_char['"'] = MD_CHAR_QUOTE;
-
- if (extensions & HOEDOWN_EXT_MATH)
- doc->active_char['$'] = MD_CHAR_MATH;
-
- /* Extension data */
- doc->ext_flags = extensions;
- doc->max_nesting = max_nesting;
- doc->in_link_body = 0;
-
- return doc;
-}
-
-void
-hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size)
-{
- static const uint8_t UTF8_BOM[] = {0xEF, 0xBB, 0xBF};
-
- hoedown_buffer *text;
- size_t beg, end;
-
- int footnotes_enabled;
-
- text = hoedown_buffer_new(64);
-
- /* Preallocate enough space for our buffer to avoid expanding while copying */
- hoedown_buffer_grow(text, size);
-
- /* reset the references table */
- memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *));
-
- footnotes_enabled = doc->ext_flags & HOEDOWN_EXT_FOOTNOTES;
-
- /* reset the footnotes lists */
- if (footnotes_enabled) {
- memset(&doc->footnotes_found, 0x0, sizeof(doc->footnotes_found));
- memset(&doc->footnotes_used, 0x0, sizeof(doc->footnotes_used));
- }
-
- /* first pass: looking for references, copying everything else */
- beg = 0;
-
- /* Skip a possible UTF-8 BOM, even though the Unicode standard
- * discourages having these in UTF-8 documents */
- if (size >= 3 && memcmp(data, UTF8_BOM, 3) == 0)
- beg += 3;
-
- while (beg < size) /* iterating over lines */
- if (footnotes_enabled && is_footnote(data, beg, size, &end, &doc->footnotes_found))
- beg = end;
- else if (is_ref(data, beg, size, &end, doc->refs))
- beg = end;
- else { /* skipping to the next line */
- end = beg;
- while (end < size && data[end] != '\n' && data[end] != '\r')
- end++;
-
- /* adding the line body if present */
- if (end > beg)
- expand_tabs(text, data + beg, end - beg);
-
- while (end < size && (data[end] == '\n' || data[end] == '\r')) {
- /* add one \n per newline */
- if (data[end] == '\n' || (end + 1 < size && data[end + 1] != '\n'))
- hoedown_buffer_putc(text, '\n');
- end++;
- }
-
- beg = end;
- }
-
- /* pre-grow the output buffer to minimize allocations */
- hoedown_buffer_grow(ob, text->size + (text->size >> 1));
-
- /* second pass: actual rendering */
- if (doc->md.doc_header)
- doc->md.doc_header(ob, 0, &doc->data);
-
- if (text->size) {
- /* adding a final newline if not already present */
- if (text->data[text->size - 1] != '\n' && text->data[text->size - 1] != '\r')
- hoedown_buffer_putc(text, '\n');
-
- parse_block(ob, doc, text->data, text->size);
- }
-
- /* footnotes */
- if (footnotes_enabled)
- parse_footnote_list(ob, doc, &doc->footnotes_used);
-
- if (doc->md.doc_footer)
- doc->md.doc_footer(ob, 0, &doc->data);
-
- /* clean-up */
- hoedown_buffer_free(text);
- free_link_refs(doc->refs);
- if (footnotes_enabled) {
- free_footnote_list(&doc->footnotes_found, 1);
- free_footnote_list(&doc->footnotes_used, 0);
- }
-
- assert(doc->work_bufs[BUFFER_SPAN].size == 0);
- assert(doc->work_bufs[BUFFER_BLOCK].size == 0);
-}
-
-void
-hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size)
-{
- size_t i = 0, mark;
- hoedown_buffer *text = hoedown_buffer_new(64);
-
- /* reset the references table */
- memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *));
-
- /* first pass: expand tabs and process newlines */
- hoedown_buffer_grow(text, size);
- while (1) {
- mark = i;
- while (i < size && data[i] != '\n' && data[i] != '\r')
- i++;
-
- expand_tabs(text, data + mark, i - mark);
-
- if (i >= size)
- break;
-
- while (i < size && (data[i] == '\n' || data[i] == '\r')) {
- /* add one \n per newline */
- if (data[i] == '\n' || (i + 1 < size && data[i + 1] != '\n'))
- hoedown_buffer_putc(text, '\n');
- i++;
- }
- }
-
- /* second pass: actual rendering */
- hoedown_buffer_grow(ob, text->size + (text->size >> 1));
-
- if (doc->md.doc_header)
- doc->md.doc_header(ob, 1, &doc->data);
-
- parse_inline(ob, doc, text->data, text->size);
-
- if (doc->md.doc_footer)
- doc->md.doc_footer(ob, 1, &doc->data);
-
- /* clean-up */
- hoedown_buffer_free(text);
-
- assert(doc->work_bufs[BUFFER_SPAN].size == 0);
- assert(doc->work_bufs[BUFFER_BLOCK].size == 0);
-}
-
-void
-hoedown_document_free(hoedown_document *doc)
-{
- size_t i;
-
- for (i = 0; i < (size_t)doc->work_bufs[BUFFER_SPAN].asize; ++i)
- hoedown_buffer_free(doc->work_bufs[BUFFER_SPAN].item[i]);
-
- for (i = 0; i < (size_t)doc->work_bufs[BUFFER_BLOCK].asize; ++i)
- hoedown_buffer_free(doc->work_bufs[BUFFER_BLOCK].item[i]);
-
- hoedown_stack_uninit(&doc->work_bufs[BUFFER_SPAN]);
- hoedown_stack_uninit(&doc->work_bufs[BUFFER_BLOCK]);
-
- free(doc);
-}
diff --git a/Sources/CHoedown/escape.c b/Sources/CHoedown/escape.c
deleted file mode 100644
index 122c6ec..0000000
--- a/Sources/CHoedown/escape.c
+++ /dev/null
@@ -1,188 +0,0 @@
-#include "escape.h"
-
-#include
-#include
-#include
-
-
-#define likely(x) __builtin_expect((x),1)
-#define unlikely(x) __builtin_expect((x),0)
-
-
-/*
- * The following characters will not be escaped:
- *
- * -_.+!*'(),%#@?=;:/,+&$ alphanum
- *
- * Note that this character set is the addition of:
- *
- * - The characters which are safe to be in an URL
- * - The characters which are *not* safe to be in
- * an URL because they are RESERVED characters.
- *
- * We assume (lazily) that any RESERVED char that
- * appears inside an URL is actually meant to
- * have its native function (i.e. as an URL
- * component/separator) and hence needs no escaping.
- *
- * There are two exceptions: the chacters & (amp)
- * and ' (single quote) do not appear in the table.
- * They are meant to appear in the URL as components,
- * yet they require special HTML-entity escaping
- * to generate valid HTML markup.
- *
- * All other characters will be escaped to %XX.
- *
- */
-static const uint8_t HREF_SAFE[UINT8_MAX+1] = {
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1,
- 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
- 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-};
-
-void
-hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size)
-{
- static const char hex_chars[] = "0123456789ABCDEF";
- size_t i = 0, mark;
- char hex_str[3];
-
- hex_str[0] = '%';
-
- while (i < size) {
- mark = i;
- while (i < size && HREF_SAFE[data[i]]) i++;
-
- /* Optimization for cases where there's nothing to escape */
- if (mark == 0 && i >= size) {
- hoedown_buffer_put(ob, data, size);
- return;
- }
-
- if (likely(i > mark)) {
- hoedown_buffer_put(ob, data + mark, i - mark);
- }
-
- /* escaping */
- if (i >= size)
- break;
-
- switch (data[i]) {
- /* amp appears all the time in URLs, but needs
- * HTML-entity escaping to be inside an href */
- case '&':
- HOEDOWN_BUFPUTSL(ob, "&");
- break;
-
- /* the single quote is a valid URL character
- * according to the standard; it needs HTML
- * entity escaping too */
- case '\'':
- HOEDOWN_BUFPUTSL(ob, "'");
- break;
-
- /* the space can be escaped to %20 or a plus
- * sign. we're going with the generic escape
- * for now. the plus thing is more commonly seen
- * when building GET strings */
-#if 0
- case ' ':
- hoedown_buffer_putc(ob, '+');
- break;
-#endif
-
- /* every other character goes with a %XX escaping */
- default:
- hex_str[1] = hex_chars[(data[i] >> 4) & 0xF];
- hex_str[2] = hex_chars[data[i] & 0xF];
- hoedown_buffer_put(ob, (uint8_t *)hex_str, 3);
- }
-
- i++;
- }
-}
-
-
-/**
- * According to the OWASP rules:
- *
- * & --> &
- * < --> <
- * > --> >
- * " --> "
- * ' --> ' ' is not recommended
- * / --> / forward slash is included as it helps end an HTML entity
- *
- */
-static const uint8_t HTML_ESCAPE_TABLE[UINT8_MAX+1] = {
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-};
-
-static const char *HTML_ESCAPES[] = {
- "",
- """,
- "&",
- "'",
- "/",
- "<",
- ">"
-};
-
-void
-hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure)
-{
- size_t i = 0, mark;
-
- while (1) {
- mark = i;
- while (i < size && HTML_ESCAPE_TABLE[data[i]] == 0) i++;
-
- /* Optimization for cases where there's nothing to escape */
- if (mark == 0 && i >= size) {
- hoedown_buffer_put(ob, data, size);
- return;
- }
-
- if (likely(i > mark))
- hoedown_buffer_put(ob, data + mark, i - mark);
-
- if (i >= size) break;
-
- /* The forward slash is only escaped in secure mode */
- if (!secure && data[i] == '/') {
- hoedown_buffer_putc(ob, '/');
- } else {
- hoedown_buffer_puts(ob, HTML_ESCAPES[HTML_ESCAPE_TABLE[data[i]]]);
- }
-
- i++;
- }
-}
diff --git a/Sources/CHoedown/hoedown_html_patch.c b/Sources/CHoedown/hoedown_html_patch.c
deleted file mode 100644
index 4a9563f..0000000
--- a/Sources/CHoedown/hoedown_html_patch.c
+++ /dev/null
@@ -1,185 +0,0 @@
-//
-// hoedown_html_patch.c
-// MacDown
-//
-// Created by Tzu-ping Chung on 14/06/2014.
-// Copyright (c) 2014 Tzu-ping Chung . All rights reserved.
-//
-
-#include
-#include "escape.h"
-#include "document.h"
-#include "html.h"
-#include "hoedown_html_patch.h"
-
-#define USE_XHTML(opt) (opt->flags & HOEDOWN_HTML_USE_XHTML)
-#define USE_BLOCKCODE_INFORMATION(opt) \
- (opt->flags & HOEDOWN_HTML_BLOCKCODE_INFORMATION)
-#define USE_TASK_LIST(opt) (opt->flags & HOEDOWN_HTML_USE_TASK_LIST)
-
-// rndr_blockcode from HEAD. The "language-" prefix in class in needed to make
-// the HTML compatible with Prism.
-void hoedown_patch_render_blockcode(
- hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang,
- const hoedown_renderer_data *data)
-{
- if (ob->size) hoedown_buffer_putc(ob, '\n');
-
- hoedown_html_renderer_state *state = data->opaque;
- hoedown_html_renderer_state_extra *extra = state->opaque;
-
- hoedown_buffer *front = NULL;
- hoedown_buffer *back = NULL;
- if (lang && USE_BLOCKCODE_INFORMATION(state))
- {
- front = hoedown_buffer_new(lang->size);
- back = hoedown_buffer_new(lang->size);
-
- hoedown_buffer *current = front;
- for (size_t i = 0; i < lang->size; i++)
- {
- uint8_t c = lang->data[i];
- if (current == front && c == ':')
- current = back;
- else
- hoedown_buffer_putc(current, c);
- }
- lang = front;
- }
-
- hoedown_buffer *mapped = NULL;
- if (lang && extra->language_addition)
- {
- mapped = extra->language_addition(lang, extra->owner);
- if (mapped)
- lang = mapped;
- }
-
- HOEDOWN_BUFPUTSL(ob, "
flags & HOEDOWN_HTML_BLOCKCODE_LINE_NUMBERS)
- HOEDOWN_BUFPUTSL(ob, " class=\"line-numbers\"");
- if (back && back->size)
- {
- HOEDOWN_BUFPUTSL(ob, " data-information=\"");
- hoedown_buffer_put(ob, back->data, back->size);
- HOEDOWN_BUFPUTSL(ob, "\"");
- }
- HOEDOWN_BUFPUTSL(ob, ">size)
- hoedown_escape_html(ob, lang->data, lang->size, 0);
- else
- HOEDOWN_BUFPUTSL(ob, "none");
- HOEDOWN_BUFPUTSL(ob, "\">");
-
- if (text)
- {
- // Remove last newline to prevent prism from adding a blank line at the
- // end of code blocks.
- size_t size = text->size;
- if (size > 0 && text->data[size - 1] == '\n')
- size--;
- hoedown_escape_html(ob, text->data, size, 0);
- }
-
- HOEDOWN_BUFPUTSL(ob, "
\n");
-
- hoedown_buffer_free(mapped);
- hoedown_buffer_free(front);
- hoedown_buffer_free(back);
-}
-
-// Supports task list syntax if HOEDOWN_HTML_USE_TASK_LIST is on.
-// Implementation based on hoextdown.
-void hoedown_patch_render_listitem(
- hoedown_buffer *ob, const hoedown_buffer *text, hoedown_list_flags flags,
- const hoedown_renderer_data *data)
-{
- if (text)
- {
- hoedown_html_renderer_state *state = data->opaque;
- size_t offset = 0;
- if (flags & HOEDOWN_LI_BLOCK)
- offset = 3;
-
- // Do task list checkbox ([x] or [ ]).
- if (USE_TASK_LIST(state) && text->size >= 3)
- {
- if (strncmp((char *)(text->data + offset), "[ ]", 3) == 0)
- {
- HOEDOWN_BUFPUTSL(ob, "
\n");
-}
-
-// Adds a "toc" class to the outmost UL element to support TOC styling.
-void hoedown_patch_render_toc_header(
- hoedown_buffer *ob, const hoedown_buffer *content, int level,
- const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
-
- if (level <= state->toc_data.nesting_level) {
- /* set the level offset if this is the first header
- * we're parsing for the document */
- if (state->toc_data.current_level == 0)
- state->toc_data.level_offset = level - 1;
-
- level -= state->toc_data.level_offset;
-
- if (level > state->toc_data.current_level) {
- while (level > state->toc_data.current_level) {
- if (state->toc_data.current_level == 0)
- HOEDOWN_BUFPUTSL(ob, "
\n");
-}
-
-static void
-rndr_paragraph(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
- size_t i = 0;
-
- if (ob->size) hoedown_buffer_putc(ob, '\n');
-
- if (!content || !content->size)
- return;
-
- while (i < content->size && isspace(content->data[i])) i++;
-
- if (i == content->size)
- return;
-
- HOEDOWN_BUFPUTSL(ob, "
");
- if (state->flags & HOEDOWN_HTML_HARD_WRAP) {
- size_t org;
- while (i < content->size) {
- org = i;
- while (i < content->size && content->data[i] != '\n')
- i++;
-
- if (i > org)
- hoedown_buffer_put(ob, content->data + org, i - org);
-
- /*
- * do not insert a line break if this newline
- * is the last character on the paragraph
- */
- if (i >= content->size - 1)
- break;
-
- rndr_linebreak(ob, data);
- i++;
- }
- } else {
- hoedown_buffer_put(ob, content->data + i, content->size - i);
- }
- HOEDOWN_BUFPUTSL(ob, "
\n");
-}
-
-static void
-rndr_raw_block(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)
-{
- size_t org, sz;
-
- if (!text)
- return;
-
- /* FIXME: Do we *really* need to trim the HTML? How does that make a difference? */
- sz = text->size;
- while (sz > 0 && text->data[sz - 1] == '\n')
- sz--;
-
- org = 0;
- while (org < sz && text->data[org] == '\n')
- org++;
-
- if (org >= sz)
- return;
-
- if (ob->size)
- hoedown_buffer_putc(ob, '\n');
-
- hoedown_buffer_put(ob, text->data + org, sz - org);
- hoedown_buffer_putc(ob, '\n');
-}
-
-static int
-rndr_triple_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (!content || !content->size) return 0;
- HOEDOWN_BUFPUTSL(ob, "");
- hoedown_buffer_put(ob, content->data, content->size);
- HOEDOWN_BUFPUTSL(ob, "");
- return 1;
-}
-
-static void
-rndr_hrule(hoedown_buffer *ob, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
- if (ob->size) hoedown_buffer_putc(ob, '\n');
- hoedown_buffer_puts(ob, USE_XHTML(state) ? "\n" : "\n");
-}
-
-static int
-rndr_image(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
- if (!link || !link->size) return 0;
-
- HOEDOWN_BUFPUTSL(ob, "data, link->size);
- HOEDOWN_BUFPUTSL(ob, "\" alt=\"");
-
- if (alt && alt->size)
- escape_html(ob, alt->data, alt->size);
-
- if (title && title->size) {
- HOEDOWN_BUFPUTSL(ob, "\" title=\"");
- escape_html(ob, title->data, title->size); }
-
- hoedown_buffer_puts(ob, USE_XHTML(state) ? "\"/>" : "\">");
- return 1;
-}
-
-static int
-rndr_raw_html(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data)
-{
- hoedown_html_renderer_state *state = data->opaque;
-
- /* ESCAPE overrides SKIP_HTML. It doesn't look to see if
- * there are any valid tags, just escapes all of them. */
- if((state->flags & HOEDOWN_HTML_ESCAPE) != 0) {
- escape_html(ob, text->data, text->size);
- return 1;
- }
-
- if ((state->flags & HOEDOWN_HTML_SKIP_HTML) != 0)
- return 1;
-
- hoedown_buffer_put(ob, text->data, text->size);
- return 1;
-}
-
-static void
-rndr_table(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data)
-{
- if (ob->size) hoedown_buffer_putc(ob, '\n');
- HOEDOWN_BUFPUTSL(ob, "
", options: .caseInsensitive)
-
- private static func render(_ text: String,
- renderer: UnsafeMutablePointer,
- extensions: UInt32, smartyPants: Bool) -> String {
- let document = hoedown_document_new(
- renderer, hoedown_extensions(rawValue: extensions), nestingLevel)
- defer { hoedown_document_free(document) }
-
- var ob = hoedown_buffer_new(64)
- var utf8 = Array(text.utf8)
- utf8.withUnsafeMutableBufferPointer { buffer in
- hoedown_document_render(document, ob, buffer.baseAddress, buffer.count)
- }
- if smartyPants {
- let ib = ob
- ob = hoedown_buffer_new(64)
- hoedown_html_smartypants(ob, ib?.pointee.data, ib?.pointee.size ?? 0)
- hoedown_buffer_free(ib)
- }
- defer { hoedown_buffer_free(ob) }
- guard let ob, let data = ob.pointee.data else { return "" }
- let bytes = UnsafeBufferPointer(start: data, count: ob.pointee.size)
- return String(decoding: bytes, as: UTF8.self)
- }
-
- private static func createHTMLRenderer(
- flags: UInt32, owner: UnsafeMutableRawPointer
- ) -> UnsafeMutablePointer {
- let renderer = hoedown_html_renderer_new(
- hoedown_html_flags(rawValue: flags), tocLevel)!
- renderer.pointee.blockcode = hoedown_patch_render_blockcode
- renderer.pointee.listitem = hoedown_patch_render_listitem
-
- let extra = UnsafeMutablePointer
- .allocate(capacity: 1)
- extra.initialize(to: hoedown_html_renderer_state_extra(
- language_addition: languageAddition, owner: owner))
- let state = renderer.pointee.opaque
- .assumingMemoryBound(to: hoedown_html_renderer_state.self)
- state.pointee.opaque = UnsafeMutableRawPointer(extra)
- return renderer
- }
-
- private static func freeHTMLRenderer(_ renderer: UnsafeMutablePointer) {
- let state = renderer.pointee.opaque
- .assumingMemoryBound(to: hoedown_html_renderer_state.self)
- if let extra = state.pointee.opaque {
- extra.assumingMemoryBound(to: hoedown_html_renderer_state_extra.self)
- .deallocate()
- state.pointee.opaque = nil
- }
- hoedown_html_renderer_free(renderer)
- }
-}
-
-/// Called by the patched block code renderer for every fenced code block
-/// language. Maps aliases and records the Prism scripts required.
-private let languageAddition: @convention(c) (
- UnsafePointer?, UnsafeMutableRawPointer?
-) -> UnsafeMutablePointer? = { language, owner in
- guard let language, let owner, let data = language.pointee.data else {
- return nil
- }
- let collector = Unmanaged.fromOpaque(owner)
- .takeUnretainedValue()
- var lang = String(decoding: UnsafeBufferPointer(start: data,
- count: language.pointee.size),
- as: UTF8.self)
-
- // Try to identify alias and point it to the "real" language name.
- var mapped: UnsafeMutablePointer?
- if let real = PrismLanguages.aliases[lang] {
- lang = real
- let bytes = Array(real.utf8)
- mapped = hoedown_buffer_new(64)
- hoedown_buffer_put(mapped, bytes, bytes.count)
- }
- // Walk dependencies to include all required scripts.
- collector.add(lang)
- return mapped
-}
diff --git a/Sources/MacDownKit/Document/Model/CMarkTree.swift b/Sources/MacDownKit/Document/Model/CMarkTree.swift
index da883e0..d0cd29d 100644
--- a/Sources/MacDownKit/Document/Model/CMarkTree.swift
+++ b/Sources/MacDownKit/Document/Model/CMarkTree.swift
@@ -2,7 +2,7 @@
// CMarkTree.swift
// MacDownKit
//
-// One cmark-gfm parse (Decision 10): the C tree, walked in place by the
+// One cmark-gfm parse: the C tree, walked in place by the
// model, the highlighter and the renderer inside the parse task. Neither
// the tree nor its nodes are Sendable; only what's built from them leaves.
//
diff --git a/Sources/MacDownKit/Document/Model/ExtendedSyntax.swift b/Sources/MacDownKit/Document/Model/ExtendedSyntax.swift
index 3fcb0f3..4d6f39b 100644
--- a/Sources/MacDownKit/Document/Model/ExtendedSyntax.swift
+++ b/Sources/MacDownKit/Document/Model/ExtendedSyntax.swift
@@ -2,9 +2,9 @@
// ExtendedSyntax.swift
// MacDownKit
//
-// Opt-in syntax swift-markdown doesn't parse (finding F4), found in plain
-// text the same way for the editor highlighting and the preview (FR-19,
-// FR-19a), with hoedown's rules.
+// Opt-in syntax cmark-gfm doesn't parse (`==highlight==`, `x^2`), found in
+// plain text the same way for the editor highlighting and the preview,
+// with hoedown's rules.
//
import Foundation
diff --git a/Sources/MacDownKit/Document/Model/HTMLRenderer.swift b/Sources/MacDownKit/Document/Model/HTMLRenderer.swift
index 881b0a3..e091ba3 100644
--- a/Sources/MacDownKit/Document/Model/HTMLRenderer.swift
+++ b/Sources/MacDownKit/Document/Model/HTMLRenderer.swift
@@ -2,8 +2,8 @@
// HTMLRenderer.swift
// MacDownKit
//
-// The preview's HTML body from the cmark-gfm tree (FR-7 to FR-20, TR-4,
-// TR-5), in the markup hoedown and MacDown's patches produced, since the
+// The preview's HTML body from the cmark-gfm tree, in the markup hoedown
+// and MacDown's C patches produced, since the
// bundled styles and page scripts rely on it: header ids `toc_N`, Prism
// code blocks, task-list items, hoedown's footnotes. Runs inside the parse
// task; only the HTML string leaves it.
@@ -21,13 +21,13 @@ struct HTMLRenderer {
/// Every line break in a paragraph is a .
var hardWrap = false
/// Protected math, in source order, to put back in place of its
- /// filler (FR-14).
+ /// filler.
var math: [MathSpan] = []
- /// `==mark==` and `x^2` in plain text (FR-19, FR-19a).
+ /// `==mark==` and `x^2` in plain text.
var highlight = false
var superscript = false
/// Blocks name their first source line, for scroll sync (preview
- /// only; FR-18).
+ /// only: exports leave it out).
var sourceLines = false
}
@@ -215,7 +215,7 @@ struct HTMLRenderer {
while output.hasSuffix("\n") { output.removeLast() }
}
- /// MacDown's Prism-compatible block code (hoedown_html_patch.c).
+ /// MacDown's Prism-compatible block code (as its hoedown patch wrote it).
private mutating func codeBlock(_ node: CMarkNode) {
newline()
var language = (node.fenceInfo ?? "").split(whereSeparator: \.isWhitespace).first.map(String.init) ?? ""
@@ -394,7 +394,7 @@ struct HTMLRenderer {
pattern: "\\s*\\[TOC\\]\\s*", options: .caseInsensitive)
/// hoedown's TOC (with MacDown's "toc" class on the outer list) in place
- /// of every `[TOC]` paragraph, as MarkdownParser did.
+ /// of every `[TOC]` paragraph.
private mutating func insertTableOfContents() {
guard output.contains("TOC]") else { return }
var toc = ""
diff --git a/Sources/MacDownKit/Document/Model/HighlightMapper.swift b/Sources/MacDownKit/Document/Model/HighlightMapper.swift
index 50ef2cf..c42da16 100644
--- a/Sources/MacDownKit/Document/Model/HighlightMapper.swift
+++ b/Sources/MacDownKit/Document/Model/HighlightMapper.swift
@@ -2,12 +2,12 @@
// HighlightMapper.swift
// MacDownKit
//
-// Editor highlighting from the cmark-gfm tree (FR-21, FR-22): the spans
+// Editor highlighting from the cmark-gfm tree: the spans
// PEG Markdown Highlight produced, keyed by the same element types, so the
// bundled themes apply unchanged. Spans follow PEG's extents: headers cover
// their line(s) and line break, emphasis, code, links and images include
// their markup, fenced code is CODE and indented code VERBATIM, and a block
-// quote colors only its `>` markers. Footnotes are NOTE (FR-27).
+// quote colors only its `>` markers. Footnotes are NOTE.
//
// What the tree doesn't keep comes from the source: reference definitions
// (cmark consumes them) and entities (cmark decodes them). The same walk
diff --git a/Sources/MacDownKit/Document/Model/LineIndex.swift b/Sources/MacDownKit/Document/Model/LineIndex.swift
index e500f39..e540d01 100644
--- a/Sources/MacDownKit/Document/Model/LineIndex.swift
+++ b/Sources/MacDownKit/Document/Model/LineIndex.swift
@@ -3,8 +3,8 @@
// MacDownKit
//
// Converts swift-markdown source positions (1-based line, and 1-based column
-// in UTF-8 bytes) to the UTF-16 offsets NSString and NSTextView use
-// (TR-3). Lines end at "\n", "\r\n" or a lone "\r", as in cmark.
+// in UTF-8 bytes) to the UTF-16 offsets NSString and NSTextView use.
+// Lines end at "\n", "\r\n" or a lone "\r", as in cmark.
//
import Foundation
diff --git a/Sources/MacDownKit/Document/Model/MarkdownDocumentModel.swift b/Sources/MacDownKit/Document/Model/MarkdownDocumentModel.swift
index 5310e22..87fbbd3 100644
--- a/Sources/MacDownKit/Document/Model/MarkdownDocumentModel.swift
+++ b/Sources/MacDownKit/Document/Model/MarkdownDocumentModel.swift
@@ -2,14 +2,12 @@
// MarkdownDocumentModel.swift
// MacDownKit
//
-// One parse of a document with swift-markdown, shared by the preview, the
-// editor highlighting and scroll sync (FR-1 to FR-3). The cmark-gfm tree
-// (Decision 10) isn't Sendable, so the model is built where the parse runs
+// One parse of a document with cmark-gfm, shared by the preview, the
+// editor highlighting and scroll sync. The cmark-gfm tree isn't Sendable, so the model is built where the parse runs
// (a detached task) and keeps only what's produced from the tree. Only this
-// folder uses the cmark-gfm API (TR-2).
+// folder uses the cmark-gfm API.
//
-import CHoedown
import Foundation
public struct MarkdownDocumentModel: Sendable {
@@ -43,16 +41,16 @@ public struct MarkdownDocumentModel: Sendable {
}
public init(_ settings: ParseSettings) {
- math = settings.extensionFlags & HOEDOWN_EXT_MATH.rawValue != 0
- inlineDollar = math && settings.extensionFlags & HOEDOWN_EXT_MATH_EXPLICIT.rawValue != 0
- smartPunctuation = settings.smartyPants
- highlight = settings.extensionFlags & HOEDOWN_EXT_HIGHLIGHT.rawValue != 0
- superscript = settings.extensionFlags & HOEDOWN_EXT_SUPERSCRIPT.rawValue != 0
- autolink = settings.extensionFlags & HOEDOWN_EXT_AUTOLINK.rawValue != 0
- lineNumbers = settings.rendererFlags & UInt32(HOEDOWN_HTML_BLOCKCODE_LINE_NUMBERS) != 0
- blockCodeInformation = settings.rendererFlags & UInt32(HOEDOWN_HTML_BLOCKCODE_INFORMATION) != 0
+ math = settings.math
+ inlineDollar = settings.math && settings.inlineDollar
+ smartPunctuation = settings.smartPunctuation
+ highlight = settings.highlight
+ superscript = settings.superscript
+ autolink = settings.autolink
+ lineNumbers = settings.lineNumbers
+ blockCodeInformation = settings.blockCodeInformation
rendersTOC = settings.rendersTOC
- hardWrap = settings.rendererFlags & HOEDOWN_HTML_HARD_WRAP.rawValue != 0
+ hardWrap = settings.hardWrap
}
}
@@ -78,7 +76,7 @@ public struct MarkdownDocumentModel: Sendable {
public let math: [NSRange]
/// Blocks in document order (parents before their children).
public let blocks: [Block]
- /// Editor highlighting, by PEG element type (`ThemeStyle.elementNames`).
+ /// Editor highlighting, by `HighlightElement` (`ThemeStyle.elementNames`).
let highlights: HighlightElements
/// The preview's HTML body, and the Prism languages its code uses.
public let body: String
@@ -109,7 +107,7 @@ public struct MarkdownDocumentModel: Sendable {
highlight: options.highlight, superscript: options.superscript,
sourceLines: options.sourceLines))
languages = rendered.languages
- // Front matter is a table before the body (FR-15), as with hoedown.
+ // Front matter is a table before the body, as with hoedown.
if let table = protected.frontMatter?.object.htmlTable {
body = table + "\n" + rendered.html
} else {
diff --git a/Sources/MacDownKit/Document/Model/ProtectedSource.swift b/Sources/MacDownKit/Document/Model/ProtectedSource.swift
index 94400fb..a357bce 100644
--- a/Sources/MacDownKit/Document/Model/ProtectedSource.swift
+++ b/Sources/MacDownKit/Document/Model/ProtectedSource.swift
@@ -3,7 +3,7 @@
// MacDownKit
//
// Hides math and front matter from the Markdown parser without moving any
-// source position (FR-4, FR-5). Each protected span is replaced by filler of
+// source position. Each protected span is replaced by filler of
// the same UTF-8 length, keeping its line breaks. Math becomes characters
// no document uses (Private Use Area, padded with C0/C1 controls), which the
// parser takes as ordinary text and the renderer finds again to put the
diff --git a/Sources/MacDownKit/Document/PrismLanguages.swift b/Sources/MacDownKit/Document/PrismLanguages.swift
index 1db4e10..e7785f7 100644
--- a/Sources/MacDownKit/Document/PrismLanguages.swift
+++ b/Sources/MacDownKit/Document/PrismLanguages.swift
@@ -2,7 +2,7 @@
// PrismLanguages.swift
// MacDownKit
//
-// Code block languages for Prism, shared by both Markdown engines (FR-10):
+// Code block languages for Prism, shared by both Markdown engines:
// aliases map to Prism's names (`js` → `javascript`), and each language
// brings the scripts it depends on.
//
diff --git a/Sources/MacDownKit/Document/Renderer.swift b/Sources/MacDownKit/Document/Renderer.swift
index ecc0ec3..c10344a 100644
--- a/Sources/MacDownKit/Document/Renderer.swift
+++ b/Sources/MacDownKit/Document/Renderer.swift
@@ -7,7 +7,39 @@
//
import Foundation
-import CHoedown
+
+/// Settings that affect the Markdown → HTML body conversion. Standard
+/// Markdown (CommonMark and GFM: tables, task lists, strikethrough,
+/// footnotes, front matter) is always on; these are the opt-in extras.
+public struct ParseSettings: Sendable, Equatable {
+ /// Bare URLs and email addresses as links.
+ public var autolink = false
+ /// `==text==` as .
+ public var highlight = false
+ /// `x^2` and `x^(y z)` as .
+ public var superscript = false
+ /// TeX math for MathJax, and with `inlineDollar`, `$…$` inline.
+ public var math = false
+ public var inlineDollar = false
+ /// Curly quotes, dashes and ellipses.
+ public var smartPunctuation = false
+ /// Code blocks: line numbers, and the `lang:info` accessory.
+ public var lineNumbers = false
+ public var blockCodeInformation = false
+ /// Every newline in a paragraph is a line break.
+ public var hardWrap = false
+ /// A paragraph of just `[TOC]` becomes a table of contents.
+ public var rendersTOC = false
+
+ public init() {}
+}
+
+public struct ParseResult: Sendable, Equatable {
+ /// The rendered HTML body.
+ public var body: String
+ /// Prism languages used in code blocks, dependencies first.
+ public var languages: [String]
+}
/// Settings that affect how the HTML page is assembled around the body.
public struct PageSettings: Sendable, Equatable {
@@ -43,29 +75,20 @@ public struct RenderSettings: Sendable, Equatable {
@MainActor
extension Preferences {
- /// Standard Markdown (tables, fenced code, footnotes, strikethrough and
- /// intra-word emphasis) is always on; extended syntax is opt-in. Quote
- /// (`"…"` as ``) is dropped in favor of smart punctuation.
- public var extensionFlags: UInt32 {
- var flags: UInt32 = HOEDOWN_EXT_TABLES.rawValue | HOEDOWN_EXT_FENCED_CODE.rawValue
- | HOEDOWN_EXT_FOOTNOTES.rawValue | HOEDOWN_EXT_STRIKETHROUGH.rawValue
- if extensionAutolink { flags |= HOEDOWN_EXT_AUTOLINK.rawValue }
- if extensionHighlight { flags |= HOEDOWN_EXT_HIGHLIGHT.rawValue }
- if extensionSuperscript { flags |= HOEDOWN_EXT_SUPERSCRIPT.rawValue }
- if htmlMathJax { flags |= HOEDOWN_EXT_MATH.rawValue }
- if htmlMathJaxInlineDollar { flags |= HOEDOWN_EXT_MATH_EXPLICIT.rawValue }
- return flags
- }
-
- /// Task lists are standard (GFM) and always on.
- public var rendererFlags: UInt32 {
- var flags = UInt32(HOEDOWN_HTML_USE_TASK_LIST)
- if htmlLineNumbers { flags |= UInt32(HOEDOWN_HTML_BLOCKCODE_LINE_NUMBERS) }
- if htmlHardWrap { flags |= HOEDOWN_HTML_HARD_WRAP.rawValue }
- if codeBlockAccessory == .custom {
- flags |= UInt32(HOEDOWN_HTML_BLOCKCODE_INFORMATION)
- }
- return flags
+ /// The opt-in Markdown settings (standard Markdown is always on).
+ public var parseSettings: ParseSettings {
+ var settings = ParseSettings()
+ settings.autolink = extensionAutolink
+ settings.highlight = extensionHighlight
+ settings.superscript = extensionSuperscript
+ settings.math = htmlMathJax
+ settings.inlineDollar = htmlMathJaxInlineDollar
+ settings.smartPunctuation = extensionSmartyPants
+ settings.lineNumbers = htmlLineNumbers
+ settings.blockCodeInformation = codeBlockAccessory == .custom
+ settings.hardWrap = htmlHardWrap
+ settings.rendersTOC = htmlRendersTOC
+ return settings
}
public var renderSettings: RenderSettings {
@@ -82,14 +105,7 @@ extension Preferences {
page.mathJax = htmlMathJax
page.templateName = htmlTemplateName ?? "Default"
page.baseFontSize = htmlBaseFontSize
- return RenderSettings(
- parse: ParseSettings(extensionFlags: extensionFlags,
- rendererFlags: rendererFlags,
- smartyPants: extensionSmartyPants,
- rendersTOC: htmlRendersTOC,
- detectsFrontMatter: true,
- engine: markdownEngine),
- page: page)
+ return RenderSettings(parse: parseSettings, page: page)
}
}
@@ -280,7 +296,7 @@ public enum PageBuilder {
linkTransform: linkTransform)
}
- /// HTML without the preview's `data-source-line` attributes (FR-18),
+ /// HTML without the preview's `data-source-line` attributes,
/// for export and copying.
public static func withoutSourceLines(_ html: String) -> String {
guard html.contains(" data-source-line=") else { return html }
@@ -334,7 +350,8 @@ public enum PageBuilder {
@MainActor
public final class Renderer {
public private(set) var result = ParseResult(body: "", languages: [])
- /// The swift-markdown model of the same text, when that engine is on.
+ /// The document model the result was rendered from, which the editor
+ /// highlights from too; nil before the first parse.
public private(set) var model: MarkdownDocumentModel?
public private(set) var lastParseSettings: ParseSettings?
public private(set) var lastPageSettings: PageSettings?
@@ -378,10 +395,7 @@ public final class Renderer {
private nonisolated static func parse(
_ markdown: String, _ settings: ParseSettings
) -> (ParseResult, MarkdownDocumentModel?) {
- guard settings.engine == .cmarkGfm else {
- return (MarkdownParser.parse(markdown, settings: settings), nil)
- }
- // One cmark-gfm parse for the preview and the editor (FR-20, NFR-3).
+ // One cmark-gfm parse for the preview and the editor.
var options = MarkdownDocumentModel.Options(settings)
options.sourceLines = true
let model = MarkdownDocumentModel(markdown, options: options)
diff --git a/Sources/MacDownKit/Document/ScrollSync.swift b/Sources/MacDownKit/Document/ScrollSync.swift
index b651363..e31386b 100644
--- a/Sources/MacDownKit/Document/ScrollSync.swift
+++ b/Sources/MacDownKit/Document/ScrollSync.swift
@@ -4,196 +4,30 @@
//
// Scroll synchronization between the editor and the preview.
//
-// Both panes report "anchors". With the cmark-gfm engine the preview marks
-// every block with its source line, and anchors are those lines (at the
-// block's top), paired by line number. With hoedown they are headers and
-// stand-alone images, in document order, paired by index, so both sides
-// must apply the same rules (see `ScrollAnchors.scan` and
-// `PreviewController.fetchMetrics`).
-// Pairs give a piecewise linear map between editor and preview content
-// positions. Each pane is aligned at a focus line, which sits at the middle
-// of the visible area, except within the first and last screen of the
-// document where it tapers to the top and bottom edges, so both panes reach
-// their top and bottom together.
+// Both panes report "anchors": the preview marks every block with its
+// source line (`data-source-line`), and anchors are those lines (at the
+// block's top) in each pane, paired by line number. Pairs give a piecewise
+// linear map between editor and preview content positions. Each pane is
+// aligned at a focus line, which sits at the middle of the visible area,
+// except within the first and last screen of the document where it tapers
+// to the top and bottom edges, so both panes reach their top and bottom
+// together.
//
import Foundation
-public enum ScrollAnchorKind: Sendable, Equatable {
- case header
- case image
- /// A 1-based source line (the new engine's `data-source-line`).
- case line(Int)
-}
-
public struct ScrollAnchor: Sendable, Equatable {
- public var kind: ScrollAnchorKind
- /// Vertical center of the anchor, in content coordinates.
+ /// A 1-based source line.
+ public var line: Int
+ /// Where the line's block starts, in content coordinates.
public var position: CGFloat
- public init(_ kind: ScrollAnchorKind, _ position: CGFloat) {
- self.kind = kind
+ public init(line: Int, _ position: CGFloat) {
+ self.line = line
self.position = position
}
}
-/// Finds anchors in Markdown source, mirroring how hoedown renders them.
-///
-/// Headers are ATX headers (`#` in the first column) and setext headers
-/// (`===` or `---` under a paragraph line). Images count only when they make
-/// up a whole top-level paragraph, one anchor per image; the preview applies
-/// the same rule to `
` elements that contain nothing but images. Fenced
-/// code blocks, HTML comment blocks and front matter are skipped.
-public enum ScrollAnchors {
- public struct SourceAnchor: Sendable, Equatable {
- public var kind: ScrollAnchorKind
- /// Character range (UTF-16) whose center is the anchor.
- public var range: NSRange
- }
-
- private static let fenceRegex = try! NSRegularExpression(
- pattern: "^( {0,3})(`{3,}|~{3,})")
- private static let setextRegex = try! NSRegularExpression(
- pattern: "^(=+|-+) *$")
- private static let hruleRegex = try! NSRegularExpression(
- pattern: "^ {0,3}([-*_])( *\\1){2,} *$")
- private static let blockStartRegex = try! NSRegularExpression(
- pattern: "^( {0,3}([-*+]|\\d+\\.)[ \\t]| {0,3}>| {4}|\\t| {0,3}\\|)")
- private static let imageRegex = try! NSRegularExpression(
- pattern: "!\\[[^\\]]*\\](?:\\([^)]*\\)|\\[[^\\]]*\\])")
- private static let imageLineRegex = try! NSRegularExpression(
- pattern: "^ {0,3}(?:!\\[[^\\]]*\\](?:\\([^)]*\\)|\\[[^\\]]*\\])[ \\t]*)+$")
-
- private static func matches(_ regex: NSRegularExpression, _ line: String) -> Bool {
- regex.firstMatch(in: line, range: NSRange(location: 0, length: line.utf16.count))
- != nil
- }
-
- /// A code fence line, as hoedown's `is_codefence` sees it: up to three
- /// spaces, then three or more backticks or tildes.
- private static func fenceMarker(_ line: String)
- -> (indent: Int, character: Character, width: Int, rest: Substring)? {
- guard let match = fenceRegex.firstMatch(
- in: line, range: NSRange(location: 0, length: line.utf16.count)),
- let indent = Range(match.range(at: 1), in: line),
- let marker = Range(match.range(at: 2), in: line)
- else { return nil }
- return (line[indent].count, line[marker].first!, line[marker].count,
- line[marker.upperBound...])
- }
-
- /// - Parameter fencedCode: Whether hoedown's fenced code extension is on.
- public static func scan(_ markdown: String, skipsFrontMatter: Bool,
- fencedCode: Bool = true) -> [SourceAnchor] {
- var anchors: [SourceAnchor] = []
- var location = 0
- if skipsFrontMatter {
- location = markdown.frontMatter().offset
- }
- let text = (markdown as NSString).substring(from: location)
-
- var fence: (indent: Int, character: Character, width: Int)?
- var inComment = false
- // Lines of the current paragraph, as (location, line).
- var paragraph: [(Int, String)] = []
- // Whether the previous line can be the text of a setext header.
- var previousIsParagraphText = false
-
- func flushParagraph() {
- defer { paragraph.removeAll() }
- guard !paragraph.isEmpty,
- paragraph.allSatisfy({ matches(imageLineRegex, $0.1) })
- else { return }
- for (lineLocation, line) in paragraph {
- let ns = line as NSString
- for match in imageRegex.matches(
- in: line, range: NSRange(location: 0, length: ns.length)) {
- anchors.append(SourceAnchor(
- kind: .image,
- range: NSRange(location: lineLocation + match.range.location,
- length: match.range.length)))
- }
- }
- }
-
- for line in text.components(separatedBy: "\n") {
- let lineLocation = location
- let length = (line as NSString).length
- location += length + 1
- let trimmed = line.trimmingCharacters(in: .whitespaces)
-
- if let open = fence {
- // Closed by the same fence (indent, character and width)
- // followed by nothing but spaces (parse_fencedcode).
- if let close = fenceMarker(line), close.indent == open.indent,
- close.character == open.character, close.width == open.width,
- close.rest.allSatisfy({ $0 == " " }) {
- fence = nil
- }
- continue
- }
- if inComment {
- if line.contains("-->") { inComment = false }
- continue
- }
- // Fences only start blocks; they don't interrupt paragraphs. A
- // line with the fence characters again (```` ``a`` ````) is a
- // code span, not a fence (parse_codefence).
- if fencedCode, paragraph.isEmpty, let open = fenceMarker(line),
- !open.rest.drop(while: { $0 == " " || $0 == "\t" })
- .contains(String(repeating: open.character, count: 3)) {
- previousIsParagraphText = false
- fence = (open.indent, open.character, open.width)
- continue
- }
- if trimmed.isEmpty {
- flushParagraph()
- previousIsParagraphText = false
- continue
- }
- if paragraph.isEmpty && line.hasPrefix("")
- previousIsParagraphText = false
- continue
- }
- if line.hasPrefix("#") {
- flushParagraph()
- anchors.append(SourceAnchor(
- kind: .header, range: NSRange(location: lineLocation, length: length)))
- previousIsParagraphText = false
- continue
- }
- if previousIsParagraphText && matches(setextRegex, line),
- let last = paragraph.last {
- // The paragraph's last line becomes the header text.
- paragraph.removeLast()
- flushParagraph()
- anchors.append(SourceAnchor(
- kind: .header,
- range: NSRange(location: last.0, length: location - 1 - last.0)))
- previousIsParagraphText = false
- continue
- }
- if matches(hruleRegex, line) {
- flushParagraph()
- previousIsParagraphText = false
- continue
- }
- if paragraph.isEmpty && matches(blockStartRegex, line) {
- // Lists, quotes, code and tables: not top-level paragraphs.
- // Following lines belong to the same block until a blank line.
- paragraph.append((lineLocation, "\u{0}"))
- previousIsParagraphText = false
- continue
- }
- paragraph.append((lineLocation, line))
- previousIsParagraphText = paragraph.first?.1 != "\u{0}"
- }
- flushParagraph()
- return anchors
- }
-}
-
/// Vertical geometry of one scrolling pane.
public struct ScrollGeometry: Sendable, Equatable {
public var contentHeight: CGFloat
@@ -253,32 +87,17 @@ public struct ScrollMap: Sendable, Equatable {
/// - previewEnd: Preview content height.
public init(editor: [ScrollAnchor], preview: [ScrollAnchor],
editorEnd: CGFloat, previewEnd: CGFloat) {
- var editor = editor
- var preview = preview
- if case .line = preview.first?.kind {
- // Pair by line number, in preview order.
- var editorByLine: [Int: CGFloat] = [:]
- for anchor in editor {
- if case .line(let line) = anchor.kind, editorByLine[line] == nil {
- editorByLine[line] = anchor.position
- }
- }
- let paired = preview.compactMap { anchor -> (ScrollAnchor, ScrollAnchor)? in
- guard case .line(let line) = anchor.kind, let y = editorByLine[line] else { return nil }
- return (ScrollAnchor(anchor.kind, y), anchor)
- }
- editor = paired.map(\.0)
- preview = paired.map(\.1)
- } else if editor.map(\.kind) != preview.map(\.kind) {
- // Something the rules didn't anticipate; headers are the most
- // reliable, so try them alone before giving up on anchors.
- editor = editor.filter { $0.kind == .header }
- preview = preview.filter { $0.kind == .header }
- if editor.count != preview.count {
- editor = []
- preview = []
- }
+ // Pair by line number, in preview order.
+ var editorByLine: [Int: CGFloat] = [:]
+ for anchor in editor where editorByLine[anchor.line] == nil {
+ editorByLine[anchor.line] = anchor.position
+ }
+ let paired = preview.compactMap { anchor -> (ScrollAnchor, ScrollAnchor)? in
+ guard let y = editorByLine[anchor.line] else { return nil }
+ return (ScrollAnchor(line: anchor.line, y), anchor)
}
+ let editor = paired.map(\.0)
+ let preview = paired.map(\.1)
var pairs: [(editor: CGFloat, preview: CGFloat)] = [(0, 0)]
for (e, p) in zip(editor, preview) {
guard let last = pairs.last, e.position > last.editor,
diff --git a/Sources/MacDownKit/Editor/EditorTextView+Paste.swift b/Sources/MacDownKit/Editor/EditorTextView+Paste.swift
index c7de1e3..7c9ba31 100644
--- a/Sources/MacDownKit/Editor/EditorTextView+Paste.swift
+++ b/Sources/MacDownKit/Editor/EditorTextView+Paste.swift
@@ -10,8 +10,8 @@ import AppKit
extension EditorTextView {
// MARK: - Paste
- /// Paste, Services and text dropped from other apps all read here
- /// (FR-8): formatted text becomes Markdown and image data an image;
+ /// Paste, Services and text dropped from other apps all read here:
+ /// formatted text becomes Markdown and image data an image;
/// anything else is read as usual.
public override func readSelection(from pasteboard: NSPasteboard,
type: NSPasteboard.PasteboardType) -> Bool {
@@ -34,14 +34,14 @@ extension EditorTextView {
.filter { !super.readablePasteboardTypes.contains($0) }
}
- /// Paste and Match Style: always the plain text (FR-4).
+ /// Paste and Match Style: always the plain text.
public override func pasteAsPlainText(_ sender: Any?) {
pastesPlainText = true
defer { pastesPlainText = false }
super.pasteAsPlainText(sender)
}
- /// Edit ▸ Paste as Markdown: converts even with the setting off (FR-28).
+ /// Edit ▸ Paste as Markdown: converts even with the setting off.
@objc public func pasteAsMarkdown(_ sender: Any?) {
if !pasteConverted(from: .general, convertsFormatting: true) {
super.paste(sender)
@@ -51,8 +51,8 @@ extension EditorTextView {
/// Inserts the Markdown for `pasteboard`'s formatted text, or its
/// image, in place of the selection, as one undoable change. False when
/// there's nothing to convert, so the caller pastes as usual: plain
- /// text, code (FR-6), a paste into a code block (FR-5), or formatted
- /// text when not converting it (FR-27; images are pasted anyway, FR-34).
+ /// text, code, a paste into a code block, or formatted text when not
+ /// converting it (images are pasted either way).
@discardableResult
func pasteConverted(from pasteboard: NSPasteboard, convertsFormatting: Bool? = nil) -> Bool {
let selection = selectedRange()
@@ -82,7 +82,7 @@ extension EditorTextView {
/// Inserts pasted Markdown as its own undo step (not merged with the
/// typing before it), through the usual text-change path so the
- /// document sees it like typing (FR-7). Blocks are set off from the
+ /// document sees it like typing. Blocks are set off from the
/// text around them by blank lines.
/// The range of the inserted text.
@discardableResult
@@ -102,7 +102,7 @@ extension EditorTextView {
// MARK: - Paste as Plain Text
/// Shows a button just below `range`, what a converted paste inserted,
- /// that puts `plainText` there instead (FR-31). It never takes focus,
+ /// that puts `plainText` there instead. It never takes focus,
/// so typing goes on in the editor.
func showPlainTextButton(replacing range: NSRange, with plainText: String) {
dismissPlainTextButton()
@@ -156,8 +156,8 @@ extension EditorTextView {
breakUndoCoalescing()
}
- /// Removes the button, and lets go of the pasted range and plain text
- /// (FR-32). It stays until then: there's no timeout.
+ /// Removes the button, and lets go of the pasted range and plain text.
+ /// It stays until then: there's no timeout.
func dismissPlainTextButton() {
guard let button = plainTextButton else { return }
button.removeFromSuperview()
@@ -215,7 +215,7 @@ extension EditorTextView {
// MARK: - Code blocks
- /// Whether `location` is inside a fenced or indented code block (FR-5):
+ /// Whether `location` is inside a fenced or indented code block:
/// by scanning the fences above it, which also works when the latest
/// parse is behind the text, or, for indented code, from the parse.
func isInCodeBlock(_ location: Int) -> Bool {
diff --git a/Sources/MacDownKit/Editor/EditorTextView.swift b/Sources/MacDownKit/Editor/EditorTextView.swift
index 3544687..d82f330 100644
--- a/Sources/MacDownKit/Editor/EditorTextView.swift
+++ b/Sources/MacDownKit/Editor/EditorTextView.swift
@@ -31,15 +31,15 @@ public final class EditorTextView: NSTextView {
/// where code blocks are when pasting (see EditorTextView+Paste.swift).
var documentBlocks: (source: String, blocks: [MarkdownDocumentModel.Block])?
- /// Whether Paste converts formatted text to Markdown (FR-27), and how
+ /// Whether Paste converts formatted text to Markdown, and how
/// it writes it: the writer's list marker and indentation.
public var convertsPastedFormatting = true
public var pasteOptions = HTMLToMarkdown.Options()
/// Set during Paste and Match Style, which reads through the same
- /// method as Paste but always pastes plain text (FR-4).
+ /// method as Paste but always pastes plain text.
var pastesPlainText = false
/// After a converted paste: the "Paste as Plain Text" button, and what
- /// the paste inserted and the plain text to put there instead (FR-31).
+ /// the paste inserted and the plain text to put there instead.
var plainTextButton: NSButton?
var pastedPlainText: (range: NSRange, inserted: String, text: String)?
diff --git a/Sources/MacDownKit/Editor/ImageMarkup.swift b/Sources/MacDownKit/Editor/ImageMarkup.swift
index 57e7d8a..653f465 100644
--- a/Sources/MacDownKit/Editor/ImageMarkup.swift
+++ b/Sources/MacDownKit/Editor/ImageMarkup.swift
@@ -3,7 +3,7 @@
// MacDown
//
// Images embedded in the document as data URIs: image files dropped on the
-// editor, and image data pasted into it (paste intent, FR-33).
+// editor, and image data pasted into it.
//
import Foundation
diff --git a/Sources/MacDownKit/Editor/MarkdownHighlighter.swift b/Sources/MacDownKit/Editor/MarkdownHighlighter.swift
index 2be254a..bab7059 100644
--- a/Sources/MacDownKit/Editor/MarkdownHighlighter.swift
+++ b/Sources/MacDownKit/Editor/MarkdownHighlighter.swift
@@ -3,12 +3,12 @@
// MacDown
//
// Swift port of HGMarkdownHighlighter and HGMarkdownHighlightingStyle from
-// PEG Markdown Highlight (Copyright 2011-2013 Ali Rantakari, MIT/GPL2+).
-// The PEG parser and stylesheet parser remain in C (CPegMarkdown).
+// PEG Markdown Highlight (Copyright 2011-2013 Ali Rantakari, MIT/GPL2+; see
+// Licenses/peg-markdown-highlight.txt). The spans now come from the
+// cmark-gfm document model; PEG's C parser is no longer used.
//
import AppKit
-import CPegMarkdown
/// A single highlighted span, in UTF-16 offsets.
struct HighlightSpan: Sendable {
@@ -17,63 +17,19 @@ struct HighlightSpan: Sendable {
var address: String?
}
-/// Parsed spans, indexed by `pmh_element_type` raw value.
+/// The element types themes style and the highlighter highlights: PEG
+/// Markdown Highlight's (`pmh_element_type`, in its order, which the themes'
+/// names follow; see `ThemeStyle.elementNames`), then those it didn't have.
+enum HighlightElement: Int, CaseIterable {
+ case link, autoLinkURL, autoLinkEmail, image, code, html, htmlEntity, emph, strong
+ case listBullet, listEnumerator, comment, h1, h2, h3, h4, h5, h6, blockquote, verbatim
+ case htmlBlock, hrule, reference, note, math, highlight, superscript
+}
+
+/// Highlight spans, indexed by `HighlightElement` raw value. They come from
+/// the document model (`HighlightMapper`).
struct HighlightElements: Sendable {
var spans: [[HighlightSpan]]
-
- static func parse(_ markdown: String, extensions: Int32) -> HighlightElements {
- var result: UnsafeMutablePointer?>?
- markdown.withCString { cString in
- pmh_markdown_to_elements(UnsafeMutablePointer(mutating: cString),
- extensions, &result)
- }
- guard let result else {
- return HighlightElements(spans: Array(repeating: [],
- count: Int(pmh_NUM_LANG_TYPES)))
- }
- pmh_sort_elements_by_pos(result)
- defer { pmh_free_elements(result) }
-
- // The parser reports Unicode code point offsets; NSString uses UTF-16.
- // Gather indexes of surrogate pairs to convert.
- let utf16 = Array(markdown.utf16)
- var surrogatePairIndexes: [Int] = []
- for (i, unit) in utf16.enumerated() where UTF16.isLeadSurrogate(unit) {
- surrogatePairIndexes.append(i)
- }
-
- var spans: [[HighlightSpan]] = []
- for langType in 0.. Int { Int(type.rawValue) }
+ func t(_ type: HighlightElement) -> Int { type.rawValue }
var styles: [HighlightingStyle] = []
- for h in [pmh_H1, pmh_H2, pmh_H3, pmh_H4, pmh_H5, pmh_H6] {
+ for h in [HighlightElement.h1, .h2, .h3, .h4, .h5, .h6] {
styles.append(.init(elementType: t(h), foreground: dark(blue),
background: light(blue), traits: .boldFontMask))
}
styles += [
- .init(elementType: t(pmh_HRULE), foreground: darkGray, background: lightGray),
- .init(elementType: t(pmh_LIST_BULLET), foreground: dark(magenta)),
- .init(elementType: t(pmh_LIST_ENUMERATOR), foreground: dark(magenta)),
- .init(elementType: t(pmh_LINK), foreground: dark(cyan), background: light(cyan)),
- .init(elementType: t(pmh_AUTO_LINK_URL), foreground: dark(cyan), background: light(cyan)),
- .init(elementType: t(pmh_AUTO_LINK_EMAIL), foreground: dark(cyan), background: light(cyan)),
- .init(elementType: t(pmh_IMAGE), foreground: dark(magenta), background: light(magenta)),
- .init(elementType: t(pmh_REFERENCE), foreground: dim(red)),
- .init(elementType: t(pmh_CODE), foreground: dark(green), background: light(green)),
- .init(elementType: t(pmh_EMPH), foreground: dark(yellow), traits: .italicFontMask),
- .init(elementType: t(pmh_STRONG), foreground: dark(magenta), traits: .boldFontMask),
- .init(elementType: t(pmh_HTML_ENTITY), foreground: medGray),
- .init(elementType: t(pmh_COMMENT), foreground: medGray),
- .init(elementType: t(pmh_VERBATIM), foreground: dark(green), background: light(green)),
- .init(elementType: t(pmh_BLOCKQUOTE), foreground: dark(magenta),
+ .init(elementType: t(.hrule), foreground: darkGray, background: lightGray),
+ .init(elementType: t(.listBullet), foreground: dark(magenta)),
+ .init(elementType: t(.listEnumerator), foreground: dark(magenta)),
+ .init(elementType: t(.link), foreground: dark(cyan), background: light(cyan)),
+ .init(elementType: t(.autoLinkURL), foreground: dark(cyan), background: light(cyan)),
+ .init(elementType: t(.autoLinkEmail), foreground: dark(cyan), background: light(cyan)),
+ .init(elementType: t(.image), foreground: dark(magenta), background: light(magenta)),
+ .init(elementType: t(.reference), foreground: dim(red)),
+ .init(elementType: t(.code), foreground: dark(green), background: light(green)),
+ .init(elementType: t(.emph), foreground: dark(yellow), traits: .italicFontMask),
+ .init(elementType: t(.strong), foreground: dark(magenta), traits: .boldFontMask),
+ .init(elementType: t(.htmlEntity), foreground: medGray),
+ .init(elementType: t(.comment), foreground: medGray),
+ .init(elementType: t(.verbatim), foreground: dark(green), background: light(green)),
+ .init(elementType: t(.blockquote), foreground: dark(magenta),
traits: .unboldFontMask),
]
return styles
@@ -182,14 +138,9 @@ public final class MarkdownHighlighter {
public weak var textView: NSTextView? {
didSet { if textView != nil { readClearTextStylesFromTextView() } }
}
- public var extensions: Int32 = Int32(pmh_EXT_NONE.rawValue)
- public var waitInterval: TimeInterval = 0
public var resetTypingAttributes = true
public var makeLinksClickable = false
public private(set) var isActive = false
- /// Spans come from elsewhere (the swift-markdown document model, via
- /// `update(_:)`) instead of the highlighter's own PEG parse.
- public var usesExternalElements = false
/// Whether the idle pass is styling the rest of the document. Its storage
/// edits come a chunk at a time; layout-dependent work can wait for
/// `onIdlePassFinished` instead of following each one.
@@ -209,7 +160,6 @@ public final class MarkdownHighlighter {
private var defaultTextSize: CGFloat = 0
private var defaultTextColor: NSColor?
private var defaultTypingAttributes: [NSAttributedString.Key: Any] = [:]
- private var parseGeneration = 0
/// Character ranges styled with `cachedElements`.
private(set) var highlightedRanges = IndexSet()
private var isHighlighting = false
@@ -224,31 +174,10 @@ public final class MarkdownHighlighter {
if textView != nil { readClearTextStylesFromTextView() }
}
- // MARK: - Parsing
-
- func requestParsing() {
- guard let textView, !usesExternalElements else { return }
- parseGeneration += 1
- let generation = parseGeneration
- let markdown = textView.string
- let extensions = self.extensions
- Task { [weak self] in
- let elements = await Task.detached(priority: .userInitiated) {
- HighlightElements.parse(markdown, extensions: extensions)
- }.value
- guard let self, self.isActive, generation == self.parseGeneration
- else { return }
- self.setElements(elements)
- }
- }
-
- public func parseAndHighlightNow() {
- requestParsing()
- }
+ // MARK: - Spans
- /// Highlights with spans parsed elsewhere, for the current text.
+ /// Highlights with the document model's spans for the current text.
func update(_ elements: HighlightElements) {
- parseGeneration += 1 // Any PEG parse in flight is stale.
if isActive {
setElements(elements)
} else {
@@ -343,8 +272,7 @@ public final class MarkdownHighlighter {
clearHighlighting(in: range)
let sourceLength = storage.length
- let linkTypes = [pmh_LINK, pmh_AUTO_LINK_URL, pmh_AUTO_LINK_EMAIL]
- .map { Int($0.rawValue) }
+ let linkTypes = [HighlightElement.link, .autoLinkURL, .autoLinkEmail].map(\.rawValue)
for style in styles {
guard style.elementType < elements.spans.count else { continue }
for span in elements.spans[style.elementType] {
@@ -360,7 +288,7 @@ public final class MarkdownHighlighter {
if makeLinksClickable, linkTypes.contains(style.elementType),
var address = span.address {
- if style.elementType == Int(pmh_AUTO_LINK_EMAIL.rawValue),
+ if style.elementType == HighlightElement.autoLinkEmail.rawValue,
!address.hasPrefix("mailto:") {
address = "mailto:" + address
}
@@ -583,7 +511,7 @@ public final class MarkdownHighlighter {
private func applyStyleDependencies() {
guard let textView else { return }
// Make link styles match the styles set for LINK elements.
- if let style = styles.first(where: { $0.elementType == Int(pmh_LINK.rawValue) }) {
+ if let style = styles.first(where: { $0.elementType == HighlightElement.link.rawValue }) {
var attrs = style.attributesToAdd
attrs[.cursor] = NSCursor.pointingHand
textView.linkTextAttributes = attrs
@@ -652,14 +580,9 @@ public final class MarkdownHighlighter {
guard let textView else { return }
applyStyleDependencies()
isActive = true
- requestParsing()
+ if cachedElements != nil { highlightVisibleRange() }
let center = NotificationCenter.default
- observers.append(center.addObserver(
- forName: NSText.didChangeNotification, object: textView, queue: .main
- ) { [weak self] _ in
- MainActor.assumeIsolated { self?.textDidChange() }
- })
// Edits move the text, so what was highlighted no longer lines up.
// Posted synchronously, while `editedMask` is valid, and for
// programmatic changes too.
@@ -697,23 +620,12 @@ public final class MarkdownHighlighter {
guard isActive else { return }
observers.forEach(NotificationCenter.default.removeObserver)
observers.removeAll()
- cachedElements = nil
+ // The spans are kept: they're the model's, for the current text,
+ // and restyle the editor when it's activated again.
highlightedRanges = IndexSet()
- parseGeneration += 1
isActive = false
}
- private func textDidChange() {
- if waitInterval <= 0 {
- requestParsing()
- } else {
- DispatchQueue.main.asyncAfter(deadline: .now() + waitInterval) {
- [weak self] in
- MainActor.assumeIsolated { self?.requestParsing() }
- }
- }
- }
-
private func textViewDidScroll() {
// Style newly revealed text right away, before it's drawn; mostly
// it's already styled by the overscan or the idle pass.
diff --git a/Sources/MacDownKit/Editor/PasteboardContent.swift b/Sources/MacDownKit/Editor/PasteboardContent.swift
index 578f7ec..cf0cd76 100644
--- a/Sources/MacDownKit/Editor/PasteboardContent.swift
+++ b/Sources/MacDownKit/Editor/PasteboardContent.swift
@@ -20,8 +20,8 @@ enum PasteboardContent: Equatable {
static let jpeg = NSPasteboard.PasteboardType("public.jpeg")
/// The clipboard's formatted text or image, in order of preference:
- /// HTML (or a web archive), then rich text (FR-1, FR-2), then image
- /// data when there's no text at all (FR-33). Nil for plain text.
+ /// HTML (or a web archive), then rich text, then image
+ /// data when there's no text at all. Nil for plain text.
@MainActor static func read(from pasteboard: NSPasteboard) -> PasteboardContent? {
let types = pasteboard.types ?? []
let archive = types.contains(webArchive) ? pasteboard.data(forType: webArchive).flatMap(mainResource) : nil
diff --git a/Sources/MacDownKit/Editor/ThemeStyleParser.swift b/Sources/MacDownKit/Editor/ThemeStyleParser.swift
index 79a47a0..50637c7 100644
--- a/Sources/MacDownKit/Editor/ThemeStyleParser.swift
+++ b/Sources/MacDownKit/Editor/ThemeStyleParser.swift
@@ -3,8 +3,9 @@
// MacDownKit
//
// Parses editor themes (`.style` files) — a Swift port of PEG Markdown
-// Highlight's pmh_styleparser.c, so the themes keep working once the C
-// library is gone (swift-markdown migration, Phase 2). Element names,
+// Highlight's pmh_styleparser.c (Copyright 2011 Ali Rantakari; see
+// Licenses/peg-markdown-highlight.txt), so the themes keep working without
+// the C library, which MacDown no longer uses. Element names,
// attributes, colors, font traits and error messages are the same, as are
// the C parser's quirks: a rule's attributes come out in reverse order,
// and a later `editor`, `editor-current-line` or `editor-selection` rule
diff --git a/Sources/MacDownKit/Preferences/PreferenceSettingKey.swift b/Sources/MacDownKit/Preferences/PreferenceSettingKey.swift
index 87f01ad..6eba4fc 100644
--- a/Sources/MacDownKit/Preferences/PreferenceSettingKey.swift
+++ b/Sources/MacDownKit/Preferences/PreferenceSettingKey.swift
@@ -23,8 +23,6 @@ public enum PreferenceSettingKey: String, CaseIterable, Sendable {
case extensionHighlight
case extensionSmartyPants
case markdownManualRender
- /// Hidden: the Markdown engine (cmark-gfm or hoedown).
- case markdownEngine
// MARK: Editor
case editorBaseFontInfo
@@ -84,8 +82,8 @@ public enum PreferenceSettingKey: String, CaseIterable, Sendable {
// MARK: Retired
// Settings that are no longer read: standard Markdown is always on, and
- // Quote and Underline are gone. Their saved values are left in place
- // (FR-36), so a downgrade still finds them.
+ // Quote and Underline are gone. Their saved values are left in place,
+ // so a downgrade still finds them.
case extensionTables
case extensionFencedCode
case extensionFootnotes
diff --git a/Sources/MacDownKit/Preferences/Preferences.swift b/Sources/MacDownKit/Preferences/Preferences.swift
index 2c7289f..08f2b71 100644
--- a/Sources/MacDownKit/Preferences/Preferences.swift
+++ b/Sources/MacDownKit/Preferences/Preferences.swift
@@ -23,15 +23,6 @@ public extension Notification.Name {
static let didRequestPreviewRender = Notification.Name("MPDidRequestPreviewRender")
}
-/// The Markdown parser behind the preview and highlighting.
-/// `cmarkGfm` is cmark-gfm, the default; `hoedown` is the
-/// original's engine, kept selectable for one release before it's removed
-/// (docs/intents/swift-markdown-migration.md, Phase 5). Hidden: set with
-/// `defaults write io.github.levous.macdown-swift markdownEngine hoedown`.
-public enum MarkdownEngine: String, CaseIterable, Sendable {
- case hoedown, cmarkGfm
-}
-
public enum UnorderedListMarkerType: Int, CaseIterable, Sendable {
case asterisk = 0, plusSign, minusSign
@@ -124,8 +115,7 @@ public final class Preferences {
public var previewZoomRelativeToBaseFontSize = false { didSet { save(previewZoomRelativeToBaseFontSize, .previewZoomRelativeToBaseFontSize) } }
- /// Not shown in Settings (FR-32).
- public var markdownEngine = MarkdownEngine.cmarkGfm { didSet { save(markdownEngine.rawValue, .markdownEngine) } }
+ /// Not shown in Settings.
// MARK: HTML rendering
@@ -290,7 +280,6 @@ public final class Preferences {
editorConvertsPastedFormatting = d.bool(forKey: .editorConvertsPastedFormatting)
previewZoomRelativeToBaseFontSize = d.bool(forKey: .previewZoomRelativeToBaseFontSize)
- markdownEngine = d.string(forKey: .markdownEngine).flatMap(MarkdownEngine.init) ?? .cmarkGfm
htmlTemplateName = d.string(forKey: .htmlTemplateName)
htmlStyleName = d.string(forKey: .htmlStyleName)
htmlHardWrap = d.bool(forKey: .htmlHardWrap)
diff --git a/Sources/MacDownKit/Preview/PreviewController.swift b/Sources/MacDownKit/Preview/PreviewController.swift
index 180dab5..b1df504 100644
--- a/Sources/MacDownKit/Preview/PreviewController.swift
+++ b/Sources/MacDownKit/Preview/PreviewController.swift
@@ -28,7 +28,7 @@ public struct TextCount: Equatable, Sendable {
}
public struct PreviewMetrics: Sendable {
- /// Scroll sync anchors, in CSS pixels; see `ScrollAnchors`.
+ /// Scroll sync anchors (blocks' source lines), in CSS pixels.
public var anchors: [ScrollAnchor] = []
public var contentHeight: CGFloat = 0
public var visibleHeight: CGFloat = 0
@@ -284,44 +284,14 @@ public final class PreviewController: NSObject {
}
public func fetchMetrics() async -> PreviewMetrics {
- // The new engine marks blocks with their source line: report each
- // one's top. Otherwise mirror ScrollAnchors.scan: top-level headers,
- // and images in top-level paragraphs that contain nothing else.
+ // Every block is marked with its source line: report each one's top.
let script = """
(function () {
var anchors = [];
var lines = document.querySelectorAll("[data-source-line]");
- if (lines.length > 0) {
- Array.prototype.forEach.call(lines, function (node) {
- anchors.push(["l", node.getBoundingClientRect().top + window.scrollY,
- parseInt(node.getAttribute("data-source-line"), 10)]);
- });
- return JSON.stringify({
- anchors: anchors,
- contentHeight: document.documentElement.scrollHeight,
- visibleHeight: window.innerHeight
- });
- }
- function add(kind, node) {
- var rect = node.getBoundingClientRect();
- anchors.push([kind, rect.top + window.scrollY + rect.height / 2]);
- }
- // The body, unless a custom template wraps it.
- var first = document.querySelector("h1, h2, h3, h4, h5, h6, p");
- var container = (first && first.parentElement) || document.body;
- Array.prototype.forEach.call(container.children, function (node) {
- if (/^H[1-6]$/.test(node.tagName)) {
- add("h", node);
- } else if (node.tagName === "P" && node.textContent.trim() === ""
- && node.querySelector("img")) {
- var children = Array.prototype.slice.call(node.children);
- if (children.every(function (c) {
- return c.tagName === "IMG" || c.tagName === "BR"; })) {
- children.forEach(function (c) {
- if (c.tagName === "IMG") add("i", c);
- });
- }
- }
+ Array.prototype.forEach.call(lines, function (node) {
+ anchors.push([parseInt(node.getAttribute("data-source-line"), 10),
+ node.getBoundingClientRect().top + window.scrollY]);
});
return JSON.stringify({
anchors: anchors,
@@ -337,13 +307,9 @@ public final class PreviewController: NSObject {
else { return PreviewMetrics() }
var metrics = PreviewMetrics()
metrics.anchors = (object["anchors"] as? [[Any]] ?? []).compactMap {
- guard $0.count >= 2, let kind = $0[0] as? String,
- let y = $0[1] as? NSNumber else { return nil }
- if kind == "l" {
- guard $0.count == 3, let line = $0[2] as? NSNumber else { return nil }
- return ScrollAnchor(.line(line.intValue), CGFloat(y.doubleValue))
- }
- return ScrollAnchor(kind == "h" ? .header : .image, CGFloat(y.doubleValue))
+ guard $0.count == 2, let line = $0[0] as? NSNumber, let y = $0[1] as? NSNumber
+ else { return nil }
+ return ScrollAnchor(line: line.intValue, CGFloat(y.doubleValue))
}
metrics.contentHeight = CGFloat((object["contentHeight"] as? NSNumber)?
.doubleValue ?? 0)
diff --git a/Sources/MacDownKit/Resources/Extensions/mermaid.init.js b/Sources/MacDownKit/Resources/Extensions/mermaid.init.js
index 632cc53..93ffd12 100644
--- a/Sources/MacDownKit/Resources/Extensions/mermaid.init.js
+++ b/Sources/MacDownKit/Resources/Extensions/mermaid.init.js
@@ -1,6 +1,6 @@
// Renders ```mermaid code blocks as diagrams (Mermaid 12).
//
-// Each block is hoedown's
; the
+// Each block is MacDown's
; the
//
is replaced with the diagram. The preview calls
// MacDownMermaid.render() again after it replaces the page body in place.
diff --git a/Tests/MacDownKitTests/CoreTests.swift b/Tests/MacDownKitTests/CoreTests.swift
index 9bdc686..978797d 100644
--- a/Tests/MacDownKitTests/CoreTests.swift
+++ b/Tests/MacDownKitTests/CoreTests.swift
@@ -8,7 +8,6 @@
import AppKit
import Foundation
import Testing
-import CHoedown
@testable import MacDownKit
private func fixture(_ name: String, _ ext: String) -> URL {
@@ -252,9 +251,9 @@ private func fixture(_ name: String, _ ext: String) -> URL {
defaults.set(true, forKey: .extensionUnderline)
let preferences = Preferences(defaults: defaults)
preferences.extensionHighlight = true
- let html = MarkdownParser.parse(
+ let html = MarkdownDocumentModel(
"_italic_ __bold__ *italic* **bold** under ==mark==\n",
- settings: preferences.renderSettings.parse).body
+ options: .init(preferences.renderSettings.parse)).body
#expect(html.contains("italicbolditalicbold"))
#expect(html.contains("under"))
#expect(html.contains("mark"))
@@ -291,18 +290,20 @@ private func fixture(_ name: String, _ ext: String) -> URL {
defaults.set(false, forKey: key)
}
defaults.set(true, forKey: .extensionQuote)
- let flags = Preferences(defaults: defaults).extensionFlags
- for flag in [HOEDOWN_EXT_TABLES, HOEDOWN_EXT_FENCED_CODE, HOEDOWN_EXT_FOOTNOTES,
- HOEDOWN_EXT_STRIKETHROUGH] {
- #expect(flags & flag.rawValue != 0, "\(flag) is off")
+ let settings = Preferences(defaults: defaults).renderSettings.parse
+ #expect(settings == ParseSettings())
+ let html = MarkdownDocumentModel(
+ "| a |\n|---|\n| 1 |\n\n```\ncode\n```\n\nn[^1] ~~x~~ in*tra*word \"q\"\n\n[^1]: N.\n",
+ options: .init(settings)).body
+ for element in ["
", "
", "class=\"footnotes\"", "x", "intraword"] {
+ #expect(html.contains(element), "missing \(element)")
}
- // Intra-word emphasis on (no NO_INTRA_EMPHASIS); Quote is dropped.
- #expect(flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS.rawValue == 0)
- #expect(flags & HOEDOWN_EXT_QUOTE.rawValue == 0)
+ // Quote is dropped.
+ #expect(!html.contains(""))
}
/// Converting pasted formatting is on for new installs and for existing
- /// ones that predate it, and stays off once turned off (FR-27).
+ /// ones that predate it, and stays off once turned off.
@Test func convertsPastedFormattingByDefault() throws {
for existing in [false, true] {
let suite = "MacDownTests-\(UUID().uuidString)"
@@ -316,22 +317,6 @@ private func fixture(_ name: String, _ ext: String) -> URL {
}
}
- /// The hidden engine switch for the swift-markdown migration (FR-32).
- @Test func markdownEngine() throws {
- let suite = "MacDownTests-\(UUID().uuidString)"
- let defaults = try #require(UserDefaults(suiteName: suite))
- defer { defaults.removePersistentDomain(forName: suite) }
- // cmark-gfm ("cmarkGfm") is the default; hoedown stays selectable.
- #expect(Preferences(defaults: defaults).markdownEngine == .cmarkGfm)
- defaults.set("hoedown", forKey: .markdownEngine)
- #expect(Preferences(defaults: defaults).markdownEngine == .hoedown)
- defaults.set("bogus", forKey: .markdownEngine)
- #expect(Preferences(defaults: defaults).markdownEngine == .cmarkGfm)
- let preferences = Preferences(defaults: defaults)
- preferences.markdownEngine = .hoedown
- #expect(defaults.string(forKey: .markdownEngine) == "hoedown")
- }
-
/// New installs follow common Markdown editing conventions.
@Test func freshInstallEditingDefaults() throws {
let suite = "MacDownTests-\(UUID().uuidString)"
@@ -382,7 +367,7 @@ private func fixture(_ name: String, _ ext: String) -> URL {
}
/// Settings that became standard are no longer read, but their saved
- /// values stay in user defaults (FR-36), so a downgrade still finds them.
+ /// values stay in user defaults, so a downgrade still finds them.
@Test func removedSettingsKeepTheirSavedValues() throws {
let suite = "MacDownTests-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
@@ -405,41 +390,35 @@ private func fixture(_ name: String, _ ext: String) -> URL {
defaults.set(false, forKey: .htmlTaskList)
defaults.set(false, forKey: .htmlDetectFrontMatter)
let settings = Preferences(defaults: defaults).renderSettings
- #expect(settings.parse.rendererFlags & UInt32(HOEDOWN_HTML_USE_TASK_LIST) != 0)
#expect(settings.page.taskList) // The checkbox script.
- #expect(settings.parse.detectsFrontMatter)
- let html = MarkdownParser.parse("---\ntitle: Notes\n---\n\n- [x] done\n",
- settings: settings.parse).body
+ let html = MarkdownDocumentModel("---\ntitle: Notes\n---\n\n- [x] done\n",
+ options: .init(settings.parse)).body
#expect(html.contains("
"))
#expect(result.body.contains("world"))
}
- @Test(arguments: MarkdownEngine.allCases)
- func fencedCodeMapsAliasesAndCollectsLanguages(_ engine: MarkdownEngine) {
- let result = parse(engine, "```js\nvar a = 1;\n```\n\n```c++\nint x;\n```\n")
+ @Test
+ func fencedCodeMapsAliasesAndCollectsLanguages() {
+ let result = parse("```js\nvar a = 1;\n```\n\n```c++\nint x;\n```\n")
#expect(result.body.contains("var a = 1;"))
#expect(result.body.contains("
"))
#expect(result.languages.contains("javascript"))
@@ -478,9 +457,8 @@ private func fixture(_ name: String, _ ext: String) -> URL {
[^n]: The note.
"""
- // Both engines.
- for html in [MarkdownParser.parse(text, settings: settings).body,
- MarkdownDocumentModel(text, options: .init(settings)).body] {
+ do {
+ let html = MarkdownDocumentModel(text, options: .init(settings)).body
for element in ["
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "
",
- "cmark": "",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "
",
- "cmark": "",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "
1) ",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": " 2) ",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": " ",
- "cmark": "",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "
",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "
",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "",
- "cmark": "",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": " - ",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark blocks: 7 hashes and #Not are paragraphs, list tightness, a new list when the marker changes, 1) lists, start numbers, indented code in items, a list may interrupt a paragraph"
- },
- {
- "document": "03-blockquotes.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark: quotes separated by a blank line are two quotes"
- },
- {
- "document": "03-blockquotes.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark: quotes separated by a blank line are two quotes"
- },
- {
- "document": "03-blockquotes.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark: quotes separated by a blank line are two quotes"
- },
- {
- "document": "04-code.md",
- "hoedown": "
swift",
- "cmark": "```swift",
- "reason": "CommonMark: a fence inside a code span stays inline"
- },
- {
- "document": "04-code.md",
- "hoedown": "`",
- "cmark": "",
- "reason": "CommonMark: a fence inside a code span stays inline"
- },
- {
- "document": "05-links-images.md",
- "hoedown": "[shortcut],",
- "cmark": "shortcut,",
- "reason": "CommonMark: an angle-bracket destination may contain spaces, so the definition and the shortcut link work"
- },
- {
- "document": "05-links-images.md",
- "hoedown": "
[shortcut]: ",
- "cmark": "",
- "reason": "CommonMark: an angle-bracket destination may contain spaces, so the definition and the shortcut link work"
- },
- {
- "document": "07-html.md",
- "hoedown": "
",
- "cmark": "",
- "reason": "CommonMark HTML blocks; an unknown entity is escaped; becomes U+FFFD"
- },
- {
- "document": "07-html.md",
- "hoedown": "
",
- "cmark": "",
- "reason": "CommonMark HTML blocks; an unknown entity is escaped; becomes U+FFFD"
- },
- {
- "document": "07-html.md",
- "hoedown": "&unknownentity;",
- "cmark": "&unknownentity;",
- "reason": "CommonMark HTML blocks; an unknown entity is escaped; becomes U+FFFD"
- },
- {
- "document": "07-html.md",
- "hoedown": "\u0000",
- "cmark": "�",
- "reason": "CommonMark HTML blocks; an unknown entity is escaped; becomes U+FFFD"
- },
- {
- "document": "08-footnotes.md",
- "hoedown": "twice[^1].",
- "cmark": "twice1.",
- "reason": "GFM footnotes: one referenced twice links both times"
- },
- {
- "document": "10-front-matter-invalid.md",
- "hoedown": "
",
- "cmark": " ",
- "reason": "CommonMark list tightness"
- },
- {
- "document": "08-footnotes.md",
- "hoedown": "twice[1].",
- "cmark": "twice1",
- "reason": "hoedown read footnote syntax as superscript; GFM footnotes come first"
- },
- {
- "document": "08-footnotes.md",
- "hoedown": "",
- "cmark": ".",
- "reason": "hoedown read footnote syntax as superscript; GFM footnotes come first"
- },
- {
- "document": "08-footnotes.md",
- "hoedown": "definition[missing].",
- "cmark": "definition[^missing].",
- "reason": "hoedown read footnote syntax as superscript; GFM footnotes come first"
- },
- {
- "document": "14-math.md",
- "hoedown": "$x1",
- "cmark": "$x_1",
- "reason": "math off: the text is Markdown"
- },
- {
- "document": "14-math.md",
- "hoedown": "y2$.",
- "cmark": "y_2$.",
- "reason": "math off: the text is Markdown"
- },
- {
- "document": "07-html.md",
- "hoedown": "",
- "cmark": " &unknownentity;",
- "reason": "same entity rules"
- },
- {
- "document": "07-html.md",
- "hoedown": "&unknownentity;",
- "cmark": "�",
- "reason": "same entity rules"
- },
- {
- "document": "14-math.md",
- "hoedown": "\\(5",
- "cmark": "$5",
- "reason": "\"$5 and $10\" isn't math (finding F7); hoedown took it"
- },
- {
- "document": "14-math.md",
- "hoedown": "\\)10,",
- "cmark": "$10,",
- "reason": "\"$5 and $10\" isn't math (finding F7); hoedown took it"
- },
- {
- "document": "01-inline.md",
- "hoedown": " ",
- "cmark": "",
- "reason": "hoedown added a second after an explicit break, and kept the backslash"
- },
- {
- "document": "01-inline.md",
- "hoedown": "backslash\\",
- "cmark": "backslash",
- "reason": "hoedown added a second after an explicit break, and kept the backslash"
- },
- {
- "document": "02-blocks.md",
- "hoedown": " 2) ",
- "cmark": "
",
- "reason": "same CommonMark block rules, seen with hard wrap"
- },
- {
- "document": "02-blocks.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "same CommonMark block rules, seen with hard wrap"
- },
- {
- "document": "02-blocks.md",
- "hoedown": " - ",
- "cmark": "
",
- "reason": "same CommonMark block rules, seen with hard wrap"
- },
- {
- "document": "07-html.md",
- "hoedown": " ",
- "cmark": "",
- "reason": "CommonMark HTML blocks with hard wrap"
- },
- {
- "document": "10-front-matter-invalid.md",
- "hoedown": "
",
- "cmark": " ",
- "reason": "same, with hard wrap"
- },
- {
- "document": "07-html.md",
- "hoedown": "&unknownentity;",
- "cmark": "&unknownentity; �",
- "reason": "same entity rules"
- },
- {
- "document": "14-math.md",
- "hoedown": " ",
- "cmark": "",
- "reason": "hard wrap: hoedown doubled an explicit break"
- },
- {
- "document": "help.md",
- "hoedown": "",
- "cmark": "",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "",
- "cmark": "",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "",
- "cmark": "",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "",
- "cmark": "",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "
",
- "cmark": "",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "
",
- "cmark": " ",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "",
- "cmark": " ",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "",
- "cmark": "
",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "",
- "cmark": "",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "",
- "cmark": "",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "\\,",
- "cmark": ",",
- "reason": "CommonMark: ***Both*** nests as ; list tightness, ordered start numbers, and a new list when the marker changes; math off: \\, is an escape"
- },
- {
- "document": "help.md",
- "hoedown": "<a name="emphasis"></a>Emphasis",
- "cmark": "Emphasis",
- "reason": "TOC entries drop inline HTML (hoedown showed the anchors as escaped text)"
- },
- {
- "document": "help.md",
- "hoedown": "<a name="standard-extensions"></a>Standard",
- "cmark": "Standard",
- "reason": "TOC entries drop inline HTML (hoedown showed the anchors as escaped text)"
- },
- {
- "document": "help.md",
- "hoedown": "<a name="fenced-code-block"></a>Fenced",
- "cmark": "Fenced",
- "reason": "TOC entries drop inline HTML (hoedown showed the anchors as escaped text)"
- },
- {
- "document": "help.md",
- "hoedown": "<a",
- "cmark": "The",
- "reason": "TOC entries drop inline HTML (hoedown showed the anchors as escaped text)"
- },
- {
- "document": "help.md",
- "hoedown": "name="markdown-pane"></a>The ",
- "cmark": "",
- "reason": "TOC entries drop inline HTML (hoedown showed the anchors as escaped text)"
- },
- {
- "document": "help.md",
- "hoedown": "<a name="rendering-pane"></a>The",
- "cmark": "The",
- "reason": "TOC entries drop inline HTML (hoedown showed the anchors as escaped text)"
- },
- {
- "document": "help.md",
- "hoedown": "<a name="general-pane"></a>The",
- "cmark": "The",
- "reason": "TOC entries drop inline HTML (hoedown showed the anchors as escaped text)"
- },
- {
- "document": "help.md",
- "hoedown": "<a name="editor-pane"></a>The",
- "cmark": "The",
- "reason": "TOC entries drop inline HTML (hoedown showed the anchors as escaped text)"
- },
- {
- "document": "help.md",
- "hoedown": " ",
- "cmark": "",
- "reason": "hoedown added a second after two-space and backslash breaks"
- },
- {
- "document": "help.md",
- "hoedown": " ",
- "cmark": "",
- "reason": "hoedown added a second after two-space and backslash breaks"
- },
- {
- "document": "help.md",
- "hoedown": "<a name="markdown-pane"></a>The",
- "cmark": "The",
- "reason": "TOC entries drop inline HTML"
- },
- {
- "document": "help.md",
- "hoedown": "name="rendering-pane"></a>The ",
- "cmark": "",
- "reason": "TOC entries drop inline HTML"
- }
-]
diff --git a/Tests/MacDownKitTests/Resources/reviewed-paste-diffs.json b/Tests/MacDownKitTests/Resources/reviewed-paste-diffs.json
index 49b6737..58209e1 100644
--- a/Tests/MacDownKitTests/Resources/reviewed-paste-diffs.json
+++ b/Tests/MacDownKitTests/Resources/reviewed-paste-diffs.json
@@ -20,13 +20,13 @@
},
{
"id": "footnote-back-links",
- "reason": "Footnotes aren't round-tripped: a note's back-link (↩) is dropped with the space before it (FR-26).",
+ "reason": "Footnotes aren't round-tripped: a note's back-link (↩) is dropped with the space before it.",
"find": "(?:\\s| )*.*?",
"replace": ""
},
{
"id": "in-page-links",
- "reason": "Links within the copied page (#anchor) and empty links keep only their text (FR-14).",
+ "reason": "Links within the copied page (#anchor) and empty links keep only their text.",
"find": "(.*?)",
"replace": "$1"
},
@@ -38,7 +38,7 @@
},
{
"id": "kbd-as-code",
- "reason": " is monospace text, written as a code span (FR-12).",
+ "reason": " is monospace text, written as a code span.",
"find": "<(/?)kbd>",
"replace": "<$1code>"
},
@@ -50,13 +50,13 @@
},
{
"id": "no-equivalent",
- "reason": "Elements without a Markdown equivalent keep only their text (FR-19).",
+ "reason": "Elements without a Markdown equivalent keep only their text.",
"find": "?(?:sub|sup|u|mark|q|span|small|abbr|ins)(?: [^>]*)?>",
"replace": ""
},
{
"id": "comments-and-scripts",
- "reason": "Comments and scripts aren't content (FR-19).",
+ "reason": "Comments and scripts aren't content.",
"find": "|",
"replace": ""
}
@@ -96,25 +96,25 @@
"document": "07-html.md",
"original": "",
"roundTrip": "",
- "reason": "A pipe table always has a header row, so an HTML table without one gets its first row as the header (FR-18)."
+ "reason": "A pipe table always has a header row, so an HTML table without one gets its first row as the header."
},
{
"document": "07-html.md",
"original": "
",
"roundTrip": "
",
- "reason": "A pipe table always has a header row, so an HTML table without one gets its first row as the header (FR-18)."
+ "reason": "A pipe table always has a header row, so an HTML table without one gets its first row as the header."
},
{
"document": "07-html.md",
"original": "",
"roundTrip": "
",
- "reason": "A pipe table always has a header row, so an HTML table without one gets its first row as the header (FR-18)."
+ "reason": "A pipe table always has a header row, so an HTML table without one gets its first row as the header."
},
{
"document": "07-html.md",
"original": "",
"roundTrip": "",
- "reason": "A pipe table always has a header row, so an HTML table without one gets its first row as the header (FR-18)."
+ "reason": "A pipe table always has a header row, so an HTML table without one gets its first row as the header."
},
{
"document": "help.md",
@@ -138,43 +138,43 @@
"document": "help.md",
"original": ",",
"roundTrip": "1+1,",
- "reason": "MathML has no Markdown equivalent: it keeps its text (FR-19)."
+ "reason": "MathML has no Markdown equivalent: it keeps its text."
},
{
"document": "help.md",
"original": "",
"roundTrip": "",
- "reason": "MathML has no Markdown equivalent: it keeps its text (FR-19)."
+ "reason": "MathML has no Markdown equivalent: it keeps its text."
}
]
}
\ No newline at end of file
diff --git a/Tests/MacDownKitTests/ScrollSyncTests.swift b/Tests/MacDownKitTests/ScrollSyncTests.swift
index bb69e5b..affd479 100644
--- a/Tests/MacDownKitTests/ScrollSyncTests.swift
+++ b/Tests/MacDownKitTests/ScrollSyncTests.swift
@@ -7,59 +7,6 @@ import AppKit
import Testing
@testable import MacDownKit
-@Suite struct ScrollAnchorsTests {
- func kinds(_ markdown: String, frontMatter: Bool = false) -> [String] {
- let ns = markdown as NSString
- return ScrollAnchors.scan(markdown, skipsFrontMatter: frontMatter).map {
- ($0.kind == .header ? "h:" : "i:") + ns.substring(with: $0.range)
- }
- }
-
- @Test func headers() {
- #expect(kinds("# One\ntext\n\n##Two\n") == ["h:# One", "h:##Two"])
- #expect(kinds("Title\n=====\n\nSub\n--- \n") == ["h:Title\n=====", "h:Sub\n--- "])
- // A rule under an ATX header, list or blank line is not a header.
- #expect(kinds("# One\n---\n") == ["h:# One"])
- #expect(kinds("- item\n---\n") == [])
- #expect(kinds("\n---\n") == [])
- }
-
- @Test func skipsCodeCommentsAndFrontMatter() {
- #expect(kinds("```sh\n# comment\n\n```\n# Real\n") == ["h:# Real"])
- #expect(kinds("~~~~\n```\n# no\n~~~~\n") == [])
- }
-
- @Test func fencesFollowHoedown() {
- // A line that repeats the fence characters is a code span.
- #expect(kinds("```` ``a`` ````\n\n# H\n") == ["h:# H"])
- // Only the same width (and indent) closes a fence.
- #expect(kinds("````\n```\n# no\n````\n# yes\n") == ["h:# yes"])
- #expect(kinds("```\n# no\n ```\n# no\n```\n# yes\n") == ["h:# yes"])
- // Fences don't interrupt paragraphs.
- #expect(kinds("text\n```\n# H\n") == ["h:# H"])
- #expect(ScrollAnchors.scan("```\n# H\n```\n", skipsFrontMatter: false,
- fencedCode: false).count == 1)
- #expect(kinds("\n# Shown\n") == ["h:# Shown"])
- #expect(kinds("---\ntitle: x\n---\n# A\n", frontMatter: true) == ["h:# A"])
- #expect(kinds("---\ntitle: x\n---\n# A\n", frontMatter: false)
- == ["h:title: x\n---", "h:# A"])
- }
-
- @Test func imagesInImageOnlyParagraphs() {
- #expect(kinds("\n") == ["i:"])
- // Consecutive lines make one paragraph; every image is an anchor.
- #expect(kinds("\n ![c][r]\n")
- == ["i:", "i:", "i:![c][r]"])
- // Not stand-alone: text, links, lists, quotes, indented code.
- #expect(kinds("Text \n") == [])
- #expect(kinds("Text\n\n") == [])
- #expect(kinds("[](http://x)\n") == [])
- #expect(kinds("- \n") == [])
- #expect(kinds("> \n") == [])
- #expect(kinds(" \n") == [])
- }
-}
-
@Suite struct ScrollMapTests {
let editor = ScrollGeometry(contentHeight: 1000, visibleHeight: 200)
let preview = ScrollGeometry(contentHeight: 3000, visibleHeight: 400)
@@ -74,10 +21,10 @@ import Testing
}
@Test func alignsAnchorsAtCenterInBothDirections() {
- let map = ScrollMap(editor: [ScrollAnchor(.header, 300), ScrollAnchor(.image, 500)],
- preview: [ScrollAnchor(.header, 700), ScrollAnchor(.image, 1800)],
+ let map = ScrollMap(editor: [ScrollAnchor(line: 3, 300), ScrollAnchor(line: 5, 500)],
+ preview: [ScrollAnchor(line: 3, 700), ScrollAnchor(line: 5, 1800)],
editorEnd: 1000, previewEnd: 3000)
- // Image anchor centered in the editor → centered in the preview.
+ // Line 5 centered in the editor → centered in the preview.
let y = map.previewOffset(forEditorOffset: 400, editor: editor, preview: preview)
#expect(abs(y - (1800 - 200)) < 0.01)
let back = map.editorOffset(forPreviewOffset: 1600, editor: editor, preview: preview)
@@ -88,27 +35,19 @@ import Testing
== preview.maxOffset)
}
- /// Source-line anchors (the new engine) pair by line number; lines one
- /// side lacks are skipped.
+ /// Anchors pair by line number; lines one side lacks are skipped.
@Test func linesPairByNumber() {
- let map = ScrollMap(editor: [ScrollAnchor(.line(1), 0), ScrollAnchor(.line(5), 200),
- ScrollAnchor(.line(9), 400)],
- preview: [ScrollAnchor(.line(1), 10), ScrollAnchor(.line(3), 300),
- ScrollAnchor(.line(9), 1500), ScrollAnchor(.line(5), 2000)],
+ let map = ScrollMap(editor: [ScrollAnchor(line: 1, 0), ScrollAnchor(line: 5, 200),
+ ScrollAnchor(line: 9, 400)],
+ preview: [ScrollAnchor(line: 1, 10), ScrollAnchor(line: 3, 300),
+ ScrollAnchor(line: 9, 1500), ScrollAnchor(line: 5, 2000)],
editorEnd: 1000, previewEnd: 3000)
// Line 3 has no editor anchor; line 5 after 9 isn't increasing.
#expect(map.pairs.map(\.editor) == [0, 400, 1000])
#expect(map.pairs.map(\.preview) == [0, 1500, 3000])
}
- @Test func mismatchedAnchorsFallBack() {
- // Image anchors disagree; headers still pair up.
- let map = ScrollMap(editor: [ScrollAnchor(.image, 100), ScrollAnchor(.header, 300)],
- preview: [ScrollAnchor(.header, 900)],
- editorEnd: 1000, previewEnd: 3000)
- #expect(map.pairs.map(\.editor) == [0, 300, 1000])
- #expect(map.pairs.map(\.preview) == [0, 900, 3000])
- }
+
}
extension LiveDocumentTests {
@@ -204,72 +143,50 @@ extension LiveDocumentTests {
"window.scrollY") as? NSNumber)?.doubleValue ?? -1)
}
- /// Sync scrolling on, a fixed style, and the engine given (hoedown's
- /// header and image anchors, or cmark-gfm's source lines).
- func withSyncScrolling(engine: MarkdownEngine = .cmarkGfm,
- _ body: () async throws -> Void) async rethrows {
+ /// Sync scrolling on, and a fixed style.
+ func withSyncScrolling(_ body: () async throws -> Void) async rethrows {
let preferences = Preferences.shared
- let saved = (preferences.editorSyncScrolling, preferences.htmlStyleName,
- preferences.markdownEngine)
+ let saved = (preferences.editorSyncScrolling, preferences.htmlStyleName)
preferences.editorSyncScrolling = true
preferences.htmlStyleName = "GitHub2"
- preferences.markdownEngine = engine
defer {
preferences.editorSyncScrolling = saved.0
preferences.htmlStyleName = saved.1
- preferences.markdownEngine = saved.2
}
try await body()
}
- /// An anchor in the middle of the document for source lines (there are
- /// many, and early ones sit in the first screen, where the focus
- /// tapers), or hoedown's anchor `hoedown`.
- func middleIndex(_ controller: DocumentController, hoedown: Int) -> Int {
- if case .line = controller.previewMetrics.anchors.first?.kind {
- return controller.previewMetrics.anchors.count / 2
- }
- return hoedown
+ /// An anchor in the middle of the document (there are many, and early
+ /// ones sit in the first screen, where the focus tapers).
+ func middleIndex(_ controller: DocumentController) -> Int {
+ controller.previewMetrics.anchors.count / 2
}
- /// The editor and preview positions of the preview's `index`th anchor:
- /// by index for hoedown's anchors, by line for source lines.
+ /// The editor and preview positions of the preview's `index`th anchor's
+ /// line.
func anchorPair(_ controller: DocumentController, _ index: Int) -> (editor: CGFloat, preview: CGFloat)? {
let preview = controller.previewMetrics.anchors
- guard index < preview.count else { return nil }
- if case .line = preview[index].kind {
- guard let editor = controller.editorAnchors.first(where: { $0.kind == preview[index].kind })
- else { return nil }
- return (editor.position, preview[index].position)
- }
- guard index < controller.editorAnchors.count else { return nil }
- return (controller.editorAnchors[index].position, preview[index].position)
+ guard index < preview.count,
+ let editor = controller.editorAnchors.first(where: { $0.line == preview[index].line })
+ else { return nil }
+ return (editor.position, preview[index].position)
}
- /// With the new engine, both panes anchor on source lines: every block
- /// the preview marks, at its top, and the same line's top in the editor.
+ /// Both panes anchor on source lines: every block the preview marks, at
+ /// its top, and the same line's top in the editor.
@Test func sourceLinesAlignInBothDirections() async throws {
- let preferences = Preferences.shared
- let savedEngine = preferences.markdownEngine
- preferences.markdownEngine = .cmarkGfm
- defer { preferences.markdownEngine = savedEngine }
try await withSyncScrolling {
let (controller, window) = makeController()
defer { controller.tearDown(); window.close() }
#expect(await waitUntil { controller.previewMetrics.anchors.count > 10 })
- let previewLines = controller.previewMetrics.anchors.compactMap {
- if case .line(let n) = $0.kind { n } else { nil }
- }
- #expect(previewLines.count == controller.previewMetrics.anchors.count)
- let editorLines = Set(controller.editorAnchors.compactMap {
- if case .line(let n) = $0.kind { n } else { nil }
- })
+ let previewLines = controller.previewMetrics.anchors.map(\.line)
+ let editorLines = Set(controller.editorAnchors.map(\.line))
#expect(editorLines == Set(previewLines))
// A line in the middle: editor top at the center → preview top at center.
let index = previewLines.count / 2
let line = previewLines[index]
- let editorY = try #require(controller.editorAnchors.first { $0.kind == .line(line) }).position
+ let editorY = try #require(controller.editorAnchors.first { $0.line == line }).position
let previewY = controller.previewMetrics.anchors[index].position
let editorHeight = controller.editorScrollView.contentView.bounds.height
let previewHeight = controller.previewMetrics.visibleHeight
@@ -286,7 +203,7 @@ extension LiveDocumentTests {
let index2 = previewLines.count / 3
let line2 = previewLines[index2]
let previewY2 = controller.previewMetrics.anchors[index2].position
- let editorY2 = try #require(controller.editorAnchors.first { $0.kind == .line(line2) }).position
+ let editorY2 = try #require(controller.editorAnchors.first { $0.line == line2 }).position
let target = previewY2 - previewHeight / 2
_ = try await controller.preview.webView.evaluateJavaScript("window.scrollTo(0, \(target)); 0")
controller.preview.pageDidScroll(to: target)
@@ -294,15 +211,11 @@ extension LiveDocumentTests {
}
}
- /// Phase 4 check: help.md (with its fence-in-code-span case) and the
+ /// help.md (with its fence-in-code-span case) and the
/// image-heavy corpus document stay aligned with unequal pane widths,
/// in both directions, at several points.
@Test(arguments: ["help.md", "19-images.md"])
func documentsStayAligned(_ name: String) async throws {
- let preferences = Preferences.shared
- let savedEngine = preferences.markdownEngine
- preferences.markdownEngine = .cmarkGfm
- defer { preferences.markdownEngine = savedEngine }
try await withSyncScrolling {
let document = try #require(try Corpus.all().first { $0.name == name })
let controller = DocumentController(
@@ -336,8 +249,8 @@ extension LiveDocumentTests {
let clipView = controller.editorScrollView.contentView
for fraction in [0.3, 0.5, 0.7] {
let index = Int(Double(anchors.count) * fraction)
- guard case .line(let line) = anchors[index].kind,
- let editorY = controller.editorAnchors.first(where: { $0.kind == .line(line) })?.position
+ let line = anchors[index].line
+ guard let editorY = controller.editorAnchors.first(where: { $0.line == line })?.position
else { Issue.record("no line anchor at \(fraction)"); continue }
let previewY = anchors[index].position
clipView.scroll(to: NSPoint(x: 0, y: editorY - editorHeight / 2))
@@ -366,60 +279,9 @@ extension LiveDocumentTests {
}
}
- @Test func anchorsMatchAndAlignAtCenter() async throws {
- try await withSyncScrolling(engine: .hoedown) {
- let (controller, window) = makeController()
- defer { controller.tearDown(); window.close() }
-
- let ok1 = await waitUntil { controller.previewMetrics.anchors.count > 0 }
-
-
- #expect(ok1)
- let kinds: (ScrollAnchorKind) -> String = { $0 == .header ? "h" : "i" }
- let editorKinds = controller.editorAnchors.map { kinds($0.kind) }
- let previewKinds = controller.previewMetrics.anchors.map { kinds($0.kind) }
- #expect(editorKinds == ["h", "i", "i", "i", "h", "h", "i", "h", "i", "h"])
- #expect(previewKinds == editorKinds)
-
- // Put an image in the middle of the editor (away from the first
- // and last screens, where alignment tapers to the edges).
- let editorAnchor = controller.editorAnchors[6].position
- let previewAnchor = controller.previewMetrics.anchors[6].position
- let editorHeight = controller.editorScrollView.contentView.bounds.height
- let previewHeight = controller.previewMetrics.visibleHeight
- let clipView = controller.editorScrollView.contentView
- clipView.scroll(to: NSPoint(x: 0, y: editorAnchor - editorHeight / 2))
- controller.editorScrollView.reflectScrolledClipView(clipView)
-
- // The preview follows, with the same image in its middle.
- let expected = previewAnchor - previewHeight / 2
- let ok2 = await waitUntil {
- abs(await previewScrollY(controller) - expected) < 2
- }
-
- #expect(ok2)
-
- // Scrolling the preview moves the editor the same way.
- try await Task.sleep(for: .milliseconds(400))
- let previewAnchor2 = controller.previewMetrics.anchors[4].position
- let editorAnchor2 = controller.editorAnchors[4].position
- let previewY = previewAnchor2 - previewHeight / 2
- _ = try await controller.preview.webView.evaluateJavaScript(
- "window.scrollTo(0, \(previewY)); 0")
- // The page reports scrolling from animation frames, which WebKit
- // doesn't run in test windows that aren't on screen.
- controller.preview.pageDidScroll(to: previewY)
- let ok3 = await waitUntil {
- abs(clipView.bounds.minY - (editorAnchor2 - editorHeight / 2)) < 2
- }
-
- #expect(ok3)
- }
- }
-
- @Test(arguments: MarkdownEngine.allCases)
- func resizingKeepsAlignment(_ engine: MarkdownEngine) async throws {
- try await withSyncScrolling(engine: engine) {
+ @Test
+ func resizingKeepsAlignment() async throws {
+ try await withSyncScrolling {
let (controller, window) = makeController()
defer { controller.tearDown(); window.close() }
let ok4 = await waitUntil { controller.previewMetrics.anchors.count > 0 }
@@ -444,7 +306,7 @@ extension LiveDocumentTests {
let editorHeight = controller.editorScrollView.contentView.bounds.height
let previewHeight = controller.previewMetrics.visibleHeight
let clipView = controller.editorScrollView.contentView
- let pair = try #require(anchorPair(controller, middleIndex(controller, hoedown: 4)))
+ let pair = try #require(anchorPair(controller, middleIndex(controller)))
clipView.scroll(to: NSPoint(x: 0, y: pair.editor - editorHeight / 2))
controller.editorScrollView.reflectScrolledClipView(clipView)
let expected = pair.preview - previewHeight / 2
@@ -468,15 +330,15 @@ extension LiveDocumentTests {
#expect(result as? String == "ok")
}
- @Test(arguments: MarkdownEngine.allCases)
- func rerenderKeepsPositionWhenPreviewLeads(_ engine: MarkdownEngine) async throws {
- try await withSyncScrolling(engine: engine) {
+ @Test
+ func rerenderKeepsPositionWhenPreviewLeads() async throws {
+ try await withSyncScrolling {
let (controller, window) = makeController()
defer { controller.tearDown(); window.close() }
let ok1 = await waitUntil { controller.previewMetrics.anchors.count > 0 }
#expect(ok1)
let previewHeight = controller.previewMetrics.visibleHeight
- let previewY = controller.previewMetrics.anchors[middleIndex(controller, hoedown: 6)].position
+ let previewY = controller.previewMetrics.anchors[middleIndex(controller)].position
- previewHeight / 2
// Past the window in which scrolling is attributed to the sync.
try await Task.sleep(for: .milliseconds(400))
@@ -497,25 +359,6 @@ extension LiveDocumentTests {
}
}
- /// The bundled help, a long document that exercises most of Markdown.
- /// hoedown's header and image anchors over the bundled help (the new
- /// engine's lines are checked by documentsStayAligned).
- @Test func helpDocumentAnchorsMatch() async throws {
- try await withSyncScrolling(engine: .hoedown) {
- let url = try #require(MPPaths.resourceBundle.url(forResource: "help",
- withExtension: "md"))
- let (controller, window) = makeController(try String(contentsOf: url, encoding: .utf8))
- defer { controller.tearDown(); window.close() }
- let ok = await waitUntil { controller.previewMetrics.anchors.count > 0 }
- #expect(ok)
- // Without sync scrolling, metrics aren't fetched; fetch them here.
- let preview = await controller.preview.fetchMetrics().anchors.map(\.kind)
- controller.updateEditorAnchors()
- #expect(controller.editorAnchors.count > 30)
- #expect(controller.editorAnchors.map(\.kind) == preview)
- }
- }
-
// MARK: - Smooth scrolling
func evaluate(_ controller: DocumentController, _ script: String) async -> Any? {
diff --git a/Tests/MacDownKitTests/ThemeStyleParserTests.swift b/Tests/MacDownKitTests/ThemeStyleParserTests.swift
index 5014b8e..4d6acaf 100644
--- a/Tests/MacDownKitTests/ThemeStyleParserTests.swift
+++ b/Tests/MacDownKitTests/ThemeStyleParserTests.swift
@@ -3,71 +3,14 @@
// MacDownKitTests
//
// The Swift theme parser must read every stylesheet exactly as
-// pmh_styleparser.c does, errors included.
+// pmh_styleparser.c did, errors included, checked against the C parser's
+// recorded results (EngineSnapshots).
//
-import CPegMarkdown
import Foundation
import Testing
@testable import MacDownKit
-/// The C parser's result, in ThemeStyle's shape.
-private func cStyle(_ stylesheet: String) -> ThemeStyle {
- final class Box { var errors: [ThemeStyle.ParseError] = [] }
- let box = Box()
- let callback: @convention(c) (UnsafeMutablePointer?, Int32, UnsafeMutableRawPointer?) -> Void = {
- message, line, context in
- let box = Unmanaged.fromOpaque(context!).takeUnretainedValue()
- box.errors.append(.init(line: Int(line), message: String(cString: message!)))
- }
- let unmanaged = Unmanaged.passRetained(box)
- defer { unmanaged.release() }
- let collection = stylesheet.withCString {
- pmh_parse_styles(UnsafeMutablePointer(mutating: $0), callback, unmanaged.toOpaque())
- }!
- defer { pmh_free_style_collection(collection) }
-
- func attributes(_ list: UnsafeMutablePointer?) -> [ThemeStyle.Attribute] {
- var result: [ThemeStyle.Attribute] = []
- var cursor = list
- while let attribute = cursor {
- let a = attribute.pointee, v = a.value.pointee
- func color() -> ThemeStyle.Color {
- let c = v.argb_color.pointee
- return .init(red: Int(c.red), green: Int(c.green), blue: Int(c.blue), alpha: Int(c.alpha))
- }
- let value: ThemeStyle.Value = switch a.type {
- case pmh_attr_type_foreground_color: .foregroundColor(color())
- case pmh_attr_type_background_color: .backgroundColor(color())
- case pmh_attr_type_caret_color: .caretColor(color())
- case pmh_attr_type_font_size_pt:
- .fontSize(points: Int(v.font_size.pointee.size_pt),
- relative: v.font_size.pointee.is_relative)
- case pmh_attr_type_font_family: .fontFamily(String(cString: v.font_family))
- case pmh_attr_type_font_style:
- .fontStyle(italic: v.font_styles.pointee.italic, bold: v.font_styles.pointee.bold,
- underlined: v.font_styles.pointee.underlined)
- default: .other(String(cString: v.string))
- }
- result.append(.init(name: String(cString: a.name), value: value))
- cursor = a.next
- }
- return result
- }
-
- var style = ThemeStyle()
- style.editor = attributes(collection.pointee.editor_styles)
- style.currentLine = attributes(collection.pointee.editor_current_line_styles)
- style.selection = attributes(collection.pointee.editor_selection_styles)
- for i in 0.. ThemeStyle {
func matchesTheCParser(_ url: URL) throws {
let stylesheet = try String(contentsOf: url, encoding: .utf8)
let swift = ThemeStyle(parsing: stylesheet)
- #expect(swift == cStyle(stylesheet))
+ #expect(themeDescription(swift) == (try EngineSnapshots.value("theme/" + url.lastPathComponent)))
#expect(swift.errors.isEmpty)
#expect(!swift.elements.isEmpty && !swift.editor.isEmpty)
}
/// Errors and the C parser's quirks, with CRLF line endings and a BOM.
- @Test func malformedStylesheet() {
- let stylesheet = "\u{FEFF}" + """
+ static let malformed = "\u{FEFF}" + """
# A comment before any rule
editor
@@ -124,9 +66,11 @@ private func cStyle(_ stylesheet: String) -> ThemeStyle {
foreground-color: -fffff
background-color: 80ff0000
""".replacingOccurrences(of: "\n", with: "\r\n")
+
+ @Test func malformedStylesheet() throws {
+ let stylesheet = Self.malformed
let swift = ThemeStyle(parsing: stylesheet)
- let c = cStyle(stylesheet)
- #expect(swift == c)
+ #expect(themeDescription(swift) == (try EngineSnapshots.value("theme/malformed")))
#expect(swift.errors.count >= 7, "\(swift.errors)")
// The later editor rule replaces the first.
#expect(swift.editor == [.init(name: "foreground", value: .foregroundColor(
@@ -135,15 +79,15 @@ private func cStyle(_ stylesheet: String) -> ThemeStyle {
#expect(!swift.elements.contains { $0.element == "EMPH" })
}
- /// The highlighter uses an element's index as its PEG element type.
+ /// Element names follow PEG Markdown Highlight's element types
+ /// (pmh_element_type), then the types PEG didn't have.
@Test func elementNamesFollowThePEGOrder() {
- for (index, name) in ThemeStyle.elementNames.prefix(Int(pmh_NUM_LANG_TYPES)).enumerated() {
- let type = name.withCString { pmh_element_type_from_name(UnsafeMutablePointer(mutating: $0)) }
- #expect(Int(type.rawValue) == index, "\(name)")
- }
- // Then the types PEG didn't have.
- #expect(ThemeStyle.elementNames.dropFirst(Int(pmh_NUM_LANG_TYPES))
- == ["MATH", "HIGHLIGHT", "SUPERSCRIPT"])
+ #expect(ThemeStyle.elementNames == [
+ "LINK", "AUTO_LINK_URL", "AUTO_LINK_EMAIL", "IMAGE", "CODE", "HTML", "HTML_ENTITY",
+ "EMPH", "STRONG", "LIST_BULLET", "LIST_ENUMERATOR", "COMMENT", "H1", "H2", "H3", "H4",
+ "H5", "H6", "BLOCKQUOTE", "VERBATIM", "HTMLBLOCK", "HRULE", "REFERENCE", "NOTE",
+ "MATH", "HIGHLIGHT", "SUPERSCRIPT",
+ ])
}
@Test func errorMessagesReadLikeTheHighlighters() {
diff --git a/Tools/import_localizations.py b/Tools/import_localizations.py
index 1253926..c5af7cf 100644
--- a/Tools/import_localizations.py
+++ b/Tools/import_localizations.py
@@ -186,6 +186,67 @@ def add_port_translations(strings):
strings[key] = entry
+
+# The port's changes to the original's credits (RTF): its contributor, and
+# the libraries it uses. hoedown and Hoextdown are gone (cmark-gfm parses);
+# PEG Markdown Highlight's highlighter and theme parser live on as Swift
+# ports (see Licenses/README.md).
+CREDITS_CONTRIBUTOR_AFTER = (
+ '{\\field{\\*\\fldinst{HYPERLINK "https://www.loopingz.com/"}}{\\fldrslt \\cf0 Remi Cattiau}}\\\n')
+CREDITS_CONTRIBUTOR = (
+ '\\pard\\pardeftab720\\partightenfactor0\n'
+ '{\\field{\\*\\fldinst{HYPERLINK "https://github.com/levous"}}{\\fldrslt \\cf0 LeVous (Rusty Zarse)}}\\\n')
+CREDITS_HOEDOWN = (
+ '{\\field{\\*\\fldinst{HYPERLINK "https://github.com/hoedown/hoedown"}}{\\fldrslt \n'
+ '\\b\\fs28 \\cf0 Hoedown}}')
+CREDITS_AFTER_HOEDOWN = '{\\field{\\*\\fldinst{HYPERLINK "http://pyyaml.org/wiki/LibYAML"}}'
+CREDITS_CMARK = (
+ '{\\field{\\*\\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark"}}{\\fldrslt \n'
+ '\\b\\fs28 \\cf0 cmark-gfm}}\\\n'
+ "Copyright \\'a9 2014, John MacFarlane\\\n"
+ 'All rights reserved.\\\n'
+ '\\\n'
+ 'Redistribution and use in source and binary forms, with or without modification, are permitted '
+ 'provided that the following conditions are met:\\\n'
+ ' * Redistributions of source code must retain the above copyright notice, this list of '
+ 'conditions and the following disclaimer.\\\n'
+ ' * Redistributions in binary form must reproduce the above copyright notice, this list of '
+ 'conditions and the following disclaimer in the documentation and/or other materials provided '
+ 'with the distribution.\\\n'
+ '\\\n'
+ 'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR '
+ 'IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND '
+ 'FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR '
+ 'CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL '
+ 'DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, '
+ 'DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER '
+ 'IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT '
+ 'OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\\\n'
+ '\\\n'
+ 'cmark-gfm includes code from other projects under their own notices: see '
+ '{\\field{\\*\\fldinst{HYPERLINK "https://github.com/swiftlang/swift-cmark/blob/main/COPYING"}}'
+ '{\\fldrslt https://github.com/swiftlang/swift-cmark/blob/main/COPYING}}.\\\n'
+ '\\\n')
+CREDITS_PEG_AFTER = ('PEG-Markdown-Highlight is licensed under the same terms as peg-markdown '
+ '(see below for more information).\\\n')
+CREDITS_PEG_NOTE = ("\\\nMacDown's editor highlighter and theme parser are Swift ports of PEG Markdown "
+ "Highlight's code, so that editor themes keep working; the library itself is no "
+ "longer used.\\\n")
+
+
+def port_credits(text):
+ """The original's Credits.rtf with the port's changes."""
+ for marker in (CREDITS_CONTRIBUTOR_AFTER, CREDITS_HOEDOWN, CREDITS_AFTER_HOEDOWN,
+ CREDITS_PEG_AFTER):
+ if text.count(marker) != 1:
+ sys.exit(f"Credits.rtf changed upstream; update port_credits: {marker[:60]!r}")
+ text = text.replace(CREDITS_CONTRIBUTOR_AFTER, CREDITS_CONTRIBUTOR_AFTER + CREDITS_CONTRIBUTOR)
+ start = text.index(CREDITS_HOEDOWN)
+ # Up to the paragraph reset before LibYAML: hoedown, then Hoextdown.
+ end = text.rindex("\\pard", start, text.index(CREDITS_AFTER_HOEDOWN))
+ text = text[:start] + CREDITS_CMARK + text[end:]
+ return text.replace(CREDITS_PEG_AFTER, CREDITS_PEG_AFTER + CREDITS_PEG_NOTE)
+
def main():
if len(sys.argv) != 2:
sys.exit(__doc__)
@@ -220,13 +281,14 @@ def main():
CATALOG.write_text(json.dumps(catalog, ensure_ascii=False, indent=2,
sort_keys=True) + "\n")
- # Credits shown in the About panel.
+ # Credits shown in the About panel, with the port's changes.
if CREDITS_DIR.exists():
shutil.rmtree(CREDITS_DIR)
for credits in sorted(localization_dir.glob("*.lproj/Credits.rtf")):
target = CREDITS_DIR / credits.parent.name / "Credits.rtf"
target.parent.mkdir(parents=True, exist_ok=True)
- shutil.copy(credits, target)
+ text = credits.read_text(encoding="latin-1")
+ target.write_text(port_credits(text), encoding="latin-1")
print(f"{len(keys)} strings written to {CATALOG.relative_to(ROOT)}")
for language in sorted(translations):
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index c7b9c5b..d1a803d 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -27,8 +27,6 @@ changes, and implements the formatting, saving and export actions.
| Path | Contents |
|------|----------|
| `Package.swift` | Swift package with everything except the app entry point |
-| `Sources/CHoedown` | hoedown 3.0.7 plus MacDown's renderer patches (`hoedown_html_patch.c`) — being replaced, see [Markdown engine](#markdown-engine) |
-| `Sources/CPegMarkdown` | PEG Markdown Highlight parser (C) — being replaced |
| `Sources/MacDownShared` | Constants shared by the app and the shell utility |
| `Sources/MacDownKit/Document` | Parsing, page assembly, the document model and per-window controller |
| `Sources/MacDownKit/Document/Model` | The cmark-gfm document model: `CMarkTree`, `LineIndex`, `ProtectedSource`, `MarkdownDocumentModel`, `HighlightMapper` |
@@ -50,14 +48,14 @@ changes, and implements the formatting, saving and export actions.
1. `DocumentController` observes editor text and preference changes.
2. `Preferences.renderSettings` turns preferences into `RenderSettings`:
- `ParseSettings` (Markdown options, and the hidden `markdownEngine`) plus
- `PageSettings` (styles, Prism, MathJax, Mermaid, Graphviz, template).
-3. `Renderer.parse` parses in a detached task; a generation counter discards
- results of superseded parses. Today the HTML comes from `MarkdownParser`
- (hoedown, bridged through C callbacks), which also renders front matter
+ `ParseSettings` (the opt-in Markdown options; standard Markdown is always
+ on) plus `PageSettings` (styles, Prism, MathJax, Mermaid, Graphviz,
+ template).
+3. `Renderer.parse` builds the `MarkdownDocumentModel` in a detached task
+ (see [Markdown engine](#markdown-engine)); a generation counter discards
+ results of superseded parses. The model's HTML renders front matter
(Yams) as a table, substitutes `[TOC]`, and collects code block languages
- for Prism. With `markdownEngine = cmarkGfm` the same task also builds
- the `MarkdownDocumentModel` (see [Markdown engine](#markdown-engine)).
+ for Prism; its highlight spans go to the editor.
4. `PageBuilder` (pure functions) builds the full page with `HTMLTemplate` (a
minimal Handlebars subset) and `Asset` (linked or embedded CSS/JS). The
preview, HTML export and PDF export use the same builder with different
@@ -72,10 +70,11 @@ changes, and implements the formatting, saving and export actions.
(`PreviewURL`) and served by `LocalFileSchemeHandler`. Relative links and
images resolve against the document's location as in the original.
-**Scroll sync.** Editor and preview are aligned by anchors (headers, images,
-code blocks) found in the source and reported by the page, mapped through
-`ScrollMap`, so unequal pane widths and tall images stay aligned in both
-directions. The migration moves this to source-line anchors from the parse.
+**Scroll sync.** Editor and preview are aligned by anchors: the preview marks
+every block with its source line (`data-source-line`), the page reports where
+each one is, and the editor finds the same lines in its layout. `ScrollMap`
+pairs them by line, so unequal pane widths and tall images stay aligned in
+both directions.
The page follows the editor smoothly: `PreviewController.scroll(to:animated:)`
hands the offset to `MacDownScroll`, a script in the page that eases towards
the latest target each animation frame. While the editor scrolls, each new
@@ -93,13 +92,13 @@ helpers (list and quote continuation, auto-pairing, toggling markup,
indenting) are in `NSTextView+Autocomplete.swift`, called from
`DocumentController`'s text view delegate methods.
-`MarkdownHighlighter` styles text from spans keyed by PEG
-Markdown Highlight's element types (`H1`, `EMPH`, `CODE`, …) so the original
-`.style` themes apply unchanged. Themes are parsed by `ThemeStyle`, a Swift
-port of the C style parser. Spans come either from the PEG C parser (today's
-default) or, with the new engine, from `HighlightMapper` via the shared
-document model, which adds `NOTE` (footnotes), `MATH`, `HIGHLIGHT` and
-`SUPERSCRIPT`.
+`MarkdownHighlighter` styles text from spans keyed by `HighlightElement`, PEG
+Markdown Highlight's element types (`H1`, `EMPH`, `CODE`, …), so the original
+`.style` themes apply unchanged. Themes are parsed by `ThemeStyle`. Both are
+Swift ports of PEG Markdown Highlight's code (its license is kept in
+`Licenses/`); the C library is no longer used. The spans come from
+`HighlightMapper` via the shared document model, which adds `NOTE`
+(footnotes), `MATH`, `HIGHLIGHT` and `SUPERSCRIPT`.
New spans are applied straight away to the visible text and a screen above
and below it. When the editor scrolls, the highlighter styles whatever of
@@ -171,17 +170,14 @@ original's classes onto this code are in [MACDOWN-PORT.md](MACDOWN-PORT.md).
The port started on the original's engines: patched hoedown for HTML and PEG
Markdown Highlight for the editor, two C parsers that parse every edit twice
-and disagree in places. They are being replaced by **cmark-gfm**, used
-directly, so one parse feeds the preview, export, highlighting and scroll
-sync. The plan, requirements and progress are in
-[docs/intents/swift-markdown-migration.md](intents/swift-markdown-migration.md),
-[docs/prd/swift-markdown-migration-prd.md](prd/swift-markdown-migration-prd.md)
-and `.ralph/fix_plan.md`.
-
-cmark-gfm is the default. For one release the original engines stay
-selectable through a hidden setting
-(`defaults write io.github.levous.macdown-swift markdownEngine hoedown`);
-then hoedown and PEG Markdown Highlight are removed.
+and disagree in places. They were replaced by **cmark-gfm** (swift-cmark),
+used directly, so one parse feeds the preview, export, highlighting and
+scroll sync, and removed on 2026-10-22. The plan and requirements are in
+[docs/intents/swift-markdown-migration.md](intents/swift-markdown-migration.md)
+and [docs/prd/swift-markdown-migration-prd.md](prd/swift-markdown-migration-prd.md).
+The renderer still writes hoedown's markup where CommonMark agrees, checked
+against hoedown's output recorded before it was removed
+(`Tests/MacDownKitTests/Resources/engine-snapshots.json`).
The new model (`Sources/MacDownKit/Document/Model`):
@@ -194,7 +190,7 @@ The new model (`Sources/MacDownKit/Document/Model`):
- `LineIndex` converts cmark's UTF-8 line/column positions into the UTF-16
ranges the editor uses.
- `MarkdownDocumentModel` is built inside the parse task and keeps only
- `Sendable` results (blocks, highlight spans; later the HTML); the C tree
+ `Sendable` results (blocks, highlight spans, the HTML); the C tree
never leaves the task.
## Decisions
@@ -202,6 +198,15 @@ The new model (`Sources/MacDownKit/Document/Model`):
Dated, newest first. The migration intent has the full list for the engine
work.
+**hoedown and PEG Markdown Highlight are removed (2026-10-22).** cmark-gfm
+has been the only engine in practice since 2026-10-09; the hidden
+`markdownEngine` setting, `MarkdownParser`, the hoedown scroll anchors, the
+highlighter's PEG parse and both C targets are gone, so the package has no C
+code. Before the removal, the outputs the port has to match (hoedown's HTML
+and escaping, PEG's theme parsing) were recorded as test fixtures. The PEG
+license stays because the highlighter and theme parser are ports of its code
+(`Licenses/README.md`); hoedown's goes.
+
**Paste conversion parses HTML with `XMLDocument` (2026-10-21).** Pasting
formatted text converts the clipboard's HTML (or RTF, via AppKit's HTML
export) to Markdown. Foundation's `XMLDocument` with `.documentTidyHTML`
@@ -299,27 +304,21 @@ The bundled help (`Resources/help.md`) has a live example of every feature
and setting, and `HelpDocumentTests` renders it, checks its highlighting and
loads it in the real preview.
-The engine migration is checked by two harnesses that run with every
-`swift test` over the corpus (19 generated documents, `help.md`, and a
-generated 10,000-line document):
+The renderer is checked against hoedown's output and PEG Markdown
+Highlight's style parser, recorded before they were removed
+(`Resources/engine-snapshots.json`, `EngineSnapshots`). The corpus (19
+generated documents, `help.md`, and a generated 10,000-line document) is
+rendered by `CorpusTests` and round-tripped by `PasteRoundTripTests` below.
+The two engine-comparison harnesses that guided the migration were retired
+with hoedown; the differences they found are summarized in
+[MACDOWN-PORT.md](MACDOWN-PORT.md).
```sh
-swift test --filter HTMLDiffHarnessTests # preview HTML, 9 settings
-swift test --filter HighlightDiffHarnessTests # editor highlight spans
-
-# Write html-diff.md and highlight-diff.md for review:
-MACDOWN_DIFF_REPORT=/tmp/diff swift test --filter "DiffHarnessTests"
-
-# Highlighting speed (NFR-1), on demand:
+# Highlighting speed against PEG Markdown Highlight's recorded time, on demand:
MACDOWN_BENCHMARK=1 swift test -c release -Xswiftc -enable-testing \
--filter HighlightBenchmarkTests
```
-Intended HTML differences are listed once in
-`Tests/MacDownKitTests/Resources/expected-html-diffs.json`; reviewed
-highlighting differences are in `HighlightDiffHarnessTests.reviewed`. There is
-no CI; run them before merging engine work.
-
Pasting is checked the same way: `PasteRoundTripTests` renders each corpus
document, converts the HTML back with `HTMLToMarkdown` and renders that
again; `Resources/reviewed-paste-diffs.json` lists the reviewed differences
@@ -358,14 +357,6 @@ on each entry), so a run after Xcode has saved the catalog rewrites every
line. If it has, commit only the new entries (2026-10-21: the paste strings
were added that way).
-### Regenerating the highlighter parser
-
-`Sources/CPegMarkdown/pmh_parser.c` is generated from PEG Markdown Highlight's
-`pmh_grammar.leg` with `greg`; don't edit it by hand. To regenerate it, run
-`make` in the original repository's `Dependency/peg-markdown-highlight`
-directory and copy `pmh_parser.c` here. (It goes away with the engine
-migration.)
-
### Releasing
`Tools/release.sh ` bumps the version (in `project.yml` and
diff --git a/docs/MACDOWN-PORT.md b/docs/MACDOWN-PORT.md
index 09c0220..4131918 100644
--- a/docs/MACDOWN-PORT.md
+++ b/docs/MACDOWN-PORT.md
@@ -62,8 +62,9 @@ fresh, and the original's plist can be copied over to migrate.
- **Markdown engine:** the original's two parsers are replaced by one,
[cmark-gfm](https://github.com/swiftlang/swift-cmark), which follows
CommonMark and GitHub Flavored Markdown; where the results differ is listed
- below. For one release the original's engine can be turned back on with
- `defaults write io.github.levous.macdown-swift markdownEngine hoedown`.
+ below. The original's engines (hoedown, PEG Markdown Highlight) were
+ removed on 2026-10-22, after a release in which hoedown could still be
+ turned back on with a hidden setting.
- **Editor highlighting** styles text as it scrolls into view, and the
rest of the document in the background, instead of restyling what's
visible after scrolling (every 100 ms in the original).
@@ -73,7 +74,7 @@ fresh, and the original's plist can be copied over to migrate.
Rendering ▸ "Enable checkbox toggling" (on by default) changes `[ ]` to
`[x]` (or back) in the Markdown, as an undoable edit. The original's
preview checkboxes were always disabled, as they still are with the
- setting off and with the hoedown engine.
+ setting off.
- **Base font size**: Settings ▸ Rendering ▸ "Base font size" (0, off, by
default) sets the root `html` font size in the preview and exports, so
stylesheets in relative units scale with it. The bundled styles size
@@ -110,8 +111,9 @@ The port renders with cmark-gfm instead of the original's hoedown (see
[ARCHITECTURE.md](ARCHITECTURE.md), "Markdown engine"). It follows
CommonMark and GitHub Flavored Markdown, so some documents render
differently from the original. These are the differences found on the test
-corpus; each is reviewed in
-`Tests/MacDownKitTests/Resources/reviewed-html-diffs.json`:
+corpus, each reviewed while both engines were in the app (the reviewed list
+is in the git history, `Tests/MacDownKitTests/Resources/reviewed-html-diffs.json`,
+removed with hoedown on 2026-10-22):
- **Emphasis:** underscores don't emphasize inside words (`snake_case_name`
stays as typed), `***text***` nests as emphasis inside strong, and nested
@@ -145,12 +147,14 @@ corpus; each is reviewed in
| Objective-C | Swift |
|-------------|-------|
| `MPDocument` (NSDocument + xib) | `MarkdownDocument` (`ReferenceFileDocument`), `DocumentController`, `DocumentView` |
-| `MPRenderer` | `MarkdownDocumentModel` and `HTMLRenderer` (cmark-gfm), `PageBuilder`, `Renderer`; `MarkdownParser` (the hoedown bridge, for one release) |
+| `MPRenderer` | `MarkdownDocumentModel` and `HTMLRenderer` (cmark-gfm), `PageBuilder`, `Renderer` |
| `MPAsset`, handlebars-objc | `Asset`, `HTMLTemplate` (minimal Handlebars subset) |
| `MPPreferences` (PAPreferences) | `Preferences` (`@Observable`, same user defaults keys) |
| MASPreferences panes (xibs) | `SettingsView` (SwiftUI `Settings` scene) |
| `MPEditorView`, `NSTextView+Autocomplete` | `EditorTextView`, `NSTextView+Autocomplete.swift` |
-| `HGMarkdownHighlighter` | `MarkdownHighlighter` |
+| `HGMarkdownHighlighter` | `MarkdownHighlighter` (a Swift port; spans from `HighlightMapper`) |
+| PEG Markdown Highlight's `pmh_styleparser.c` | `ThemeStyle` (`ThemeStyleParser.swift`, a Swift port) |
+| hoedown, PEG Markdown Highlight's parser | cmark-gfm (swift-cmark), through `MarkdownDocumentModel` |
| `MPDocumentSplitView` | `DocumentSplitView` |
| `MPToolbarController` | `DocumentToolbar` (customizable SwiftUI toolbar) |
| `MainMenu.xib` | `MacDownCommands` |
@@ -170,8 +174,8 @@ original's.
engine change, the port rendered with the original's patched hoedown, and its
HTML matched the original byte for byte. The new cmark-gfm renderer follows
CommonMark and GitHub Flavored Markdown instead; where its output differs from
-the original is reviewed with the diff harnesses and listed under
-"Differences from the original". (See [ARCHITECTURE.md](ARCHITECTURE.md),
+the original was reviewed with diff harnesses (removed with hoedown) and is
+listed under "Markdown differences with the new engine". (See [ARCHITECTURE.md](ARCHITECTURE.md),
"Markdown engine".)
**Removed rather than ported:** Sparkle updates (releases go through GitHub
diff --git a/docs/intents/swift-markdown-migration.md b/docs/intents/swift-markdown-migration.md
index c40a321..97ce9cc 100644
--- a/docs/intents/swift-markdown-migration.md
+++ b/docs/intents/swift-markdown-migration.md
@@ -1,6 +1,6 @@
# Migrate Markdown parsing to swift-markdown
-- **Status:** Accepted (decisions recorded below)
+- **Status:** Done (2026-10-22; decisions recorded below)
- **Date:** 2026-10-09
- **Scope:** Replace both Markdown parsers, hoedown (`Sources/CHoedown`, preview and export) and PEG Markdown Highlight (`Sources/CPegMarkdown`, editor highlighting), with Apple's [swift-markdown](https://github.com/swiftlang/swift-markdown) (built on cmark-gfm). Align Markdown formatting, settings and editing defaults with standard conventions (see "Principle").
@@ -261,6 +261,8 @@ Each phase from 0 on lands behind a hidden setting (`markdownEngine` = `hoedown`
10. **The parser is cmark-gfm, used directly** (decided 2026-10-09, after finding F8). swift-cmark's `cmark-gfm` and `cmark-gfm-extensions` products, pinned exactly, parse once per edit with source positions, footnotes (`CMARK_OPT_FOOTNOTES`), the `table`, `strikethrough` and `tasklist` extensions always, `autolink` only with the Autolink setting on, and `CMARK_OPT_SMART` only with Smart punctuation on. Our model, highlighter and renderer walk cmark's C tree inside the parse task; swift-markdown's Swift tree is no longer used and the dependency goes. This resolves open questions 1 (footnotes are native) and 2 (cmark-gfm parses and walks 10,000 lines in 4.5 ms, against PEG's 20). Documents keep the "swift-markdown migration" name.
+11. **hoedown and PEG Markdown Highlight are removed now** (2026-10-22, at the owner's request), without the separate release with hoedown still selectable that Phase 5 planned. The hidden `markdownEngine` setting, `MarkdownParser`, `ScrollAnchors.scan`, the highlighter's PEG parse, `Sources/CHoedown`, `Sources/CPegMarkdown` and both diff harnesses are gone. hoedown's HTML and escaping, and PEG's theme parsing, were recorded as test fixtures first (`engine-snapshots.json`). `Licenses/peg-markdown-highlight.txt` stays: `MarkdownHighlighter` and `ThemeStyle` are Swift ports of its code. hoedown's and Hoextdown's licenses go. There's no swift-markdown dependency (Decision 10), so no license for it; `Licenses/cmark-gfm.txt` is swift-cmark's.
+
### Phase 0 findings
Answers from the spikes in `Tests/MacDownKitTests/cmarkGfmSpikeTests.swift` (swift-markdown 0.9.0, swift-cmark 0.9.0):