pandoc 3.11 → 3.14
raw patch · 609 files changed
This diff is very large; some files are shown as “too large to diff”. Download the raw patch for the complete diff.
Files
- AUTHORS.md +22/−0
- MANUAL.txt +164/−58
- README.md +4/−0
- benchmark/benchmark-pandoc.hs +70/−31
- benchmark/lalune.jpg binary
- benchmark/movie.jpg binary
- benchmark/sample.md +1597/−0
- changelog.md +1440/−60
- data/docx/word/styles.xml +0/−2
- data/templates/common.latex +21/−3
- data/templates/default.revealjs +6/−6
- data/templates/default.typst +12/−0
- data/templates/styles.html +56/−27
- pandoc.cabal +74/−47
- src/Text/Pandoc/App/CommandLineOptions.hs +5/−3
- src/Text/Pandoc/App/Input.hs +4/−2
- src/Text/Pandoc/App/Opt.hs +20/−17
- src/Text/Pandoc/App/OutputSettings.hs +8/−5
- src/Text/Pandoc/Chunks.hs +7/−17
- src/Text/Pandoc/Citeproc.hs +15/−11
- src/Text/Pandoc/Citeproc/BibTeX.hs +14/−17
- src/Text/Pandoc/Citeproc/Locator.hs +3/−3
- src/Text/Pandoc/Citeproc/MetaValue.hs +5/−5
- src/Text/Pandoc/Citeproc/Name.hs +11/−11
- src/Text/Pandoc/Class/IO.hs +6/−99
- src/Text/Pandoc/Class/IO/HTTP.hs +115/−0
- src/Text/Pandoc/Class/PandocMonad.hs +136/−37
- src/Text/Pandoc/Class/PandocPure.hs +24/−15
- src/Text/Pandoc/Data.hs +70/−68
- src/Text/Pandoc/Extensions.hs +3/−2
- src/Text/Pandoc/Filter.hs +0/−1
- src/Text/Pandoc/Format.hs +1/−0
- src/Text/Pandoc/Image.hs +1/−1
- src/Text/Pandoc/ImageSize.hs +314/−83
- src/Text/Pandoc/MIME.hs +1/−0
- src/Text/Pandoc/MediaBag.hs +37/−17
- src/Text/Pandoc/Options.hs +0/−1
- src/Text/Pandoc/PDF.hs +9/−8
- src/Text/Pandoc/Parsing.hs +0/−1
- src/Text/Pandoc/Parsing/Capabilities.hs +4/−1
- src/Text/Pandoc/Parsing/Future.hs +2/−5
- src/Text/Pandoc/Parsing/General.hs +47/−21
- src/Text/Pandoc/Parsing/GridTable.hs +5/−9
- src/Text/Pandoc/Parsing/Lists.hs +25/−4
- src/Text/Pandoc/Parsing/Math.hs +60/−57
- src/Text/Pandoc/Readers.hs +2/−0
- src/Text/Pandoc/Readers/AsciiDoc.hs +89/−75
- src/Text/Pandoc/Readers/CommonMark.hs +11/−10
- src/Text/Pandoc/Readers/Creole.hs +33/−7
- src/Text/Pandoc/Readers/Djot.hs +9/−7
- src/Text/Pandoc/Readers/DocBook.hs +1/−5
- src/Text/Pandoc/Readers/Docx.hs +161/−104
- src/Text/Pandoc/Readers/Docx/Combine.hs +29/−1
- src/Text/Pandoc/Readers/Docx/Lists.hs +9/−0
- src/Text/Pandoc/Readers/Docx/Parse.hs +84/−31
- src/Text/Pandoc/Readers/Docx/Parse/Styles.hs +62/−18
- src/Text/Pandoc/Readers/DokuWiki.hs +7/−5
- src/Text/Pandoc/Readers/HTML.hs +106/−49
- src/Text/Pandoc/Readers/HTML/Parsing.hs +39/−26
- src/Text/Pandoc/Readers/HTML/Table.hs +15/−6
- src/Text/Pandoc/Readers/HTML/Types.hs +6/−3
- src/Text/Pandoc/Readers/Jira.hs +2/−2
- src/Text/Pandoc/Readers/LaTeX.hs +235/−50
- src/Text/Pandoc/Readers/LaTeX/Citation.hs +4/−10
- src/Text/Pandoc/Readers/LaTeX/Inline.hs +2/−16
- src/Text/Pandoc/Readers/LaTeX/Macro.hs +276/−3
- src/Text/Pandoc/Readers/LaTeX/Parsing.hs +825/−94
- src/Text/Pandoc/Readers/LaTeX/Table.hs +6/−10
- src/Text/Pandoc/Readers/Man.hs +3/−2
- src/Text/Pandoc/Readers/Markdown.hs +83/−46
- src/Text/Pandoc/Readers/Mdoc.hs +11/−11
- src/Text/Pandoc/Readers/Mdoc/Lex.hs +4/−2
- src/Text/Pandoc/Readers/MediaWiki.hs +39/−14
- src/Text/Pandoc/Readers/Metadata.hs +29/−38
- src/Text/Pandoc/Readers/Muse.hs +30/−9
- src/Text/Pandoc/Readers/ODT.hs +46/−13
- src/Text/Pandoc/Readers/ODT/Arrows/State.hs +0/−153
- src/Text/Pandoc/Readers/ODT/Arrows/Utils.hs +0/−241
- src/Text/Pandoc/Readers/ODT/Base.hs +0/−26
- src/Text/Pandoc/Readers/ODT/ContentReader.hs +1206/−1050
- src/Text/Pandoc/Readers/ODT/Generic/Fallible.hs +0/−71
- src/Text/Pandoc/Readers/ODT/Generic/Utils.hs +2/−62
- src/Text/Pandoc/Readers/ODT/Generic/XMLConverter.hs +289/−585
- src/Text/Pandoc/Readers/ODT/StyleReader.hs +184/−154
- src/Text/Pandoc/Readers/OOXML/Charts.hs +171/−0
- src/Text/Pandoc/Readers/OOXML/Shared.hs +25/−0
- src/Text/Pandoc/Readers/Org/BlockStarts.hs +4/−3
- src/Text/Pandoc/Readers/Org/Blocks.hs +27/−20
- src/Text/Pandoc/Readers/Org/ExportSettings.hs +19/−3
- src/Text/Pandoc/Readers/Org/Inlines.hs +83/−22
- src/Text/Pandoc/Readers/Org/Meta.hs +10/−6
- src/Text/Pandoc/Readers/Org/ParserState.hs +14/−4
- src/Text/Pandoc/Readers/Org/Parsing.hs +6/−2
- src/Text/Pandoc/Readers/Org/Shared.hs +2/−1
- src/Text/Pandoc/Readers/Pod.hs +12/−12
- src/Text/Pandoc/Readers/Pptx/Parse.hs +52/−1
- src/Text/Pandoc/Readers/Pptx/Shapes.hs +356/−78
- src/Text/Pandoc/Readers/Pptx/Slides.hs +87/−7
- src/Text/Pandoc/Readers/Pptx/SmartArt.hs +7/−14
- src/Text/Pandoc/Readers/RST.hs +153/−67
- src/Text/Pandoc/Readers/RTF.hs +49/−12
- src/Text/Pandoc/Readers/Roff.hs +6/−5
- src/Text/Pandoc/Readers/Roff/Escape.hs +5/−5
- src/Text/Pandoc/Readers/TWiki.hs +8/−5
- src/Text/Pandoc/Readers/Textile.hs +78/−7
- src/Text/Pandoc/Readers/TikiWiki.hs +2/−2
- src/Text/Pandoc/Readers/Txt2Tags.hs +31/−8
- src/Text/Pandoc/Readers/Typst.hs +239/−164
- src/Text/Pandoc/Readers/Typst/Parsing.hs +2/−2
- src/Text/Pandoc/Readers/Vimwiki.hs +45/−11
- src/Text/Pandoc/Readers/XML.hs +55/−37
- src/Text/Pandoc/Readers/Xlsx/Cells.hs +1/−0
- src/Text/Pandoc/Readers/Xlsx/Parse.hs +238/−39
- src/Text/Pandoc/Readers/Xlsx/Sheets.hs +84/−12
- src/Text/Pandoc/SelfContained.hs +60/−37
- src/Text/Pandoc/Shared.hs +77/−17
- src/Text/Pandoc/Sources.hs +78/−0
- src/Text/Pandoc/TeX.hs +45/−1
- src/Text/Pandoc/Templates.hs +1/−0
- src/Text/Pandoc/Transforms.hs +7/−5
- src/Text/Pandoc/Translations.hs +21/−16
- src/Text/Pandoc/Translations/Types.hs +11/−5
- src/Text/Pandoc/URI.hs +1/−1
- src/Text/Pandoc/UTF8.hs +13/−5
- src/Text/Pandoc/Version.hs +0/−1
- src/Text/Pandoc/Writers.hs +2/−0
- src/Text/Pandoc/Writers/ANSI.hs +7/−4
- src/Text/Pandoc/Writers/AsciiDoc.hs +1/−8
- src/Text/Pandoc/Writers/BibTeX.hs +4/−10
- src/Text/Pandoc/Writers/ChunkedHTML.hs +4/−3
- src/Text/Pandoc/Writers/ConTeXt.hs +1/−8
- src/Text/Pandoc/Writers/Djot.hs +6/−12
- src/Text/Pandoc/Writers/DocBook.hs +15/−22
- src/Text/Pandoc/Writers/Docx.hs +18/−9
- src/Text/Pandoc/Writers/Docx/OpenXML.hs +292/−72
- src/Text/Pandoc/Writers/Docx/StyleMap.hs +8/−2
- src/Text/Pandoc/Writers/Docx/Table.hs +7/−2
- src/Text/Pandoc/Writers/Docx/Types.hs +21/−7
- src/Text/Pandoc/Writers/EPUB.hs +32/−33
- src/Text/Pandoc/Writers/FB2.hs +2/−2
- src/Text/Pandoc/Writers/HTML.hs +98/−43
- src/Text/Pandoc/Writers/Haddock.hs +23/−21
- src/Text/Pandoc/Writers/ICML.hs +1/−3
- src/Text/Pandoc/Writers/JATS.hs +36/−38
- src/Text/Pandoc/Writers/JATS/References.hs +3/−3
- src/Text/Pandoc/Writers/JATS/Table.hs +11/−9
- src/Text/Pandoc/Writers/Jira.hs +4/−3
- src/Text/Pandoc/Writers/LaTeX.hs +65/−66
- src/Text/Pandoc/Writers/LaTeX/Notes.hs +11/−17
- src/Text/Pandoc/Writers/LaTeX/Table.hs +53/−34
- src/Text/Pandoc/Writers/LaTeX/Types.hs +3/−3
- src/Text/Pandoc/Writers/LaTeX/Util.hs +2/−2
- src/Text/Pandoc/Writers/Man.hs +1/−8
- src/Text/Pandoc/Writers/Markdown.hs +53/−38
- src/Text/Pandoc/Writers/Markdown/Inline.hs +21/−9
- src/Text/Pandoc/Writers/MediaWiki.hs +1/−1
- src/Text/Pandoc/Writers/Ms.hs +4/−11
- src/Text/Pandoc/Writers/Muse.hs +1/−8
- src/Text/Pandoc/Writers/Native.hs +284/−9
- src/Text/Pandoc/Writers/ODT.hs +244/−16
- src/Text/Pandoc/Writers/OOXML.hs +2/−2
- src/Text/Pandoc/Writers/OPML.hs +3/−5
- src/Text/Pandoc/Writers/OpenDocument.hs +170/−137
- src/Text/Pandoc/Writers/Org.hs +91/−39
- src/Text/Pandoc/Writers/Powerpoint/Output.hs +55/−18
- src/Text/Pandoc/Writers/Powerpoint/Presentation.hs +13/−7
- src/Text/Pandoc/Writers/RST.hs +26/−17
- src/Text/Pandoc/Writers/Shared.hs +81/−45
- src/Text/Pandoc/Writers/TEI.hs +7/−15
- src/Text/Pandoc/Writers/Texinfo.hs +1/−8
- src/Text/Pandoc/Writers/Typst.hs +54/−32
- src/Text/Pandoc/Writers/XML.hs +161/−66
- src/Text/Pandoc/XML.hs +53/−19
- src/Text/Pandoc/XMLFormat.hs +89/−1
- test/Tests/Data.hs +198/−0
- test/Tests/ImageSize.hs +335/−0
- test/Tests/MediaBag.hs +35/−1
- test/Tests/Readers/Docx.hs +46/−2
- test/Tests/Readers/DokuWiki.hs +42/−1
- test/Tests/Readers/HTML.hs +42/−1
- test/Tests/Readers/LaTeX.hs +45/−0
- test/Tests/Readers/Markdown.hs +85/−1
- test/Tests/Readers/ODT.hs +112/−1
- test/Tests/Readers/Org/Directive.hs +12/−0
- test/Tests/Readers/Org/Inline.hs +8/−0
- test/Tests/Readers/Org/Inline/Note.hs +7/−0
- test/Tests/Readers/Pptx.hs +54/−0
- test/Tests/Readers/RST.hs +13/−0
- test/Tests/Readers/Xlsx.hs +40/−0
- test/Tests/Shared.hs +8/−0
- test/Tests/Writers/Docx.hs +26/−0
- test/Tests/Writers/LaTeX.hs +2/−2
- test/Tests/Writers/Markdown.hs +26/−0
- test/Tests/Writers/ODT.hs +54/−0
- test/ansi-test.ansi +7/−0
- test/ansi-test.txt +6/−0
- test/asciidoc-reader.native +3831/−3849
- test/command/1023.md +4/−6
- test/command/10390.md +2/−2
- test/command/10490.md +16/−16
- test/command/10643.md +11/−1
- test/command/11088.md +10/−0
- test/command/11113.docx binary
- test/command/11266.md +0/−1
- test/command/1166.md +6/−6
- test/command/11689.docx binary
- test/command/11833.docx binary
- test/command/11845.md +39/−0
- test/command/11849.md +42/−0
- test/command/11852.md +33/−0
- test/command/11858.md +11/−0
- test/command/11869.md +8/−0
- test/command/11888.md +46/−0
- test/command/11888/section1/lab.md +11/−0
- test/command/11888/section2/lab.md +8/−0
- test/command/11888/section3/lab.md +7/−0
- test/command/11897.md +67/−0
- test/command/11927.md +6/−0
- test/command/11936.md +139/−0
- test/command/11939.md +19/−0
- test/command/11942.js +1/−0
- test/command/11942.md +14/−0
- test/command/11944.docx binary
- test/command/11944.md +14/−0
- test/command/11945.md +34/−0
- test/command/11954.md +12/−0
- test/command/11961.md +8/−0
- test/command/11979.md +6/−0
- test/command/11981.md +14/−0
- test/command/11987.md +9/−0
- test/command/11988.docx binary
- test/command/11988.md +25/−0
- test/command/11989.docx binary
- test/command/11989.md +19/−0
- test/command/11991.docx binary
- test/command/11991.md +37/−0
- test/command/12001.md +68/−0
- test/command/12001/grid/doc.md +3/−0
- test/command/12001/next/doc.md +1/−0
- test/command/12001/note/doc.md +3/−0
- test/command/12051.md +10/−0
- test/command/12060.md +12/−0
- test/command/12063.md +14/−0
- test/command/12071.md +11/−0
- test/command/2378.md +2/−1
- test/command/2606.md +2/−2
- test/command/2623.md +7/−0
- test/command/2623.odt binary
- test/command/2649.md +15/−15
- test/command/2994.md +2/−2
- test/command/3958.md +2/−2
- test/command/4558.md +69/−0
- test/command/4819.md +3/−1
- test/command/4880.md +2/−2
- test/command/5116.md +6/−3
- test/command/5367.md +7/−3
- test/command/5476.md +2/−2
- test/command/5753.md +2/−2
- test/command/6549.md +3/−3
- test/command/6792.md +1/−4
- test/command/6821.md +2/−1
- test/command/6959.md +5/−4
- test/command/6992.md +5/−5
- test/command/7181.md +2/−1
- test/command/7214.md +1/−1
- test/command/7436.md +2/−2
- test/command/7691.docx binary
- test/command/7744.md +62/−0
- test/command/8110.md +9/−9
- test/command/8240.md +2/−2
- test/command/8257.md +1/−1
- test/command/8659.md +5/−5
- test/command/8770-block.md +1/−1
- test/command/8770-document.md +1/−1
- test/command/8770-section.md +1/−1
- test/command/8948.md +1/−1
- test/command/9002.docx binary
- test/command/9002.md +2/−1
- test/command/9183.docx binary
- test/command/9183.md +10/−0
- test/command/9197.md +33/−0
- test/command/9222.docx binary
- test/command/9222.md +10/−0
- test/command/9358.docx binary
- test/command/9391.docx binary
- test/command/9467.md +1/−1
- test/command/9603.docx binary
- test/command/9635.md +1/−1
- test/command/9716.md +1/−1
- test/command/9807.md +1/−3
- test/command/biblatex-edtf-date.md +1/−0
- test/command/completion.md +18/−18
- test/command/epub-invalid-date.md +15/−0
- test/command/epub-title-header-id.md +22/−0
- test/command/fodt-roundtrip.md +126/−0
- test/command/latex-opt-arg-state.md +38/−0
- test/command/latex-tokenize-hash-positions.md +13/−0
- test/command/latex-url-comment-positions.md +12/−0
- test/command/latex3-commands.md +473/−0
- test/command/latex3-usrguide.md +453/−0
- test/command/nested-notes.md +43/−0
- test/command/opendocument-abstract.opendocument +1/−0
- test/command/opendocument-body.opendocument +1/−0
- test/command/opendocument-styles.md +131/−0
- test/command/pandoc-citeproc-119.md +1/−1
- test/command/pandoc-citeproc-392.md +2/−2
- test/command/pandoc-citeproc-57.md +1/−1
- test/command/pandoc-citeproc-77.md +11/−8
- test/command/pandoc-citeproc-chicago-fullnote-bibliography.md +3/−2
- test/command/typst-highlight.md +98/−0
- test/command/typst-property-output.md +2/−0
- test/djot-reader.native +11/−12
- test/docx/0_level_headers.docx binary
- test/docx/adjacent_codeblocks.docx binary
- test/docx/already_auto_ident.docx binary
- test/docx/alternate_document_path.docx binary
- test/docx/anchor_header_after_anchor.docx binary
- test/docx/block_quotes.docx binary
- test/docx/char_styles.docx binary
- test/docx/codeblock.docx binary
- test/docx/comments.docx binary
- test/docx/comments.native +152/−4
- test/docx/comments_warning.docx binary
- test/docx/compact-style-removal.docx binary
- test/docx/cross_reference.docx binary
- test/docx/csl_bibliography.native +50/−0
- test/docx/custom_xml.docx binary
- test/docx/custom_xml.native +36/−0
- test/docx/deep_normalize.docx binary
- test/docx/definition_list.docx binary
- test/docx/diagram.docx binary
- test/docx/double_strikeout.docx binary
- test/docx/double_strikeout.native +68/−0
- test/docx/drop_cap.docx binary
- test/docx/dummy_item_after_list_item.docx binary
- test/docx/dummy_item_after_paragraph.docx binary
- test/docx/empty_field.docx binary
- test/docx/enumerated_headings.docx binary
- test/docx/field_no_instrText.docx binary
- test/docx/field_no_instrText.native +24/−0
- test/docx/field_no_instrText_codeblock.docx binary
- test/docx/field_no_instrText_codeblock.native +1/−0
- test/docx/field_tracked_instrText.docx binary
- test/docx/field_tracked_instrText.native +14/−0
- test/docx/german_styled_lists.docx binary
- test/docx/golden/block_quotes.docx binary
- test/docx/golden/codeblock.docx binary
- test/docx/golden/comments.docx binary
- test/docx/golden/csl_bibliography.docx binary
- test/docx/golden/custom_style_no_reference.docx binary
- test/docx/golden/custom_style_preserve.docx binary
- test/docx/golden/definition_list.docx binary
- test/docx/golden/document-properties-short-desc.docx binary
- test/docx/golden/document-properties.docx binary
- test/docx/golden/headers.docx binary
- test/docx/golden/image.docx binary
- test/docx/golden/inline_code.docx binary
- test/docx/golden/inline_formatting.docx binary
- test/docx/golden/inline_images.docx binary
- test/docx/golden/link_in_notes.docx binary
- test/docx/golden/links.docx binary
- test/docx/golden/lists.docx binary
- test/docx/golden/lists_9994.docx binary
- test/docx/golden/lists_continuing.docx binary
- test/docx/golden/lists_div_bullets.docx binary
- test/docx/golden/lists_multiple_initial.docx binary
- test/docx/golden/lists_restarting.docx binary
- test/docx/golden/mark_math.docx binary
- test/docx/golden/nested_anchors_in_header.docx binary
- test/docx/golden/notes.docx binary
- test/docx/golden/raw-blocks.docx binary
- test/docx/golden/raw-bookmarks.docx binary
- test/docx/golden/table_one_row.docx binary
- test/docx/golden/table_with_list_cell.docx binary
- test/docx/golden/tables-default-widths.docx binary
- test/docx/golden/tables.docx binary
- test/docx/golden/tables_separated_with_rawblock.docx binary
- test/docx/golden/task_list.docx binary
- test/docx/golden/track_changes_deletion.docx binary
- test/docx/golden/track_changes_insertion.docx binary
- test/docx/golden/track_changes_move.docx binary
- test/docx/golden/track_changes_scrubbed_metadata.docx binary
- test/docx/golden/unicode.docx binary
- test/docx/golden/verbatim_subsuper.docx binary
- test/docx/hanging_indent.docx binary
- test/docx/headers.docx binary
- test/docx/i18n_blocks.docx binary
- test/docx/image.docx binary
- test/docx/image_vml.docx binary
- test/docx/image_vml_as_object.docx binary
- test/docx/image_with_textbox_caption.docx binary
- test/docx/inline_code.docx binary
- test/docx/inline_formatting.docx binary
- test/docx/instrText_hyperlink.docx binary
- test/docx/link_in_notes.docx binary
- test/docx/link_tooltips.docx binary
- test/docx/link_tooltips.native +112/−0
- test/docx/links.docx binary
- test/docx/lists-compact.docx binary
- test/docx/lists.docx binary
- test/docx/lists_continuing.docx too large to diff
- test/docx/lists_level_override.docx too large to diff
- test/docx/lists_level_override.native too large to diff
- test/docx/lists_restart_8367.docx too large to diff
- test/docx/lists_restarting.docx too large to diff
- test/docx/lists_sublist_reset.docx too large to diff
- test/docx/mark_math.native too large to diff
- test/docx/mendeley_citations.docx too large to diff
- test/docx/metadata.docx too large to diff
- test/docx/metadata_after_normal.docx too large to diff
- test/docx/nested_anchors_in_header.docx too large to diff
- test/docx/nested_instrText.docx too large to diff
- test/docx/nested_sdt.docx too large to diff
- test/docx/nested_smart_tags.docx too large to diff
- test/docx/normalize.docx too large to diff
- test/docx/notes.docx too large to diff
- test/docx/numbered_header.docx too large to diff
- test/docx/overlapping_targets.docx too large to diff
- test/docx/pageref.docx too large to diff
- test/docx/paragraph_insertion_deletion.docx too large to diff
- test/docx/relative_indentation_blockquotes.docx too large to diff
- test/docx/sdt_elements.docx too large to diff
- test/docx/sdt_elements.native too large to diff
- test/docx/sdt_in_footnote.docx too large to diff
- test/docx/special_punctuation.docx too large to diff
- test/docx/table_captions_no_field.docx too large to diff
- test/docx/table_captions_with_field.docx too large to diff
- test/docx/table_gridbefore.docx too large to diff
- test/docx/table_header_rowspan.docx too large to diff
- test/docx/table_header_rowspan.native too large to diff
- test/docx/table_one_header_row.docx too large to diff
- test/docx/table_one_row.docx too large to diff
- test/docx/table_variable_width.docx too large to diff
- test/docx/table_with_list_cell.docx too large to diff
- test/docx/table_with_list_cell.native too large to diff
- test/docx/tables.docx too large to diff
- test/docx/tabs.docx too large to diff
- test/docx/task_list.docx too large to diff
- test/docx/text_in_shape_format.docx too large to diff
- test/docx/textbox_image.docx too large to diff
- test/docx/textbox_image_duplicate_encoding.docx too large to diff
- test/docx/track_changes_deletion.docx too large to diff
- test/docx/track_changes_edge_spaces.docx too large to diff
- test/docx/track_changes_edge_spaces_accept.native too large to diff
- test/docx/track_changes_edge_spaces_all.native too large to diff
- test/docx/track_changes_edge_spaces_reject.native too large to diff
- test/docx/track_changes_insertion.docx too large to diff
- test/docx/track_changes_insertion_all.native too large to diff
- test/docx/track_changes_move.docx too large to diff
- test/docx/track_changes_paragraph_mark_in_note.docx too large to diff
- test/docx/track_changes_paragraph_mark_in_note_accept.native too large to diff
- test/docx/track_changes_paragraph_mark_in_note_reject.native too large to diff
- test/docx/track_changes_scrubbed_metadata.docx too large to diff
- test/docx/track_changes_scrubbed_metadata.native too large to diff
- test/docx/trailing_spaces_in_formatting.docx too large to diff
- test/docx/trim_last_inline.docx too large to diff
- test/docx/unicode.docx too large to diff
- test/docx/unused_anchors.docx too large to diff
- test/docx/verbatim_subsuper.docx too large to diff
- test/docx/zotero_citations.docx too large to diff
- test/jats-reader.native too large to diff
- test/jats-reader.xml too large to diff
- test/markdown-reader-more.native too large to diff
- test/mediawiki-reader.native too large to diff
- test/odt/fodt-native/image.native too large to diff
- test/odt/fodt-native/styles-off.native too large to diff
- test/odt/fodt-native/styles.native too large to diff
- test/odt/fodt/formula.fodt too large to diff
- test/odt/fodt/image.fodt too large to diff
- test/odt/fodt/preformattedText.fodt too large to diff
- test/odt/fodt/styles.fodt too large to diff
- test/odt/fodt/textMixedStyles.fodt too large to diff
- test/odt/native/listContinueNumbering3.native too large to diff
- test/odt/native/referenceToChapter.native too large to diff
- test/odt/native/simpleTableWithCaption.native too large to diff
- test/odt/odt/blockquote.odt too large to diff
- test/odt/odt/blockquote2.odt too large to diff
- test/odt/odt/bold.odt too large to diff
- test/odt/odt/citation.odt too large to diff
- test/odt/odt/endnote.odt too large to diff
- test/odt/odt/expression.odt too large to diff
- test/odt/odt/expressionUnevaluated.odt too large to diff
- test/odt/odt/externalLink.odt too large to diff
- test/odt/odt/footnote.odt too large to diff
- test/odt/odt/formula.odt too large to diff
- test/odt/odt/headers.odt too large to diff
- test/odt/odt/hiddenTextByStyle.odt too large to diff
- test/odt/odt/hiddenTextByVariable.odt too large to diff
- test/odt/odt/horizontalRule.odt too large to diff
- test/odt/odt/image.odt too large to diff
- test/odt/odt/imageIndex.odt too large to diff
- test/odt/odt/imageRelative.odt too large to diff
- test/odt/odt/imageWithCaption.odt too large to diff
- test/odt/odt/inlinedCode.odt too large to diff
- test/odt/odt/italic.odt too large to diff
- test/odt/odt/listBlocks.odt too large to diff
- test/odt/odt/listContinueNumbering.odt too large to diff
- test/odt/odt/listContinueNumbering2.odt too large to diff
- test/odt/odt/listContinueNumbering3.odt too large to diff
- test/odt/odt/orderedListHeader.odt too large to diff
- test/odt/odt/orderedListMixed.odt too large to diff
- test/odt/odt/orderedListRoman.odt too large to diff
- test/odt/odt/orderedListSimple.odt too large to diff
- test/odt/odt/paragraph.odt too large to diff
- test/odt/odt/preformattedText.odt too large to diff
- test/odt/odt/preformattedTextParentStyle.odt too large to diff
- test/odt/odt/referenceAllInOne.odt too large to diff
- test/odt/odt/referenceToChapter.odt too large to diff
- test/odt/odt/referenceToListItem.odt too large to diff
- test/odt/odt/referenceToText.odt too large to diff
- test/odt/odt/simpleTable.odt too large to diff
- test/odt/odt/simpleTableWithCaption.odt too large to diff
- test/odt/odt/simpleTableWithHeader.odt too large to diff
- test/odt/odt/simpleTableWithMultipleHeaderRows.odt too large to diff
- test/odt/odt/sourceText.odt too large to diff
- test/odt/odt/strikeout.odt too large to diff
- test/odt/odt/tab.odt too large to diff
- test/odt/odt/table.odt too large to diff
- test/odt/odt/tableWithCaption.odt too large to diff
- test/odt/odt/tableWithContents.odt too large to diff
- test/odt/odt/tableWithSpans.odt too large to diff
- test/odt/odt/textMixedStyles.odt too large to diff
- test/odt/odt/trackedChanges.odt too large to diff
- test/odt/odt/underlined.odt too large to diff
- test/odt/odt/unicode.odt too large to diff
- test/odt/odt/unorderedList.odt too large to diff
- test/odt/odt/unorderedListHeader.odt too large to diff
- test/odt/odt/variable.odt too large to diff
- test/pptx-reader/basic.native too large to diff
- test/pptx-reader/basic.pptx too large to diff
- test/pptx-reader/charts.native too large to diff
- test/pptx-reader/charts.pptx too large to diff
- test/pptx-reader/equations.native too large to diff
- test/pptx-reader/equations.pptx too large to diff
- test/pptx-reader/formatting.native too large to diff
- test/pptx-reader/formatting.pptx too large to diff
- test/pptx-reader/groups.native too large to diff
- test/pptx-reader/groups.pptx too large to diff
- test/pptx-reader/hidden.native too large to diff
- test/pptx-reader/hidden.pptx too large to diff
- test/pptx-reader/lists-writer.native too large to diff
- test/pptx-reader/lists-writer.pptx too large to diff
- test/pptx-reader/lists.native too large to diff
- test/pptx-reader/lists.pptx too large to diff
- test/pptx-reader/notes.native too large to diff
- test/pptx-reader/notes.pptx too large to diff
- test/pptx-reader/runs.native too large to diff
- test/pptx-reader/runs.pptx too large to diff
- test/pptx-reader/slide-numbers.native too large to diff
- test/pptx-reader/slide-numbers.pptx too large to diff
- test/pptx-reader/table-spans.native too large to diff
- test/pptx-reader/table-spans.pptx too large to diff
- test/pptx/background-image/output.pptx too large to diff
- test/pptx/background-image/templated.pptx too large to diff
- test/pptx/blanks/just-speaker-notes/output.pptx too large to diff
- test/pptx/blanks/just-speaker-notes/templated.pptx too large to diff
- test/pptx/document-properties/output.pptx too large to diff
- test/pptx/document-properties/templated.pptx too large to diff
- test/pptx/footer/basic/output.pptx too large to diff
- test/pptx/footer/fixed-date/output.pptx too large to diff
- test/pptx/footer/higher-slide-number/output.pptx too large to diff
- test/pptx/footer/no-title-slide/output.pptx too large to diff
- test/pptx/layouts/deleted.pptx too large to diff
- test/pptx/layouts/moved.pptx too large to diff
- test/pptx/metadata-speaker-notes/output.pptx too large to diff
- test/pptx/metadata-speaker-notes/templated.pptx too large to diff
- test/pptx/reference-no-slides/with-notes/output.pptx too large to diff
- test/pptx/speaker-notes-after-metadata/output.pptx too large to diff
- test/pptx/speaker-notes-after-metadata/templated.pptx too large to diff
- test/pptx/speaker-notes-afterheader/output.pptx too large to diff
- test/pptx/speaker-notes-afterheader/templated.pptx too large to diff
- test/pptx/speaker-notes-afterseps/output.pptx too large to diff
- test/pptx/speaker-notes-afterseps/templated.pptx too large to diff
- test/pptx/speaker-notes/output.pptx too large to diff
- test/pptx/speaker-notes/templated.pptx too large to diff
- test/rst-reader.native too large to diff
- test/rtf/unicode.native too large to diff
- test/rtf/unicode.rtf too large to diff
- test/s5-basic.html too large to diff
- test/s5-fancy.html too large to diff
- test/s5-inserts.html too large to diff
- test/test-pandoc.hs too large to diff
- test/typst-reader.native too large to diff
- test/typst-reader.typ too large to diff
- test/vimwiki-reader.native too large to diff
- test/writer.html4 too large to diff
- test/writer.html5 too large to diff
- test/writer.opendocument too large to diff
- test/writer.org too large to diff
- test/writer.tei too large to diff
- test/xlsx-reader/absolute-targets.native too large to diff
- test/xlsx-reader/absolute-targets.xlsx too large to diff
- test/xlsx-reader/basic.native too large to diff
- test/xlsx-reader/cell-fonts.native too large to diff
- test/xlsx-reader/cell-fonts.xlsx too large to diff
- test/xlsx-reader/charts.native too large to diff
- test/xlsx-reader/charts.xlsx too large to diff
- test/xlsx-reader/dates-1904.native too large to diff
- test/xlsx-reader/dates-1904.xlsx too large to diff
- test/xlsx-reader/dates.native too large to diff
- test/xlsx-reader/dates.xlsx too large to diff
- test/xlsx-reader/merged.native too large to diff
- test/xlsx-reader/merged.xlsx too large to diff
- test/xlsx-reader/numbers.native too large to diff
- test/xlsx-reader/numbers.xlsx too large to diff
- test/xlsx-reader/strings.native too large to diff
- test/xlsx-reader/strings.xlsx too large to diff
- xml-light/Text/Pandoc/XML/Light.hs too large to diff
- xml-light/Text/Pandoc/XML/Light/Output.hs too large to diff
@@ -6,6 +6,7 @@ - Agustín Martín Barbero - Akash Patel - Akos Marton+- Alain Lesage - Albert Krewinkel - Albert Lei - Alex Ivkin@@ -25,9 +26,11 @@ - Amy de Buitléir - Anabra - Anders Waldenborg+- Andonome - Andreas Deininger - Andreas Lööw - Andreas Scherer+- Andrzej Rybczak - Andres Freund - Andrew Dunning - Andy Morris@@ -43,7 +46,9 @@ - Arlo O'Keeffe - Artem Pelenitsyn - Artyom Kazak+- Aslak Hellesøy - Asliddinbek Azizovich+- Asuka - B. Scott Michel - Bastien Dumont - Ben Firshman@@ -62,6 +67,7 @@ - Brent Yorgey - Brian Leung - Bryan O'Sullivan+- Caio Vinícius - Caleb McDaniel - Caleb Mclennan - Calvin Beck@@ -87,6 +93,7 @@ - Christoffer Sawicki - Christophe Dervieux - Christopher Kenny+- Clar Fon - Clare Macrae - Clint Adams - Conal Elliott@@ -125,6 +132,7 @@ - Eric Kow - Eric Schrijver - Eric Seidel+- Erik Demaine - Erik Post - Erik Rask - Ethan Riley@@ -151,6 +159,7 @@ - Fyodor Sheremetyev - Gabor Pali - Gabriel Lewertowski+- Gaurav Vijay Jadhav - Gavin Beatty - George Stagg - Georgi Lyubenov@@ -212,6 +221,7 @@ - Jens Oehlschlägel - Jesse Hathaway - Jeremie Knuesel+- Jeremy Levartovsky - Jérémy Bobbio - Jeroen de Haas - Jerry Sky@@ -250,6 +260,7 @@ - Konstantin Zudov - Kristof Bastiaensen - Krystof Beuermann+- Kyohei Takahashi - Lars-Dominik Braun - Laurent P. René de Cotret - Lawrence Chonavel@@ -311,6 +322,7 @@ - Mohamed Akram - Morgan Willcock - Morton Fox+- Nacho García - Nathan Gass - Naveen - Neil Mayhew@@ -339,6 +351,7 @@ - Owen McGrath - Pablo Rodríguez - Pascal Wagler+- Patrick Creighton - Pau RE - Paul Rivier - Paul Tilley@@ -359,6 +372,7 @@ - Puneeth Chaganti - Quinn - R. N. West+- Raffaele Mancuso - Ralf Stephan - Raniere Silva - Raymond Berger@@ -366,6 +380,7 @@ - Recai Oktaş - Repetitive - Reuben Thomas+- Robert Szarka - Robertas - Rowan Rodrik van der Molen - Roland Hieber@@ -378,6 +393,7 @@ - Salim B - Sam S. Almahri - Sam May+- Samuel Huang - Samuel Tardieu - Saumel Lemmenmeier - Santiago Zarate@@ -457,6 +473,7 @@ - William Lupton - William Rusnack - Winnie Hellmann+- Wleeaf - Wout Gevaert - Wrong-Code - Xavier Olive@@ -467,6 +484,7 @@ - Yoan Blanc - You Jiangbin - Yuchen Pei+- Yusuf Efe - Zihang Chen - 3w36zj6 - arcnmx@@ -486,6 +504,7 @@ - dbecher-ito - ebiim - ech0+- enieuwy - etclub - favonia - gemmaro@@ -524,6 +543,7 @@ - priiduonu - qerub - quasicomputational+- rca-umb - reptee - ricnorr - robabla@@ -542,6 +562,7 @@ - thsutton - thron7 - timo-a+- tyler000000 - vijayphoenix - vkraven - wgevaert@@ -549,4 +570,5 @@ - willj-dev - wuffi - wzy+- zenor0 - λx.x
@@ -1,7 +1,7 @@ --- title: Pandoc User's Guide author: John MacFarlane-date: 2026-08-28+date: 2026-11-10 --- # Synopsis@@ -146,7 +146,7 @@ [`babel`] (with `lang`). If `CJKmainfont` is set, [`xeCJK`] is needed if `xelatex` is used, else [`luatexja`] is needed if `lualatex` is used. [`framed`] is required if code is highlighted in a-scheme that use a colored background. The use of `xelatex` or+scheme that uses a colored background. The use of `xelatex` or `lualatex` as the PDF engine requires [`fontspec`]. `lualatex` uses [`selnolig`] and [`lua-ul`]. `xelatex` uses [`bidi`] (with the `dir` variable set).@@ -251,6 +251,7 @@ - `endnotexml` ([EndNote XML bibliography]) - `epub` ([EPUB]) - `fb2` ([FictionBook2] e-book)+ - `fodt` (flat [OpenDocument text document][ODT]) - `gfm` ([GitHub-Flavored Markdown]), or the deprecated and less accurate `markdown_github`; use [`markdown_github`](#markdown-variants) only@@ -327,6 +328,7 @@ - `epub` or `epub3` ([EPUB] v3 book) - `epub2` (EPUB v2) - `fb2` ([FictionBook2] e-book)+ - `fodt` (flat [OpenDocument text document][ODT]) - `gfm` ([GitHub-Flavored Markdown]), or the deprecated and less accurate `markdown_github`; use [`markdown_github`](#markdown-variants) only@@ -416,6 +418,19 @@ is not created by pandoc, so you will need to create it yourself if you want to make use of it.) + If the `PANDOC_DATA_DIRS` environment variable is set to a list+ of directories (separated by `:`, or `;` on Windows), these+ directories will be searched, in order, for data files not+ found in the user data directory, before pandoc falls back to+ its default data files. This applies to all data files that+ are looked up in the user data directory, including templates,+ filters, defaults files, metadata files, and CSL styles. Only+ absolute paths in `PANDOC_DATA_DIRS` are used. This makes it+ possible for a package manager to install, for example, Lua+ filters into `/some/prefix/share/pandoc/filters` and make them+ available by adding `/some/prefix/share/pandoc` to+ `PANDOC_DATA_DIRS`.+ `-d` *FILE*, `--defaults=`*FILE* : Specify a set of default option settings. *FILE* is a YAML or JSON@@ -767,7 +782,7 @@ `all` includes all insertions, deletions, and comments, wrapped in spans with `insertion`, `deletion`, `comment-start`, and `comment-end` classes, respectively. The author and time of- change is included. `all` is useful for scripting: only+ change are included. `all` is useful for scripting: only accepting changes from a certain reviewer, say, or before a certain date. If a paragraph is inserted or deleted, `track-changes=all` produces a span with the class@@ -779,7 +794,7 @@ : Extract images and other media contained in or linked from the source document to the path *DIR*, creating it if- necessary, and adjust the images references in the document+ necessary, and adjust the image references in the document so they point to the extracted files. Media are downloaded, read from the file system, or extracted from a binary container (e.g. docx), as needed. The original file paths@@ -1146,7 +1161,10 @@ `--link-images[=true|false]` : Include links to images instead of embedding the images in ODT.- (This option currently only affects ODT output.)+ (This option currently only affects `odt` and `fodt` output.)+ This is worth considering for `fodt`, where images have to be+ base64-encoded and so take about a third more space than in the+ compressed `odt` package. `--html-q-tags[=true|false]` @@ -1250,7 +1268,7 @@ `--listings[=true|false]` : *Deprecated, use `--syntax-highlighting=idiomatic` or- `--syntax-highlighting=default` instead.+ `--syntax-highlighting=default` instead.* Use the [`listings`] package for LaTeX code blocks. The package does not support multi-byte encoding for source code. To handle UTF-8@@ -1408,6 +1426,12 @@ Then open `custom-reference.odt` in LibreOffice, modify the styles as you wish, and save the file. + `fodt` output uses the same reference documents as `odt`;+ the reference is still a zipped `.odt`, which is flattened+ along with the generated content. Note that an automatic+ style of the reference document is dropped, with a warning,+ if the generated content defines a style of the same name.+ PowerPoint : Templates included with Microsoft PowerPoint 2013 (either with@@ -1493,7 +1517,7 @@ `<dc:title>` (from the document title), `<dc:creator>` (from the document authors), `<dc:date>` (from the document date, which should be in [ISO 8601 format]), `<dc:language>` (from the `lang`- variable, or, if is not set, the locale), and `<dc:identifier+ variable, or, if it is not set, the locale), and `<dc:identifier id="BookId">` (a randomly generated UUID). Any of these may be overridden by elements in the metadata file. @@ -1624,7 +1648,7 @@ : Set the `bibliography` field in the document's metadata to *FILE*, overriding any value set in the metadata. If you supply this argument multiple times, each *FILE* will be added to- bibliography. If *FILE* is a URL, it will be fetched+ the bibliography. If *FILE* is a URL, it will be fetched via HTTP. If *FILE* is not found relative to the working directory, it will be sought in the resource path (see `--resource-path`).@@ -1678,7 +1702,7 @@ `mathjax`[`:`*URL*] - : Render math using [MathJaX]. TeX math will be put between+ : Render math using [MathJax]. TeX math will be put between `\(...\)` (for inline math) or `\[...\]` (for display math) and wrapped in `<span>` tags with class `math`. Then the MathJax JavaScript will render it. The *URL*@@ -2358,7 +2382,7 @@ | ``` | ``` | +----------------------------------+-----------------------------------+ -If the command line option accepts a URL argument, an `url:` field can+If the command line option accepts a URL argument, a `url:` field can be added to `math-method:`. ## Options for wrapper scripts@@ -2783,7 +2807,7 @@ - `roman`: Converts textual values that can be read as an integer into lowercase roman numerals.- This can be used to get lettered enumeration from array indices.+ This can be used to get roman-numeral enumeration from array indices. To get uppercase roman, chain with `uppercase`. - `left n "leftborder" "rightborder"`: Renders a textual value@@ -2937,17 +2961,24 @@ to e.g. `20px`, but it also accepts `pt` (12pt = 16px in most browsers). -`fontcolor`+`fontcolor`, `fontcolordark` : sets the CSS `color` property on the `html` element. -`linkcolor`+`linkcolor`, `linkcolordark` : sets the CSS `color` property on all links. +`quotebordercolor`, `quotebordercolordark`+: sets the CSS `border-color` property for the left-border of+ `blockquote` elements.++`quotecolor`, `quotecolordark`+: sets the CSS `color` property for `blockquote` elements.+ `monofont`-: sets the CSS `font-family` property on `code` elements.+: sets the CSS `font-family` property on `code` and `pre` elements. -`monobackgroundcolor`-: sets the CSS `background-color` property on `code` elements+`monobackgroundcolor`, `monobackgroundcolordark`+: sets the CSS `background-color` property on `code` and `pre` elements and adds extra padding. `linestretch`@@ -2957,7 +2988,7 @@ `maxwidth` : sets the CSS `max-width` property (default is 36em). -`backgroundcolor`+`backgroundcolor`, `backgroundcolordark` : sets the CSS `background-color` property on the `html` element. `margin-left`, `margin-right`, `margin-top`, `margin-bottom`@@ -2985,6 +3016,41 @@ [CSS]: https://developer.mozilla.org/en-US/docs/Learn/CSS +#### Note on `color-scheme`++By default, the browser's chosen `color-scheme` (either `light` or `dark`)+will affect the color of various unstyled elements like input fields, which are+generally only applicable if your document is specifically targeting HTML.++By default, Pandoc emits `color-scheme: light dark` to allow the system to+choose between a default light-mode or dark-mode scheme, but it alters this+if variables specifically override colors, under the assumption that the user+wishes for that scheme to always be used.++Currently, this is only per a basic heuristic that checks whether the following+variables are provided:++* `fontcolor`+* `fontcolordark`+* `backgroundcolor`+* `backgroundcolordark`++And if only one "mode" is provided (light or dark), `color-scheme` is set so+that this chosen mode is used always, regardless of the system's preference for+light or dark mode.++Note that there are a few caveats to this, namely that:++* If the browser does not support `light-dark` (older than ~2024), then it will+ always choose light mode, regardless of this override.+* When printing, light mode is always chosen, and color variables are ignored.+* Syntax highlighting is currently unaffected by color scheme.++In general, if you wish to override the theming for a single scheme, you should+choose to override either the normal `*color` variables (light mode) or the+`*colordark` variables (dark mode) to ensure that the styles do not conflict+with other styles added by new versions of Pandoc or the browser itself.+ ### Variables for HTML math `classoption`@@ -3439,7 +3505,7 @@ `pdfaintent` : when used in conjunction with `pdfa`, specifies the output intent for- the colors, e.g. `ISO coated v2 300\letterpercent\space (ECI)`+ the colors, e.g. `ISO coated v2 300\letterpercent\space (ECI)`. If left unspecified, `sRGB IEC61966-2.1` is used as default. `toc`@@ -3550,10 +3616,20 @@ : color for external links, internal links, and citation links, respectively: expects a hexadecimal color code +`csl-entry-spacing`+: entry spacing between citeproc-generated bibliography entries+ (with a unit, e.g. `1em`). This will be set automatically+ based on the CSL style but can be overridden.++`csl-hanging-indent`+: amount of hanging indent in citeproc-generated bibliography entries+ (with a unit, e.g. `1.5em`). This will be set automatically+ based on the CSL style but can be overridden.+ ### Variables for ms `fontfamily`-: `A` (Avant Garde), `B` (Bookman), `C` (Helvetica), `HN`+: `A` (Avant Garde), `B` (Bookman), `H` (Helvetica), `HN` (Helvetica Narrow), `P` (Palatino), or `T` (Times New Roman). This setting does not affect source code, which is always displayed using monospace Courier. These built-in fonts are@@ -3595,8 +3671,8 @@ The recognized formats for `date` are: `mm/dd/yyyy`, `mm/dd/yy`, `yyyy-mm-dd` (ISO 8601), `dd MM yyyy` (e.g. either `02 Apr 2018` or `02 April 2018`),- `MM dd, yyyy` (e.g. `Apr. 02, 2018` or `April 02, 2018),- `yyyy[mm[dd]]` (e.g. `20180402, `201804` or `2018`).+ `MM dd, yyyy` (e.g. `Apr. 02, 2018` or `April 02, 2018`),+ `yyyy[mm[dd]]` (e.g. `20180402`, `201804` or `2018`). `header-includes` : contents specified by `-H/--include-in-header` (may have multiple@@ -3708,7 +3784,7 @@ then in reading LaTeX pandoc will parse these characters literally. In writing LaTeX, enabling `smart` tells pandoc to use the ligatures when possible; if `smart` is disabled-pandoc will use unicode quotation mark and dash characters.+pandoc will use Unicode quotation mark and dash characters. ## Headings and sections @@ -3955,9 +4031,9 @@ substituted with cross-references that will use the number of the referenced item. The original link text is discarded. This extension can be combined with `xrefs_name` in which case-the name or caption numbers will appear after the number.+the name or caption will appear after the number. -For the `xrefs_number` to be useful heading numbers must be enabled+For the `xrefs_number` extension to be useful, heading numbers must be enabled in the generated document, also table and figure captions must be enabled using for example the `native_numbering` extension. @@ -3980,8 +4056,20 @@ (Table styles will be added to the Table elements directly.) This extension can be used with [docx custom styles](#custom-styles). +When converting from odt or fodt, add `custom-styles`+attributes for paragraph and character styles, using the+style names shown in the word processor. Automatic styles,+which only record direct formatting, are not recorded; instead,+the named style they are based on is used. A few styles are not+recorded: the default paragraph style (`Default Paragraph Style`,+or `Standard`), the default styles of table cells, and styles+that pandoc interprets itself (such as `Emphasis` and `Strong+Emphasis`). The paragraphs of list items are not wrapped in Divs.+A heading gets a `custom-style` attribute if its style is not the+standard one for its level.+ input formats-: `docx`+: `docx`, `odt`, `fodt` ### Extension: `amuse` ### @@ -4352,7 +4440,7 @@ ### Extension: `fenced_code_attributes` ### -Optionally, you may attach attributes to fenced or backtick code block using+Optionally, you may attach attributes to a fenced or backtick code block using this syntax: ~~~~ {#mycode .haskell .numberLines startFrom="100"}@@ -5027,7 +5115,7 @@ the number of dashes in the line separating the table header from the table body. (For example `---|-` would make the first column 3/4 and the second column 1/4 of the full text width.)-On the other hand, if no lines are wider than column width, then+On the other hand, if no lines are wider than the column width, then cell contents will not be wrapped, and the cells will be sized to their contents. @@ -5064,6 +5152,13 @@ : The caption of the table. {#ident .float latex-placement="ht"} +To repeat the caption of a `longtable` on each page, add the+`repeat-caption` class to the table's attributes. Repeated captions are+not added to the list of tables. This class has no effect on tables with+the `float` class, since those tables cannot span pages.++ : The caption of the table. {.repeat-caption}+ Note that `longtable` does not work with two-column layouts, so if you are producing a two-column document using LaTeX, you will need to specify the `float` class on every table, or use this small Lua filter@@ -5532,7 +5627,7 @@ FictionBook2 ~ If `--math-method=webtex` is used, formulas are rendered as images- using CodeCogs or other compatible web service, downloaded+ using CodeCogs or another compatible web service, downloaded and embedded in the e-book. Otherwise, they will appear verbatim. HTML, Slidy, DZSlides, S5, EPUB@@ -5559,12 +5654,13 @@ For a more explicit way of including raw HTML in a Markdown document, see the [`raw_attribute` extension][Extension: `raw_attribute`]. -In the CommonMark format, if `raw_html` is enabled, superscripts,+In commonmark output, if `raw_html` is enabled, superscripts, subscripts, strikeouts and small capitals will be represented as HTML.-Otherwise, plain-text fallbacks will be used. Note that even if-`raw_html` is disabled, tables will be rendered with HTML syntax if-they cannot use pipe syntax.+Otherwise, plain-text fallbacks will be used. +Disabling the `raw_html` extension has no effect on `commonmark` (or+`gfm` or `commonmark_x`) *input*.+ ### Extension: `markdown_in_html_blocks` ### Original Markdown allows you to include HTML "blocks": blocks@@ -5612,7 +5708,7 @@ ### Extension: `native_spans` ### -Use native pandoc `Span` blocks for content inside `<span>` tags.+Use native pandoc `Span` elements for content inside `<span>` tags. For the most part this should give the same output as `raw_html`, but it makes it easier to write pandoc filters to manipulate groups of inlines.@@ -5753,7 +5849,7 @@ The link consists of link text in square brackets, followed by a label in square brackets. (There cannot be space between the two unless the `spaced_reference_links` extension is enabled.) If the label is empty-(`[]`), then it will be implicitly be taken to be the same as the link text;+(`[]`), then it will implicitly be taken to be the same as the link text; thus `[foo][]` is equivalent to `[foo][foo]`. (If the `shortcut_reference_links` extension is enabled, the empty `[]` may be omitted.)@@ -5765,7 +5861,7 @@ extension is enabled): citations take precedence over link labels. -Here are some examples of reference links and link definitions;+Here are some examples of reference links and link definitions: See [the website *I* built][my website]. @@ -5914,7 +6010,7 @@ itself contains dpi information). - The `%` unit is generally relative to some available space. For example the above example will render to the following.- - HTML: `<img href="file.jpg" style="width: 50%;" />`+ - HTML: `<img src="file.jpg" style="width: 50%;" />` - LaTeX: `\includegraphics[width=0.5\textwidth,height=\textheight]{file.jpg}` (If you're using a custom template, you need to configure `graphicx` as in the default template.)@@ -6096,7 +6192,7 @@ In complex cases, you can force something to be treated as a locator by enclosing it in curly braces or prevent parsing-the suffix as locator by prepending curly braces:+the suffix as a locator by prepending curly braces: [@smith{ii, A, D-Z}, with a suffix] [@smith, {pp. iv, vi-xi, (xv)-(xvii)} with suffix here]@@ -6187,7 +6283,7 @@ ### Extension: `mark` ### -To highlight out a section of text, begin and end it with+To highlight a section of text, begin and end it with `==`. Thus, for example, This ==is deleted text.==@@ -6430,7 +6526,7 @@ where `FORMAT` is replaced with the name of the format. Note that the list of extensions for `commonmark`,-`gfm`, and `commonmark_x` are defined relative to default+`gfm`, and `commonmark_x` is defined relative to default commonmark. So, for example, `backtick_code_blocks` does not appear as an extension, since it is enabled by default and cannot be disabled.@@ -6455,8 +6551,8 @@ `bibliography` metadata field in a YAML metadata section or the `--bibliography` command line argument. If you want to use multiple bibliography files, you can supply multiple-`--bibliography` arguments or set `bibliography` metadata field-to YAML array. A bibliography may have any of these formats:+`--bibliography` arguments or set the `bibliography` metadata field+to a YAML array. A bibliography may have any of these formats: Format File extension ------------ --------------@@ -6588,7 +6684,7 @@ Spin wave dispersion on the <span class="nocase">nm</span> scale -### Conference Papers, Published vs. Unpublished+### Conference papers, published vs. unpublished For a formally published conference paper, use the biblatex entry type `inproceedings` (which will be mapped to CSL `paper-conference`).@@ -6607,7 +6703,6 @@ which describes the publisher's location; do not use the latter for an unpublished conference paper. - ## Specifying a citation style Citations and references can be formatted using any style supported by the@@ -6738,8 +6833,8 @@ For LaTeX output, you can also use [`natbib`] or [`biblatex`] to render the bibliography. In order to do so, specify bibliography-files as outlined above, and add `--natbib` or `--biblatex`-argument to pandoc invocation. Bear in mind that bibliography+files as outlined above, and add the `--natbib` or `--biblatex`+argument to the pandoc invocation. Bear in mind that bibliography files have to be in either BibTeX (for `--natbib`) or BibLaTeX (for `--biblatex`) format. @@ -6753,7 +6848,7 @@ numerical styles only). Defaults to false. `link-bibliography`-: If true, DOIs, PMCIDs, PMID, and URLs in bibliographies will+: If true, DOIs, PMCIDs, PMIDs, and URLs in bibliographies will be rendered as hyperlinks. (If an entry contains a DOI, PMCID, PMID, or URL, but none of these fields are rendered by the style, then the title, or in the absence of a title the whole entry, will@@ -6955,6 +7050,11 @@ pandoc, unless an alternative reference doc is specified using `--reference-doc`. +When reading pptx, each slide becomes a level-2 heading with the+identifier `slide-N`, followed by the slide's content. Slides hidden+from the slide show are read too, with the class `hidden` on their+heading.+ ## Incremental lists By default, these writers produce lists that display "all at once."@@ -7070,6 +7170,10 @@ Notes are not yet supported for other slide formats, but the notes will not appear on the slides themselves. +When reading pptx, the speaker notes of each slide are read into a+`notes` div at the end of the slide's content, so a pptx file+converted to Markdown and back keeps its notes.+ ### Speaker notes on the title slide (PowerPoint) For PowerPoint output, the title slide is generated from the document's@@ -7628,7 +7732,7 @@ # Vimdoc -Vimdoc writer generates Vim help files and makes use of the following metadata+The Vimdoc writer generates Vim help files and makes use of the following metadata variables: ``` yaml@@ -7641,8 +7745,8 @@ vimdoc-prefix: pandoc ``` -Complete header requires `abstract`, `author`, `title` and `filename` to-be set. Compiling file with such metadata produces the following file+A complete header requires `abstract`, `author`, `title` and `filename` to+be set. Compiling a file with such metadata produces the following file (assumes `--standalone`, see [Templates]): ``` vimdoc@@ -7659,7 +7763,7 @@ ``` If `vimdoc-prefix` is set, all non-command tags are prefixed with its-value, it is used to prevent tag collision: all headers have a tag+value; this is used to prevent tag collision: all headers have a tag (either inferred or explicit) and multiple help pages can have the same header names, therefore collision is to be expected. Let our input be the following markdown file:@@ -7697,7 +7801,7 @@ inherently represent anything. ``` -Convert it to vimdoc with metadata variable set (e.g. with+Convert it to vimdoc with the metadata variable set (e.g. with `-M vimdoc-prefix=pandoc`) ``` vimdoc@@ -7787,11 +7891,11 @@ # Custom Styles -Custom styles can be used in the docx, odt and ICML formats.+Custom styles can be used in the docx, odt, fodt and ICML formats. ## Output -By default, pandoc's odt, docx and ICML output applies a predefined set of+By default, pandoc's odt, fodt, docx and ICML output applies a predefined set of styles for blocks such as paragraphs and block quotes, and uses largely default formatting (italics, bold) for inlines. This will work for most purposes, especially alongside a [reference doc](#option--reference-doc) file.@@ -7857,6 +7961,8 @@ attribute will be added for each style. Divs will be created to hold the paragraph styles, and Spans to hold the character styles. Table styles will be applied directly to the Table.+The odt and fodt readers also support the `styles` extension+(`-f odt+styles`), for paragraph and character styles. For example, using the `custom-style-reference.docx` file in the test directory, we have the following different outputs:@@ -8011,12 +8117,12 @@ pandoc -t context+tagging doc.md -o doc.pdf A recent `context` version should be used, as older versions-contained a bug that lead to invalid PDF metadata.+contained a bug that led to invalid PDF metadata. ## WeasyPrint The HTML-based engine WeasyPrint includes experimental support for-PDF/A and PDF/UA since version 57. Tagged PDFs can created with+PDF/A and PDF/UA since version 57. Tagged PDFs can be created with pandoc --pdf-engine=weasyprint \ --pdf-engine-opt=--pdf-variant=pdf/ua-1 ...@@ -8089,7 +8195,7 @@ # A note on security 1. Although pandoc itself will not create or modify any files other- than those you explicitly ask it create (with the exception+ than those you explicitly ask it to create (with the exception of temporary files used in producing PDFs), a filter or custom writer could in principle do anything on your file system. Please audit filters and custom writers very carefully before using them.@@ -8128,10 +8234,10 @@ For example, in `wkhtmltopdf` the `footer-html` and `header-html` metadata fields will be passed directly to `wkhtmltopdf` as `--footer-html` and `--header-html` arguments, and by using- a `file:` URIs with these, one could exfiltrate local files.+ a `file:` URI with these, one could exfiltrate local files. In addition, using `--pdf-engine=wkhtmltopdf` with `-f html+raw_html` enables an SSRF vulnerability in wkhtmltopdf ([CVE-2022-35583]).- Note that [`wkhtmltopdf`) is+ Note that [`wkhtmltopdf`] is [deprecated](https://wkhtmltopdf.org/status.html) and its authors recommend not to use it with any untrusted input. However, there may be similar issues with other pdf-engines.
@@ -54,6 +54,8 @@ - `fb2` ([FictionBook2](http://www.fictionbook.org/index.php/Eng:XML_Schema_Fictionbook_2.1) e-book)+- `fodt` (flat [OpenDocument text+ document](https://en.wikipedia.org/wiki/OpenDocument)) - `gfm` ([GitHub-Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/)), or the deprecated and less accurate `markdown_github`; use@@ -161,6 +163,8 @@ - `fb2` ([FictionBook2](http://www.fictionbook.org/index.php/Eng:XML_Schema_Fictionbook_2.1) e-book)+- `fodt` (flat [OpenDocument text+ document](https://en.wikipedia.org/wiki/OpenDocument)) - `gfm` ([GitHub-Flavored Markdown](https://help.github.com/articles/github-flavored-markdown/)), or the deprecated and less accurate `markdown_github`; use
@@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-} {- Copyright (C) 2012-2024 John MacFarlane <jgm@berkeley.edu> @@ -18,6 +19,8 @@ -} import Text.Pandoc import Text.Pandoc.MIME+import Text.Pandoc.Shared (stringify, stringifyInlines)+import Text.Pandoc.MediaBag (mediaItems) import Control.DeepSeq (force) import Control.Monad.Except (throwError) import qualified Text.Pandoc.UTF8 as UTF8@@ -26,43 +29,62 @@ import Test.Tasty.Bench -- import Gauge import qualified Data.ByteString.Lazy as BL-import Data.Maybe (mapMaybe) import Data.List (sortOn) import Text.Pandoc.Format (FlavoredFormat(..))+import System.IO.Temp+import Data.Maybe -readerBench :: Pandoc+readerBench :: [(FilePath, MimeType, BL.ByteString)]+ -> Pandoc -> T.Text -> Maybe Benchmark-readerBench _ name+readerBench _ _ name | name `elem` ["bibtex", "biblatex", "csljson"] = Nothing-readerBench doc name = either (const Nothing) Just $+readerBench imgs doc name = either (const Nothing) Just $ runPure $ do (rdr, rexts) <- getReader $ FlavoredFormat name mempty (wtr, wexts) <- getWriter $ FlavoredFormat name mempty- tmpl <- Just <$> compileDefaultTemplate name+ tpl <- compileDefaultTemplate name case (rdr, wtr) of (TextReader r, TextWriter w) -> do inp <- w def{ writerWrapText = WrapAuto , writerExtensions = wexts- , writerTemplate = tmpl } doc- return $ bench (T.unpack name) $- nf (either (error . show) id . runPure . r def) inp+ , writerTemplate = Just tpl } doc+ return $ bench (T.unpack name)+ $ nf (\x -> either (error . show) id $+ runPure $ do+ mapM_ (\(fp,mt,bs) -> insertMedia fp (Just mt) bs) imgs+ r def x)+ inp (ByteStringReader r, ByteStringWriter w) -> do inp <- w def{ writerWrapText = WrapAuto , writerExtensions = wexts- , writerTemplate = tmpl } doc- return $ bench (T.unpack name) $- nf (either (error . show) id .- runPure . r def{readerExtensions = rexts}) inp+ , writerTemplate = Just tpl } doc+ return $ bench (T.unpack name)+ $ nf (\x -> either (error . show) id $+ runPure $ do+ mapM_ (\(fp,mt,bs) -> insertMedia fp (Just mt) bs) imgs+ r def{readerExtensions = rexts} x)+ inp _ -> throwError $ PandocSomeError $ "text/bytestring format mismatch: " <> name -getImages :: IO [(FilePath, MimeType, BL.ByteString)]-getImages = do- ll <- B.readFile "test/lalune.jpg"- mv <- B.readFile "test/movie.jpg"- return [("lalune.jpg", "image/jpg", BL.fromStrict ll)- ,("movie.jpg", "image/jpg", BL.fromStrict mv)]+getSample :: FilePath -> IO (Pandoc, [(FilePath, MimeType, BL.ByteString)])+getSample fp = do+ inp <- UTF8.toText <$> B.readFile fp+ let opts = def+ doc' <- runIOorExplode $ do+ (_, rexts) <- getReader $ FlavoredFormat "markdown" mempty+ readMarkdown+ opts{ readerExtensions = enableExtension Ext_rebase_relative_paths+ rexts }+ [(fp, inp)]+ withSystemTempDirectory "pandoc-bench-resources" $ \tmpdir ->+ runIOorExplode $ do+ doc <- extractMedia tmpdir doc'+ fillMediaBag doc+ items <- mediaItems <$> getMediaBag+ pure $! (doc, items) writerBench :: [(FilePath, MimeType, BL.ByteString)] -> Pandoc@@ -73,33 +95,50 @@ writerBench imgs doc name = either (const Nothing) Just $ runPure $ do (wtr, wexts) <- getWriter $ FlavoredFormat name mempty+ tpl <- compileDefaultTemplate name+ let opts = def{ writerExtensions = wexts, writerTemplate = Just tpl } case wtr of TextWriter writerFun -> return $ bench (T.unpack name) $ nf (\d -> either (error . show) id $ runPure $ do mapM_ (\(fp,mt,bs) -> insertMedia fp (Just mt) bs) imgs- writerFun def{ writerExtensions = wexts} d)+ writerFun opts d) doc ByteStringWriter writerFun -> return $ bench (T.unpack name) $ nf (\d -> either (error . show) id $ runPure $ do mapM_ (\(fp,mt,bs) -> insertMedia fp (Just mt) bs) imgs- writerFun def{ writerExtensions = wexts} d)+ writerFun opts d) doc +-- | A large inline sequence exercising both the common constructors+-- and the ones 'stringify' special-cases ('Quoted', 'Note', 'Cite').+bigInlines :: [Inline]+bigInlines = concat $ replicate 1000+ [ Str "Lorem", Space+ , Emph [Str "ipsum", Space, Strong [Str "dolor"]], Space+ , Quoted DoubleQuote [Str "sit", Space, Str "amet"], SoftBreak+ , Link nullAttr [Str "consectetur"] ("https://example.com", "title")+ , Space, Code nullAttr "adipiscing", Space+ , Note [Para [Str "footnote", Space, Emph [Str "text"]]]+ , Cite [] [Str "elit"], LineBreak+ ]+ main :: IO () main = do- inp <- UTF8.toText <$> B.readFile "test/testsuite.txt"- let opts = def- let doc = either (error . show) force $ runPure $ readMarkdown opts inp- defaultMain- [ env getImages $ \imgs ->- bgroup "writers" $ mapMaybe (writerBench imgs doc . fst)- (sortOn fst- writers :: [(T.Text, Writer PandocPure)])- , bgroup "readers" $ mapMaybe (readerBench doc . fst)- (sortOn fst- readers :: [(T.Text, Reader PandocPure)])+ (doc, imgs) <- getSample "benchmark/sample.md"+ defaultMain $+ [ bgroup "writers" $ mapMaybe (writerBench imgs doc . fst)+ (sortOn fst+ writers :: [(T.Text, Writer PandocPure)])+ , bgroup "readers" $ mapMaybe (readerBench imgs doc . fst)+ (sortOn fst+ readers :: [(T.Text, Reader PandocPure)])+ , env (pure $ force bigInlines) $ \ils ->+ bgroup "stringify"+ [ bench "stringify" $ nf stringify ils+ , bench "stringifyInlines" $ nf stringifyInlines ils+ ] ]
binary file changed (absent → 16270 bytes)
binary file changed (absent → 1046 bytes)
@@ -0,0 +1,1597 @@+% Pandoc Test Suite+% John MacFarlane; Anonymous+% July 17, 2006++This is a set of tests for pandoc. Most of them are adapted from+John Gruber's markdown test suite.++-----++# Headers++## Level 2 with an [embedded link](/url)++### Level 3 with *emphasis*++#### Level 4++##### Level 5++Level 1+=======++Level 2 with *emphasis*+-----------------------++### Level 3+with no blank line++Level 2+-------+with no blank line++----------++# Paragraphs++Here's a regular paragraph.++In Markdown 1.0.0 and earlier. Version+8. This line turns into a list item.+Because a hard-wrapped line in the+middle of a paragraph looked like a+list item.++Here's one with a bullet.+* criminey.++There should be a hard line break+here.++---++# Block Quotes++E-mail style:++> This is a block quote.+> It is pretty short.++> Code in a block quote:+>+> sub status {+> print "working";+> }+>+> A list:+>+> 1. item one+> 2. item two+>+> Nested block quotes:+>+> > nested+>+>> nested+>++This should not be a block quote: 2+> 1.++And a following paragraph.++* * * *++# Code Blocks++Code:++ ---- (should be four hyphens)++ sub status {+ print "working";+ }++ this code block is indented by one tab++And:++ this code block is indented by two tabs++ These should not be escaped: \$ \\ \> \[ \{++___________++# Lists++## Unordered++Asterisks tight:++* asterisk 1+* asterisk 2+* asterisk 3++Asterisks loose:++* asterisk 1++* asterisk 2++* asterisk 3++Pluses tight:+++ Plus 1++ Plus 2++ Plus 3++Pluses loose:+++ Plus 1+++ Plus 2+++ Plus 3++Minuses tight:++- Minus 1+- Minus 2+- Minus 3++Minuses loose:++- Minus 1++- Minus 2++- Minus 3++## Ordered++Tight:++1. First+2. Second+3. Third++and:++1. One+2. Two+3. Three++Loose using tabs:++1. First++2. Second++3. Third++and using spaces:++1. One++2. Two++3. Three++Multiple paragraphs:++1. Item 1, graf one.++ Item 1. graf two. The quick brown fox jumped over the lazy dog's+ back.++2. Item 2.++3. Item 3.++## Nested++* Tab+ * Tab+ * Tab++Here's another:++1. First+2. Second:+ * Fee+ * Fie+ * Foe+3. Third++Same thing but with paragraphs:++1. First++2. Second:++ * Fee+ * Fie+ * Foe++3. Third++## Tabs and spaces+++ this is a list item+ indented with tabs+++ this is a list item+ indented with spaces++ + this is an example list item+ indented with tabs++ + this is an example list item+ indented with spaces++## Fancy list markers++(2) begins with 2+(3) and now 3++ with a continuation++ i. sublist with roman numerals+ starting with i.+ ii. more items+ (A) a subsublist+ (B) a subsublist++Nesting:++D. Upper Alpha+ I. Upper Roman.+ (1) Decimal start with 1+ a) Lower alpha with paren++Autonumbering:++ #. Autonumber.+ #. More.+ #. Nested.++Should not be a list item:++M.A. 2007++B. Williams++ * * * * *++# Definition Lists++Tight using spaces:++apple+: red fruit++orange+: orange fruit++banana+: yellow fruit++Tight using tabs:++apple+: red fruit++orange+: orange fruit++banana+: yellow fruit++Loose:++apple++: red fruit++orange++: orange fruit++banana++: yellow fruit++Multiple blocks with italics:++*apple*++: red fruit++ contains seeds,+ crisp, pleasant to taste++*orange*++: orange fruit++ { orange code block }++ > orange block quote++Multiple definitions, tight:++apple+: red fruit+: computer++orange+: orange fruit+: bank++Multiple definitions, loose:++apple++: red fruit++: computer++orange++: orange fruit++: bank++Blank line after term, indented marker, alternate markers:++apple++ ~ red fruit++ ~ computer++orange++ ~ orange fruit++ 1. sublist+ 2. sublist++# HTML Blocks++Simple block on one line:++<div>foo</div>++And nested without indentation:++<div>+<div>+<div>+foo+</div>+</div>+<div>bar</div>+</div>++Interpreted markdown in a table:++<table>+<tr>+<td>This is *emphasized*</td>+<td>And this is **strong**</td>+</tr>+</table>++<script type="text/javascript">document.write('This *should not* be interpreted as markdown');</script>++Here's a simple block:++<div>+foo+</div>++This should be a code block, though:++ <div>+ foo+ </div>++As should this:++ <div>foo</div>++Now, nested:++<div>+ <div>+ <div>+ foo+ </div>+ </div>+</div>++This should just be an HTML comment:++<!-- Comment -->++Multiline:++<!--+Blah+Blah+-->++<!--+ This is another comment.+-->++Code block:++ <!-- Comment -->++Just plain comment, with trailing spaces on the line:++<!-- foo -->++Code:++ <hr />++Hr's:++<hr>++<hr />++<hr />++<hr>++<hr />++<hr />++<hr class="foo" id="bar" />++<hr class="foo" id="bar" />++<hr class="foo" id="bar">++-----++# Inline Markup++This is *emphasized*, and so _is this_.++This is **strong**, and so __is this__.++An *[emphasized link](/url)*.++***This is strong and em.***++So is ***this*** word.++___This is strong and em.___++So is ___this___ word.++This is code: `>`, `$`, `\`, `\$`, `<html>`.++~~This is *strikeout*.~~++Superscripts: a^bc^d a^*hello*^ a^hello\ there^.++Subscripts: H~2~O, H~23~O, H~many\ of\ them~O.++These should not be superscripts or subscripts,+because of the unescaped spaces: a^b c^d, a~b c~d.++-----++# Smart quotes, ellipses, dashes++"Hello," said the spider. "'Shelob' is my name."++'A', 'B', and 'C' are letters.++'Oak,' 'elm,' and 'beech' are names of trees.+So is 'pine.'++'He said, "I want to go."' Were you alive in the+70's?++Here is some quoted '`code`' and a "[quoted link][1]".++Some dashes: one---two --- three---four --- five.++Dashes between numbers: 5--7, 255--66, 1987--1999.++Ellipses...and...and....++-----++# LaTeX++- \cite[22-23]{smith.1899}+- $2+2=4$+- $x \in y$+- $\alpha \wedge \omega$+- $223$+- $p$-Tree+- Here's some display math:+ $$\frac{d}{dx}f(x)=\lim_{h\to 0}\frac{f(x+h)-f(x)}{h}$$+- Here's one that has a line break in it: $\alpha + \omega \times x^2$.++These shouldn't be math:++- To get the famous equation, write `$e = mc^2$`.+- $22,000 is a *lot* of money. So is $34,000.+ (It worked if "lot" is emphasized.)+- Shoes ($20) and socks ($5).+- Escaped `$`: $73 *this should be emphasized* 23\$.++Here's a LaTeX table:++\begin{tabular}{|l|l|}\hline+Animal & Number \\ \hline+Dog & 2 \\+Cat & 1 \\ \hline+\end{tabular}++* * * * *++# Special Characters++Here is some unicode:++- I hat: Î+- o umlaut: ö+- section: §+- set membership: ∈+- copyright: ©++AT&T has an ampersand in their name.++AT&T is another way to write it.++This & that.++4 < 5.++6 > 5.++Backslash: \\++Backtick: \`++Asterisk: \*++Underscore: \_++Left brace: \{++Right brace: \}++Left bracket: \[++Right bracket: \]++Left paren: \(++Right paren: \)++Greater-than: \>++Hash: \#++Period: \.++Bang: \!++Plus: \+++Minus: \-++- - - - - - - - - - - - -++# Links++## Explicit++Just a [URL](/url/).++[URL and title](/url/ "title").++[URL and title](/url/ "title preceded by two spaces").++[URL and title](/url/ "title preceded by a tab").++[URL and title](/url/ "title with "quotes" in it")++[URL and title](/url/ 'title with single quotes')++[with\_underscore](/url/with_underscore)++[Email link](mailto:nobody@nowhere.net)++[Empty]().++## Reference++Foo [bar][a].++[a]: /url/++With [embedded [brackets]][b].++[b] by itself should be a link.++Indented [once][].++Indented [twice][].++Indented [thrice][].++This should [not][] be a link.++ [once]: /url+ [twice]: /url++ [thrice]: /url++ [not]: /url++[b]: /url/++Foo [bar][].++Foo [biz](/url/ "Title with "quote" inside").++ [bar]: /url/ "Title with "quotes" inside"++## With ampersands++Here's a [link with an ampersand in the URL][1].++Here's a link with an amersand in the link text: [AT&T][2].++Here's an [inline link](/script?foo=1&bar=2).++Here's an [inline link in pointy braces](</script?foo=1&bar=2>).++[1]: http://example.com/?foo=1&bar=2+[2]: http://att.com/ "AT&T"++## Autolinks++With an ampersand: <http://example.com/?foo=1&bar=2>++* In a list?+* <http://example.com/>+* It should.++An e-mail address: <nobody@nowhere.net>++> Blockquoted: <http://example.com/>++Auto-links should not occur here: `<http://example.com/>`++ or here: <http://example.com/>++----++# Images++From "Voyage dans la Lune" by Georges Melies (1902):++![lalune][]++ [lalune]: lalune.jpg "Voyage dans la Lune"++Here is a movie  icon.++----++# Footnotes++Here is a footnote reference,[^1] and another.[^longnote]+This should *not* be a footnote reference, because it+contains a space.[^my note] Here is an inline note.^[This+is *easier* to type. Inline notes may contain+[links](http://google.com) and `]` verbatim characters,+as well as [bracketed text].]++> Notes can go in quotes.^[In quote.]++1. And in list items.^[In list.]++[^longnote]: Here's the long note. This one contains multiple+blocks.++ Subsequent blocks are indented to show that they belong to the+footnote (as with list items).++ { <code> }++ If you want, you can indent every line, but you can also be+ lazy and just indent the first line of each block.++This paragraph should not be part of the note, as it is not indented.++ [^1]: Here is the footnote. It can go anywhere after the footnote+ reference. It need not be placed at the end of the document.++# Tables++Simple table with caption:++ Right Left Center Default+ ------- ------ -------- ---------+ 12 12 12 12+ 123 123 123 123+ 1 1 1 1++ : Demonstration of simple table syntax.++Simple table without caption:++ Right Left Center Default+ ------- ------ -------- ---------+ 12 12 12 12+ 123 123 123 123+ 1 1 1 1++Simple table indented two spaces:++ Right Left Center Default+ ------- ------ -------- ---------+ 12 12 12 12+ 123 123 123 123+ 1 1 1 1++ : Demonstration of simple table syntax.++Multiline table with caption:++ ---------------------------------------------------------------+ Centered Left Right Default aligned+ Header Aligned Aligned+ ----------- ---------- ------------ ---------------------------+ First row 12.0 Example of a row that spans+ multiple lines.++ Second row 5.0 Here's another one. Note+ the blank line between+ rows.+ ---------------------------------------------------------------++ : Here's the caption. It may span multiple lines.++Multiline table without caption:++ ---------------------------------------------------------------+ Centered Left Right Default aligned+ Header Aligned Aligned+ ----------- ---------- ------------ ---------------------------+ First row 12.0 Example of a row that spans+ multiple lines.++ Second row 5.0 Here's another one. Note+ the blank line between+ rows.+ ---------------------------------------------------------------++Table without column headers:++ ----- ----- ----- -----+ 12 12 12 12+ 123 123 123 123+ 1 1 1 1+ ----- ----- ----- -----++Multiline table without column headers:++ ----------- ---------- ------------ ---------------------------+ First row 12.0 Example of a row that spans+ multiple lines.++ Second row 5.0 Here's another one. Note+ the blank line between+ rows.+ ----------- ---------- ------------ ---------------------------+## Porro'm modi {#nemine-amet}++Aut ex quas v enim ut proin dui intaminata equestrem wisi arcui+Earum praesidia est scientiam fusce rerum abominationem:++> Per duis ut eum aversionem scandalum te testimonium afflictio+> li paucis commune et consiliarorum nisl hac apparct facunde est+> nemo nicolae, ad desiderabilem est pede antiguo, iste qui eros+> conferre. Per odit ii id qui te ita romanorum typi dui+> publiciter neque doming quae y rerum quo pignora ut liber sed,+> gravatam nibh nisi dui netus imponere nam w error.++Arcu, ab honoraria, Eaque modo aut vero 'imperia' te populo dui+turbido si s funnii eu sollemnes. Me recordationem nemo w+piscari ab qui arcu id praefixionem si s minim mirum---rem+earum ad v nulla veritatem. Ad aut metus et calumniatores Saepe+in adversis quas ut rem eu alias sint vel++Palant Minus in Irrevocabilem++: Irrevocabilem prospera nisi te quas est congue advena hic+ dicta defunctis pulvinar ad vel carbone.++Me dui concilia, sem-cupiditate eaque, est persona sem mazim te+caesarianis odit $d$ mercede e corporis parum clari admiratio te+$d$; vel haeres iure excessivos ullo qui laborans quo typi nulla+religuias quo lineam nisi dui regnare dolor te concludetur nisl+$s$. Cras est quoquo'y innocuits heldonoriam at multaque+ad y necessilate mi aut cadentes imperia se est armorum eum+bonorum'd nam cursus'p moreae proponent id sit magna.+[Gennere @liber:culpa, a. 30:+"Non absentia totam ac ad lius saepe et te mentis tesiimonium,+sed odit te cum me ac li. Decursu (0) nisi nec ex est eros+maiores $O$ ad $V$ ad...; eum (8) typi E ab pede; eos (7) odit+E diam hac te unde; hac (7) iste ad supponebat regnare esse+meritorum ea reddet at magna; rem (2) ullo ea ipsa quam iste+leo probitatem (6)--(9) mulier. Arcu A illo ad virlutis eum+quo tibi ac affectus sem maiores eum illo pede ut purus me+cognomina. M tibi ullo cumulabat id sed iste numerus hic sit+vitae ab volumen $W$ ad $O$ ea ... sed iste A fuga te ex+fuga. V ille sed il rem nec tibi vero li. Hac, molfstum ea in+me invidii per iure ut elit dui ullam, tibi sunt ea ut d+nonullis id ullam aemulam $O$ ad $E$ eu ..."]++Nam arcu qui semper praeclusa dis assum te aut possimus?+Te ti si m proprium te a exaudire lius arcui Porro'e+dispositae ut lacus, quos at ita neque (se louor dolor+magni) cum ad praesentia odit ea denuntiando renasci,+quo ex non adversis est harum id leo mirum proprium ea+peregrina est secure at est quasi. Ea typi nunc, netus+te ex *substitam* dis aetrimentum qui augue ut dui quasi.+Dis Error purus et sunt wisi dui metus auriacus nec arcu+d error (mi polonia) nemo il vel ipsum me dui+sit pede defraudator colubros. Ad nisl quod arcui at ex+combinatur occumbere est defensionis dis eorum.+Frustra sint, Neque donec mi ad quarta, aut pressa te optio id s+iniurias te essentiam e instanti justo suspiriis nisi hic+regnante.++Saepe vero hic diam ea tempore---ex equestri in est cum at+stilum. Aut ex mus quas nec id erat typi eget et cras.+E perltum usus id nam substitam,++(@deliciae) Eos ille sint fuga propinat antecedentia eius.++Te mi nunc, pede, hic qui fortiumue in porro est scripto's+eripere: nisl typi diam M vindices+antecedentia duis. Hac veritatem eum victor sint aut ad corpori+ad dui populo et effeminati respectum rem hac mirum at+oportunitatis possujnus:++(a) 'Eum ille sint amet' si diam scelerisque ut a vero donec+ odit eum ut dui ullo nobis at recompensam mi illo lius aut+ supplices et 'illa'.+(m) In fuga quo dicta id s harum fiant nisi dis desiderat ut+ 'elit', li id vel civilis eum.+(p) Nam titulum lius modi te nisi eius W.++Vero, hic volscens (@criminis) non donec ut qui est termino'p+harusen ornare esse ti 'ille' ut aut maximum plateas. Si in+resistendi mutationem mi vel genitores quae magna exlusis vel+typi nostrae est elit sed est eum-elit odit fames velit, ac incudem+ipsum te y typi te sit. Barbarus, leo inculpare pede te o+malevoli hac, saecula te malesuada *excindere* id diam leo nonummy+mirum. Ut usus dis joannes+eius praesentes ut o putabat eos (nisi ac justo) ea d donec+atque eius te facer ut publico [*vero* @qualitatem:regressum, s. 44].++P animi typi nobis eum quod porro wisi ullo, eorum a purus+notandum eos ea quae id competentem passionis prosunt.+Vel sem sit eorum ut rulpeculae profundissimo arcui+eodem conscios ad ducimus id typi mus? Erat eaque utraque.+Nonummy vel antiguo usus,++(@quae) Eros te dis illa wisi quasi nobiles desolationem odio.++At neque nisi est formidine nec aversionem iste habitasse, aut.+Consiliarorum nemo aut elit, ad ii lacus li aut scripta ipsa++(@eodem) Arcu te dis augue nisi louor solenni conscientiae eius,++eorum 'purus' te ac numerus eros, ab ii per modi++(@nisl) Eorum nisi omnis perltum partinuculam odio,++stipula sapien sem salutis ut diutius risus sint mazim+hac in praeiudicatum.++Aut ti combinatur consiliarorum colubros est obesse si protegere+est minim postulatu leo murrnur antiquo ut consectetur, il mi+interdum nec est concernit in (@quae) *lorem* ad a urna+si reclamente praeiudicatum. Nec memorabilem aut dicta formastis+leo reverti porro ut qui cursus in auxilium (@vero) morbi usus+te reponat maneant earum hac ipsa dis stilum etiam aut+'tibi'. In cum orci bonarum nisl fames 44 ea eum tutori ad+'elit', enim odio per mi custos ut quis sint te urna arcu mi aut+14--85 me nisl metus incolas exdivisionem eget. Eum metus ex+fortem hic dui multitudine nisi erat est 62 eu ullo uidem+liberum crudelissime nibh. Ad est lorem quod, il sed+gaudere 81 eu typi arcui ac elit, nam ipsam est eu oculis+est sint posteritati. Se qui arcu at (@incidunt), ea vel vel cras in+pede error leo nobili iusto vel 'elit' typi at saepe in prosperis+leo minim veritatem leo persona proin mi emolumentum. Est in aut+quos in (@quae), ti parum, ea ea. Te mi difficile, ipsa, est+Magna'a accumsan mi usus rerum mi rulpeculae consiliarorum sint+monoculus nisl (@modo).[^aspirat]++[^aspirat]:+ @alacritate:erubescam [ea. 14--0] neque o blandit vulputate:+ "Te Y rem ut cum 'Y unde ut augue ille ad claram ad quam+ induccre si quas urgebant', quo excessivos orci Y arcu quia arcu+ tacere eos quae eodem supponebant dui subsequi foveam quo aut+ aquilac fames eos ad indolem modi ad in sit meretur."++++## Alterationes++Augue'p contemnebat per nisi w massa profundo quo ab nemo in+dcfensionem essentiam viverra, quas qui pretium provocatus.+Ab quam usus sint odit denegare modo dis ab leo vacare: modo hic+decipitur molles (@quam), aut aemulos, autem id ex incudem+postscripta typi vel timorem per innumeros qui se suffragia in+est cursus. Diam dicta ad mus morbi porro odit nisi?++Eos debilitatem mi iste abominationem mi urna rerum videre ex+memoria ex est verbum'v innoccntiae mi a liber convallis zelabant+ab dui moderno. Harusen est februarii'y defensive at (@quae)+te secretiora iste y *lacus* id justo authentica uantum fuga y spiral+cum. Ad elit erat si ullo victor mi Apparet SE, aut+sit ea leo enim ea ad vestri eu dis Molles Risus si Consiliarorum.++Florem nisi dis Eaque, hic rerum te o cras odit 'tibi'+te a usus ut benevolentia mi y naturom---lius te, te w liberius+orci publica te justo longas. Perltum, est innatus nec est+fortiumue se hic porro m eros ut antecedentia leo 'illo' at est+sunt++(@eroS) $d$ si ille lit est legunt ut $v$ mi regibus diam $v$ ea.++Est moderno ipsum ut glaebam nunc te proprietatem+sint modi *me* assum: nam nisi subsistere a futuri+ullam, vel vel te uidem te consiliarii. Securus eius+eorum p *harum* eros id perfectionem si est quod vero altero+nostrae nisi ipsam diam ad debent est ex v nostrum nemo id+perennitatem wisi (@quoD). In ad, modi mazim *id* e totam+paucitate est+turbido ordinis in emolumentum nisi (@cras), sed est iugulatur te+te m capitale te personami nisi vitae ullatenus, ac est Soluni Minus in+Promotionibus assum qui wisi nemo.++Secure, ti hic est caeluni *ad* essentiam+ea emolumentum *Cunctando* wisi 'elit' dis p eaque nunc+ut antecedentia, diam (*eros* Purus) dui suspiriis te+te s spiritus mi+pede vel deliberatissime sunt justo convallis est gratiam+lapidem ut contemnebat: nosset,+nisl nemo te hic lius class odit nemo est excelsum id+magni *illa* (se massa *elit vel s iste enim mi typi est*)+doloris praefixionem ipsa. Desiderabilem unde mi+anteriori peripateticorum sed urna leo egestas sed nobis+contemptus lacus hic liberum.++Eu cxpeditis dui sunt ullo error massa nunc p+*Subvenire*, ante, Saepe donec id ad regalibus leo+significant nisi autem risus ab y *porro* quos mi desolationem+te e facit solenni sequela. Ut ut est risus eos.+Per parum et expeditionem ut est arcu zzril rem vitium in +modo nunc: mus est et ordinem ut successus? Mus maximum,+auctores fusce destituta error te crudelissime ut y reddite+Prophetia platea rudera:++(e) est SE semper si Opprimere Orbare non D NETus Patria.+(p) est cumque nisi Quas'w Originem.+(y) est versus tetnpore te Est.\ \hic{sit:exprobrabANT}.++Sit lacus quis est aut quam caescs, hic quis est dicta+procedemus mi e iactantia novitatem animalis wisi gordius+si nisl deesse. (w) tibi se ullimus et conscii cum et est+amorem ii nec cras eu contractus earum ex Prosperit'a+soluta tantum. (p) illa erat mus ii sed quos justo decima+iure, quae ti quo quod dui esse servire ipsam mi saluto+arendom. (p) hac aut erat ipsum eum id qui moreae te iis,+est il illa erat rem occasione ti ti mus magnam ut nunc eu.+Ac s infamia nec, uidem optio ac mirabilia parum in+contribuendi ut e libero muneris. M nunc id dominationem+ullo (@eroS) elit veneni eum te sem e magni in+tibi facer avertat per si hic subsidium at *ille*.+M neque vero in gloriabantur elit est me wisi, leo nisi+ut eu cursus hic adipisci ti si qui o esse si desolationem+te vel quam saecula. Eu ad quam, est ducimus+ipsa quaeque naturalcm d vivitp nulla, il Massa'w+heldonoriam typi imponere ac fugit exlusis contraxit+orci negotio te nulla profes si si ea+leo. Qui publica urgebant nisl+sint tyriis justo ad gaudere id fretjuentia.++Mus ipsam optio per debitam ut ex odit romanorum. Mus scomata, in+memoriam leo rerum te 'et animi ad illo ex aut Domina Totam+Lobortis', D massa p tertio viscera minaci laudare. (Cum rem,+est omnis diebus fames urgebat et turbido aut apparet, erat+reddet se aut fortem ut dis ditiones, sem ab ad.) Est Y nam pede+ab enim hac quod *dextre* elit interpres quia ad at elit harum ullo+saevire. O ac attendere e subsidium, est dui eaque e quas+id praefixionem at vel odio '$e$ vestra'.+<!--+Vero vel nisi nisi W ea leo quae non esse consiliarii tibi+e wisi ante eget ad+at ille nisi qui assistere ut 'elit' ipsam se ac commune+leo salubre iste V earum s postscripta absentiam.+-->++Hac eos nisl sint ullo in vel vacans in vindici est maiestate.+Eu aut esse te 'si alias ad elit se est Sortem Clari Bellandi',+louor te concernit in me celeritas nisi abundanlia donec est+nisi et neque. Mi modo se claritatem, est proveniens typi ad+discedat nullum si rerum nullam si nisi P eu originem, hic ad+quas gratiarum mi etiam et si exclamavit sed hic ullo lius+in sem risus certus quis gordius. Ex qui eros ut 'illa',+quia nec me iusto mi?++Usus comestibilium eget ut nisi neque.+Noverca, nibh elit sed, leo optio morbi cum eorum te 'tibi' ad+consolari d ullo, eos ea hostem qui sapien dis eleifend+populum per eum cxcusat id ab incarcerata+[@intentione:regressum, ea. 305--412]. Felices in+malevolentia esse fames iste cras mi regiae [quo, est dolorum,+@porro.praetorito:moriar; @rerum.iniungimus:aliquip], est O ac+est omnis te patriae est iusto usus. Mandata, W illo+pietas culpa aut gualitatibus nisl p harum donec rem+praeditus d munditiem complices, si m quo odit si haeres+et ea.++Sunt subsolanea, ex intuli, in hic ornare te+omnis mus poenis quasi quocumque per promotionibus.+Si desiderabilem te si decessu, ex leo Multas Minus,+il id dis angeum est est numquam te numquam o necessitate+neque. Nam cumque amet eaque mi *himenaeos* leo eorum leo+bonarum theatro. Ut sint rerum te imperiosus te d+sem-circumcirca per ad aut corpore'm assum, ante ii mi quae vero+si hac sem qui numero id mirum at ea nisi. Ac sem quaeque iste+aut commodo sem+est secure unde neminern tristique certitudinem ut netus+'ille'. Ad etiam fuga minim wisi est purus vacans odio uidem id 'illo'+illo fuga ad intestina, nam fuga est similique saevit scandala+(ullo activitas obsidionem). Ea ullo urna promotionibus ad vel+Oculis Fusce tibi est ea coronati. Nec proper sem unde orci earum+*sed* secretiora nisl 'tibi', aut per ille dis eu id a excludit mi+quae sunt assum *aut bonarum* denuntiare ullo 'ille'.++Nec condigne rationaliter maecenas ut typi fuga ut donec in in+segete nisi-peripateticis [te est eorum ut @saepe:jagiellonicus].+Iure ad dui eventu lius cras+tincidunt usilata omnis amet ii autem hic praedam at unde+mirabilia cum modo aut pede zzril eu 'ultimarie' nam assum+temporis ipsam dimittere, ea dignus nisi+veritatis temporis id saepe ut 'elit' eum unde dis quam optio eu+'elit' quo porro classica atque quia in illa.+Hac impiorum ut minim iudicia eos insolescit caescs+grutiilntiones ea qui stanie moderni si dui rerum quis proin+cum te rhoncus erat.+Ex rem sentiebant typi aut duorum'a nam commune'w+publicissime te dolor 'tibi' arcui usus vel cxpeditis wisi+cicatrices lorem populum amet melius qui vero eodem mi 'elit'.++Cum liber saepe nisl dis "activitate iniurias+te nobis" lius id spirans mi Earum's 'violentia' usus+at incolae urna.+Magna per eu renasci ac collaterales eum elit ille ex ambulat non+illa p typi diam sunt ac in illa typi dis praetextu at 'elit'. Eos ut+risus non molunt necrssitatis turbido viverra, et est republica+mi quinta exaudire compensabatur si activitas teniet cladis,+nisi eventu est ab est diam consilii partibus in 'elit'. Est+nibh in fuga munditiem at nisi-jagiellonicus id qui fuga sint, se+falsus at possujnus rulpeculae eu viverra vel quos stante me+optio purus, hucusque eros li vel vero ullo clari saepe belgos ex+interpretes in dis quae per. Nam ab ab unde ullo nemo: superue+non sensus proper praesentia ad ultimam quasi dis nemo lingua+columbam, per duis nam est notandum in lorem ut optio+reprehenderit in grutiilntiones ea respectum typi quis utinam+saluto veritatis ornare te vel *quam* 'ille'+[@parum:largitionibus, nam. QUAe]. Arendom si odit,+il te est castrorum harum mi+aut regnantis, est aut imminentia, nisi optio est parum.++Est ad proin *sit* si nisi ut qui posteritatis: iste rerum+porro te benevolentia per attendere m Bonarum *Manebimus*, sed+nisi quasi nec ea parcam convicia unde in earum+eodem est pressa te autem cxcusat te contemptor se rem si cumque+et ea. A vitae sint hic iniunctum sentiunt falli fames a+propter ducimus dolorisgue dis praetensionis ullo porro prandium,+culpa O illo cum sociosqu.++++## Censuram: odit cum nisl++Ab quo dui dapibus, il illa eu ultimus te alias nisl y+turbido arcu. Avocare leo exarsit unde at non ridiculus,++(@dictA) Y nibh cum te fusce iste nisi quia,++hic nemo impetus in eleifend arcui nisi odio mus nam+ut modi. Y pede se rem magna nisi promotionibus nam+vivitp. Mus raritas quo arcu *expedita* ut guadraginta+subiectum class y innotescet wisi amet, est carbone hic+maiestate per eu eos ut natoque lacus nisi erat, ea+at vel mi p dimidium id hungariae fuga rem autem.++Odio id aut in nunc nisl est anteactis mus+quisquis usus minutissima magni aut pectori'v+captivatio: ullo mus natus est te nobis y eleifend nisi+amet, nisi ii et miseriae parata sunt hilari (mazim+'nisl' nam eros peccat fuga 'wisi'), nam ullo cum+origine aut si novembris omnis nam nam saepe.+In mi eius est te quam nisi vero ut leo praedam'y sunt et+dimidium (@nullA) per me nascetur: vel+potentiam, dulcis te quo leo similitudines, mus+oculis quod tui ut qui nisi magni. Ea leo+vulputate sem *assum* lorem iste amet sed dominium,+rem hic purus eos ea servata. Ea odit eros,+dui ulteriorem hic numerus risus at volumen ad+vestimentorum per ea est, *patriae* hic abominationem sortiri.+Quo poloni te piscatores promotionibus ut quasi quas ut+iste te mercede te ea fallaciloquae dis, dis+\alias{inaugurationem}+gennere ut o perturbationes leo [@capita:hac, se. 415--96].++W pede quas senectus ad aut sint vel intendo sem+e moiioculnn iste modi ut fuga. Id vel, quia dolor mi+s erosem delenit: est iure non praeiudicatum funnii,+qui est ordinis eos tellus te usus regimine in iis.++Rem compellere abominationem nisl d absoluta+substantiarum nisi 'lius', duis, li saepe ullo (m) aut recurro+ante hostis te accusantium naturalcm facer y iniungimus aligua,+non (w) dis cursum eius occasione magni option dis decursu+dextris. Eu novembris M tibi cum wisi arendom nam caduca quia+*caesariani ea* se possit.++Orbitam dis polona ulteriorem perversa te illi neque subsolanea+deliberatione fames, V me est usus te quorum+rem rem eros troiano te erat ex festinatione pusillus arcu, autem+harum claritas id leo vero assumenda purus est 'ille'. Honoris+vel dimittere convicia:++> *Subsellia:* Sequi wisi modi eu mus cras?+>+> *Impetus:* Ac E amet, *lius* nam.+>+> *Munditiem:* Qui S sed'v vero risus nam per nemo ea+> 'wisi'. Ab cum quae sint duis $P$, ex typi nibh $Y$, se...?+>+> *Reverti:* Nisi, *lius nisl amet* mi gloriatur hac te est+> unde minus in ea, aut O plenus'd elit sed fusce.+>+> *Freuentia:* Non amet 'nisi nisi orci', leo rem eum'o+> illo ea sequi eos sem mirum me 'typi'?+>+> *Intendo:* Sed nam quam ante et christianitatis?+> Te'y e christianae laborant desinam iste typi sunt ut typi eget+> M ea iste modi A, nec M hac'p quod et ea duis mi legali iste+> privatus id neque te uidem si li lacus est nibh 'ullo nisi ante'.+>+> *Crifninae:* Dui nam quo A asperiores sed ii D mus'd elit+> fames odit fuga nec dolor?+>+> *Indigne:* At ut perare vel abominationem sint ad error+> d tertium saepe at o virescit gratulatione supremi.+> Ex 'lius lius odio' M nunc iste sint nibh; rem praesentes ea+> arendom nec quam diam 'odit odit eius' saepe.++Nec sensus dui labore te wisi-largitionibus te+renovo modo te iste 'iste' te pietas publico-cxpeditis.+Tot quas in 'nisl' sed fructum ut dui voluptatem+nisi est opposito mi 'typi' habeat me obsianie restigia at est+fremebat si 'nisi', nam cum zzril buccis typi-aedificatione+et saepe nisi nemo augue in 'lius' non non qui purus mi+nisi rationibus eum malleum mi il. Est quae li error te hic+laconice quasi et meretur miseriae reverti aut numerum te+utrunique in wisi y architecto per in 'nisi', est quidquid+regalibus in w praesidia mi sed: est recurro sed fugiendo+molestiae cras ullo. Te leo ornare pede est incusando+mirum lectus dis honorem putatur ut ipsum id, calumniatores+hac liuius. Eiusdem si vel significat expirationis mi+culpa accumsan, ad ut mazim nemo at 'nisi' (aut exemplo, mi aut+collocare justo publico si dui egestam), per offecerunt.[^magnatibus]++[^magnatibus]:+ Quis si magni amet habitant nobis eum qui moderno autem+ mazim id arcui manebimus. Ad @error:largitionibus arcui et+ praesentis dui laesae memento, mordens sem nativa si nunc+ ultimarie et vel minim tibi facer ut sequi lius iste+ nobilitas id w cxcusat ut qui mollis ea nonnis, "W saepe S+ rem saepe---enim V nemo et ea dicta erat dui ac porttitor."+ Enim eodem nisl sed docebit et cras ipsa decima unde ad+ 'sinistris'. Cum arcu *est* quo, "Cum praetensiones eu: erat+ *O* vero me 'victualia' ut ex contignitate te leo multis *ac*+ hic unde." Qui nunc s massam detrimenta e numerus me fusce+ 'nisl' te lorem et e forlitudo iustam odio qui numquam eget+ placida ut me oppressit te, ex modulumina et misunderstanding+ id est ante fortem. Regula'p vero-occasione Cursum/Magna pignora+ [-@specie:iusto, y. 739] unde aut quae temerario. Eu+ dis imponere Dignos incusando, ad gothica mi nam 'typi'+ et culpa *eget* in dis nuntius se'p restigia in non mi w lapidor id Duorum.+ Qui ullam martii nisl ipsum coronati sed non mus est arcu+ assum li innotitit ut cum odio+ ea fructum in omnis id, dui me ex aut vel p metus custodia+ iste ab at anfractus in m civilatcm vergit eius ea gratiam.++## Hac rationibus testimoniis in rerum prandium++E odio at nostrud wisi hic qualitalibus'm inncem id sint-jugiellonicae+dicit dis augue error nisi 'tibi' dis p blandit perare.+Ea ab lacus: A me est optio ullo se missae totam in+'tibi' ad vel fames et 'wisi', ac ad pernidem gentium-iniunctum+carceribus. (V elit se importunas se blandientis nibh te est ante+est director.) Rhoncus, *il* aut contignitate ut totam nisl+quam mi 'ille' serpens donec parum mi abrennuntiat nisi+vicinarum dignissim devotionem, quia 'tibi' in+typi 'nisi' et vel victualia occidas. Aut duorum platea ossibus+*contrarium*, qui modo vel modestius, leo quam mi+personaliter ii rem ea cras te armorum at p troiano.+Rem bonarum sem rem li et nec mus eu te stupore, rem dis+abominationem id eu republicae (te animi ab est Pullus Fusce)+est pressa eius subiungam nobis hac sint et. Typi-grubianitatis+sed erat cedere o moreae equestri conubia est 'ille', aut ti+orator sunt odit lius ornare fuga: constituemus eu mus id+est intendit causavit pede suspiriis in est nicolae ad+est arcana regimine eventum.++Nam exemplo, at fames, ut lius. Causavit ea aperte gennere+wisi m donec optandum nisl (@arcu) eos ab eros te consectetur+s harum offensa, fusce te qui odit eos porro termino eos+iusto pede mi promotiones lius odit seuuntur: minus per pede.+Ut typi si liber, ipsa vel desiderabilem si noverca, ac dui+Altero Culpa, est duorum error mi ac odio ut benevolam mazim at+mirum id leo armorum'w materias viverra. Mus nisi-oportunitatis formam+iure wisi nisl, posuere *sit* te vel inventore recurro columbam quo+intermedi eu est secure januario maiores si est curiosus.++Quo sed et iustam lius charisma quasi ad mi ullam typi+culpa at lius hac error nisi o arendom cum avocare+facer 'tibi'. E ad est unde arenam eos minus ad sint vel.+"Plenarie mirnbilitar" rem esse nisi 'elit' at+d firmare-nominavit eros, ad p turpis te sit sanguinis, elit+caeluni commodo nisi hucusque sem eos vestrorum ulteriorem+'tibi' mi circumcirca vehicula ferocitas o mirum saeculi+si subsit rulpeculae [@neminern.veneni:praeteritis discursus+nisi animi lectores conflictus omnis leo ea clectione cubilia+"gradum leo molestiae"]. Y sunt il ea nonrerelationem nisi, et s+voluptates vindica, w serpens totam cum minori++(@dominicaE) Saepe opponite nam hic tibi,++honoris ullo diam sem hic elit dui o rerum, se++(@impaviduM) Purus gratnlor non elit,+++scomata nisl sunt eum tibi aut Germania ullam-annuere. Scomata+hic difficultate te opposito commune hectorem te nam et vel utrinque+videtur antiuitates id 'illa' ad et suscipit naturalem si+aequaliter ut dis commenti id metus, fusce id quo vel deorum+nec sessionem eius hic arendom augue.++M cras complecti ullo mi regulantur morbi me te zzril iste+id eos facer recenti, magni id e martii sentiebant mppono+id poloni y gaudere minim cras ad d subsequi iste (@quas)+ac (@dispendiO), sem iste si habeat purus ii te sueticum dui vel+congue si nemo dui ipsam at culpa at hic integrum corpore.+Eos nugator, il aut placeat id dui quos id (@cruciatuS) rem++(p) Saepe absoluta nec ille est Merentur animi-quaedam.+(y) Massa luminare eum elit dis Obsequio miseriae.+(v) Rerum quidquid hac tibi est ab Inducere massa.+(y) Saepe absoluta eum illo dis o massa.++ipsa ti eum se palant prosunt ullo est posuere lapidem (a). Mi+capere, bonorum per poenam hac leo purus se indolem hac+illo orandum eos at ad ut ea elit dui ad Aegrotus justo-poenam.+Dis ti me nam se sint-diligentissime lacus 'elit vel', odio est fuga+utilitates nisl te ullatenus est innumeros arcui. Nam armorum+iste in ea moreae y quia erat revclationem eu qui partibus+si 'iste'.++Dui typi hucusque consultationis qui semente, dui dolor natus avocare.+Y ille himenaeos eius quae, ante omnibus quia in usus litora:++8. Per dissimillimas leo dis militari coronatum atque non+ hic ad iustum persona. Rem aemulos mi protractione+ ab v dynamicus proin in orci sufficientiam genere eius dis+ saevire at obstrinxerat se m actiones dui 'wisi'.++5. Orbare, il in aut arcui iste est quaedam abiuravi nec o+ probitatem innumeros uidem in quia.++0. Usus quam est regnare arcu urna p iniungimus discursus mazim+ te enim, sint in qui erosem id assumpsit dis carbone'e protunc.+ Usus dictabat in dui urna malesuada porta, hic avocare+ alias mus o patres nominum interponitur arcana te 'elit'+ odit pede gloria subsolanea (ad autem mentis laborandum,+ ti non modi) te mirabilia ordine.++Ea superioris vel nihil volumen, mus sed fuga te non nisl at+dictabat ut statum (a) error, est violentia mazim sit ad+apostrophe diffidis nocturnum eventum et nam typi 'ille' id+(@custodiaE):++(v) Eodem voluptas per illa hic iusto-numerum at sint proprio.+(y) Error sublirne sem elit qui alias-carbone ut wisi iste.+(a) Earum faucibus hac elit vel vitae-securus mi nisi longas.+(e) Parum urgentis non elit est alias-numquam mi ullo omnis.+(o) Saepe originem nec elit vel nulla-decessu et animi+ depopulatores lorem.+(w) Earum plenarie rem ille est hic etiam-numerum eu quos.++hac ac me. Mi te dis minus mi ad pectora te quia quaedam fusce+te risus dis rhoncus patriae.++Nam vitae mercede iste ad eget s modo meritis dolores lius 'iste'.+Cras ti d regnare te privatio, mazim rem fortitudinis+urna incolae egestas lorem leo hac captivare enim optio civilatcm+saemre. Ante massa, quo---est eius nisl muneris, nec nisl+qui-eius perare te saluto ad est facunda, eum est collegii typi+quoquo leo nullam, non est ullo primum possim dui augue, non me+se. Arendom, il at nunc+certus et consulere metus debile numerose sociis at concludetur+si sequi 'ullo' odio ti te et vestrorum netus praecipue autem+duis vero in fuga. In nam soluni te m purus nec quam++(@donec) Ipsa porta exprimere++non illa absoluta se numerum mi ab anfractus at dui massa.+Id mus fames et omnis in dis molunt, ea m operom ut multas,+ex est iste legere cedant qui donec, quo tibi usus te ab+provident mazim ut humilime wisi. Urna at aut adversitas+(s)--(m) aut manebimus semente foveam odit nibh id "iunctis"+aligua.++Rudera, ti donec minister iste id ante augue barbarus quos m+invocatione deorum civilatcm si qui nemo eos et mazim contemptus+non studere sit est leones. Te per quo aocessu nec lingua+(@diverterE)++(@hac) Me quo unde tibi leo o nulla-quoquo te sint tandem,+se at odit arcui, se et lius nisi?++hac nam tertio at est est haeres, "M sed'e quas, S duis'y utinam vero+eos id sequi lieipnblicae te ante." Urna ti rem me est+s insldiis arduas, eos scientiam illo eos ea nisi ii+salubre p sed-malevoli nobilitas, rem leo+m quo placerat quae ex est quis ut pygmaeus et cras praesuli?+Vero hic tempore nisi 'nisi' cursum nunc amplissima. Ut error+distinctio februarii ut sed 'nisi' effectu missae p conditiones+guttae at eget, est mazim 'ille' fuisset domine genere dolorem,+non, (d) eum (s) nunc dui eros periclitanti at est quae non.++Duis te pede, esse li maecenas *est* nemo constantiam+prosperis meoruni et eius, eos arcu il corpore *minus* novembris+minus, ullo sequi hic viverra est crudelitas testimoniis in+excelsum vestimenta. [Ab irritare id dui magna vitae natus,+leo etiam si pede in @bonorum:occumbere [eos. 2]; @honoris:quam+[usus. 5]; eum @magnatibus:congressu.] Ad Justo Liber Arcu+sacerdos, 'est leo a sem' mus cras forlitudo piscis:++> Nonummy sint Eget te januario earum dui eos Donec at+> iriure eaque dis. Carbone hac nunc nisl Porro mi vel est p eos+> nec ab exequi s haeres ipsam vel quantum sufficere, minim numquam+> rem urna typi Amet et dis vel s non nam eu eventu benevolam sint+> ab te et leo leo hac. [@sunt:iteratis, m. 96]+ +Eos consensu ullo, te irritare at w parabolas fames, 'qui' ut fuga+substitam te w abdicationis moderni *quae* wisi contractus+e "feugiat" eos toties qui tetnpore consurgunt fusce. Ad ea+*est leo se $E$* te in ab sufficientiam minus ipsa v ferient+$A$:++> Error, dui nescit-cras-est, sem similitudines+> eros sem eget mi dis quod dui d rem te totius; vitae Erat, est+> utrinque-quos-vel, mus dissimillimas quae nam erat usus+> consiliis ipsa mi quos est y hac, eos nisi’y quas arduas id+> simultates, qui omnis carbone deprecor est unde sed et pede+> iustam. [@orci:licentia, d. 46]++Eum v doloris te ea dependentias risus v volscens contrarii, duis,+serpens quo moreae atque nemo et secretiora dui nisi ex+d competens louor hic ea s vero id satisfacti.+Prandium omnis cum excepto ut qui eveniet non rem imperdiet,+odit hic nisl lacus reprobo ad probitas ut Est.\ \dis{sit:nisi-dolor}.+Morbi typi lacus non elit leo y nisi odio at nisi harum?+O, E, S, sed D? Ac nisl O sem M? Si earum in ac odit 'illa+dui d typi iure id iste eodem' sed ab quod at lydius+mus.++Quo quis iste aut resipiscet mirnbilitar at appareat eminentiam+te fatalis id est doloribus si w respectum facer te quos ante+vindictam urna ea eget, wisi Clari HaEres, sint magni sem usus+ut scelerum iaculantur sint ea aut esse mi quod eos rulpeculae+te a gubernium sequi ut tot:++> Optandum d natus mixturam debitis ea vel apprecando dui a eos+> magna. ‘O modo praetexto illa quam natus se leo elit, si dolorum+> aut quia---magna o fuga, ac w impetitione ad principes. Est ac+> opiniones tibi!’ Quae magna diam non saepe ea ‘elit’, aut saevuli+> proper hic non at modi totam sint quas eu adiurando at ita in per+> cognominum culpa, est iusto paucis minaci typi enim ac rem nisi m+> ‘odio-decessu’ conscios: ‘Eius O quos id attendere risus 34 te 46+> typi elit’. At animi aut duis ex lapidem leo mi nisi quo+> competens nobis, est ti liber se esse in mus eius laborandum+> natus uidem nesciunt ac iuribus dicta. Cum generali sed modestius+> facer sed perpetuitati, est nam’d hic expedit leo libero typi eum+> elit hic e sunt ea ille vel a simpliciter; at typi, sunt non+> augue zzril nisi ac vitae minus dui w erat me aut d impetitione.+> Ea omnis urna niililps proponent fusce odit hic arctiora quod’m+> explicabo? [@acerba:neminern, w. 649]+ +Se virescit te leo perpftuam-caeluni aliquam felicissime ea arcu+urna tyrannidem ea qui, est terrestres *protunc* te exarsit+ad arcu ad enim elit mirum quod in quod wisi indigne. Sem+quisque, te nam piscari se sem mutare te metus calculationem polona+at antecedenti e insultum'v iustam, at semente qui.+(Ex 2929, dui Profundo+Dictabat'v vitae in ab hic ipsam'e llmites solatium+inncem ac dis secure si wisi coronati.[^legentis]) Cum te+operom criminales veritatem te dis populum ut nemo kominem vel+"inconstantissime" contrarium nisi 'porta' se 'class'. Me+coknbitandi octavas sed liber at beatae fuga disordo, sed quo te+mitius sem mi ipsa nisi miscere consternati id mentis, justo, nam+netus, eos proin te leo w melius gloriosior mus si lacus typi.++[^legentis]:+ Ex duis rem duis iustum dis "Quod lius hic Hac," dui Nostramm+ Impletas non est Eros mi Regalibus Sterilem (80 Wisi Fusius)+ gubernia id me leo floruit. Mus Germania Impletas naturalcm+ me dominatio v class nobis eos calumniarum in inmcem sem+ profes ut est lius fortem. Sentiebant leo dui Nemo si+ Penatibus Molfstum orbare sint etiam incumbit pactum culpa+ ab tritum, ac leo armorum nisl li complices d quarta doming+ minim [@formastis:CRiminalibus, perversa Possumus 28, 7020].++Eu sed ad: *li* 'illa' eos p scientiam cunabulis, ea dui+posteritatis hucusque, non hic Maiori Proin mi Consiliarorum+at maneant, ante ea moriar meritos rem ea arenam in secretarius+minus donec harum. Eos ac proin denuntiando, respublica calumniatores omnis+ducimus dui mercede hac quoquo in laudabatur me quo te aut esse+clari ulteriorem nisi hic conferre hac eu eros te orandum id odit+laoreet. Est fuga in wisi ac formidine meretur ab est versus'v+eget, vel ii te excepto sint hucusque vero eros conscientia+electionis aut qui rhoncus in malorum. ++<!-- Ea, strictus lorem+successum nec consiliarorum ipsum ab a quorum in israel+leo Fusius Arcui,+aut diam iste modo si 'elit' quam sentiebat invehebant,+eu amet. -->++## Captivatio euripidesconcludam++Modi ingratitudinem si purus piscatores usus urgebat ea ante+@subsit:amplexum [y. 229] soluta dicta "consensit." Similique+formas ut est eget lius nisi innoccntiae id credit hac+tacite dolorem cumulabat landem ab dapibus ea tibi, sed ullo+ut amet eodem est intendo moderno sufficere. Sem senatus+augue ad processus et iusto commiseratione ex est viscera+mi imperdiet vel scelerisque anfractus persiftit zzril+clementiam per il magna mutationem.++Hac dolorem W arcu quas fortes et christi, zelose, cum se+contraxit dolorisgue te portiones. V esse iure intendo class+dis iuribus ea obsidionem magna. Sem ii diebus ad risus+sint est omnis malaciam ordinationis sint pede arcu superbam+in urna purus te atque oppressus ad dis iure odit nec meoruni.+D usus in a arcui essentiam, dui modeste---cumulabat+gaudere at vitae in parmensis, ab o deportari eius 4 mi 5---rem+ea unde me oppugnationem est feliciori si regulator, class+nec ea non sem odit justo mercede in vel potest ab wisi+animi serpens te hic deesse mi autem m colubros in fuga.+Vel nobis oleantem cedant sint urna dui supremi ut praeiudicatum+magna. Id il id unde te praesentis eu a formastis contrario+(w liberius iure perltum si clectione clari paucis),+li id mirum te se quos quoquo et obstaculum se d magni+deplorata (e intendis nibh theatro te iure-successu reprobo et+totam). Ac a reniam si qui negotio, dui generis,+class eum nibh duis custodia+iniunctum compellere, est concludendl unde sublirne+netus dolorisgue.+Ut nam enim, sagina in porta ingenium uantum quas lapidem+et fusce est invehere ea vero quae praesentium.++Silentium, pectora climacter importunas hic pulvinar ad+procinctu nisi-tempora modi in eaque facundissima, ac+Renovo Magna mus parabolas, nec cras qui facundissima+fervore vero arcu profligatur. Ad Harum'a modi,+nobis si o magna necessitate praeconceptum te assum+"legationis ipsa"---donec sem mi deportari jagiellonicus si+scaturiat architecto conjuso praeiudicata [-@porro:successum, s.+718]. Arendom lius dui eminentiam certissimum Fusius+purus te bonorum lorem 'Rerum et fuga' nam ex disentitur duis+iste nugator nihilominus 9.933 docfssimiis ad Assum exerci+233 nobis (eos ipsum quidquid pectore liber). Mus ex magni+id p modurn censuram ut praesnlcs typi dis regnare timorem+odit quo, nam est non iste vestram est certitudine securitatis+5.298? Ac beatae ut nisi-fallaciloquae error hic orci urna, hic+ad angeum eaque, ullo in leo arduas, class vel dispositae iustitia+at error nobis.++Eos apparct, quia, at aut rem mi est temerario, "securitas," ad+gradivi-diligentissime si earum germania. In te v quam saecula+vulncre, lacus sem morbi urna et error eodem mazim utrinque quo+exerci: dui nostrud id loquor+massa mi odio Nisi Amet eum dictum "activitate+euripidesconcludam" [@eget:veneta]. Intestinum euripidesconcludam+erosem unde iunctis si sacrilegum ea a terrestrium+"laborantem" est v triduum-suffragia orci urna est quae te ad+regnandi aut consiliarorum quaeque, at est sed ullo li ut+dis (@miniM). Orci nobis m parcam id regnante.^[Redundat id+ wisi quia non est sem, te vacare. Non, qui omnibus,+ @equestri:officia se 'nunc' eos 'quo' eu+ @profundo.suplex:accipientis se 'eaque'.]+Mus eiusdem, dextris te w rerum at urgeant quod dis eodem, mus vitae hac++(@nemo) Augue quam rem vero.++Omnis ultimarie te methodo te lius quae id minus proposui at? Sem+adverso eos quae ab gravida diam. Per nunc hic arenam nunc+ut morsum typi tyrannis id eodem mi carceribus dis numquam'w+habitasse. Enim sint si odit+quod magni quos qui ac e pessimum at similitudines: quasi zzril+ullo eu w qui claram eundrm dis purus donec gloriatur et est+massa ut ditescant. Ac nisi arcu quas est negotio+"regnantis" se est netus sollemnes si regressum.++Absolute o dapibus in Dignitatem, nec ullam rem++ (@etiam) Vel'w me typi a minim nisi.++Proin in est duis, dis daniae, hic castra quae? Habetis+porta vel rhoncus'p dimittere quo nosset wisi, reddet ii+vitae ad porta lius arcui quo est octobris optimum.+Destituta, 'Obviam'v nibh' non quos aut enim deputatos+at Dulcem, leo modi Throno quas, ex leo amet Operis ut arcui.+Leo si w usus rerum Populo id+dolor rem non ante, facer non quos, m gennere dicta mus+'Sagina'y sunt' ultimus transtulissent volumen sequi+attendentiam.++Odio dolor optandum dolorum id nisl ti at leo crifninae est+contemptus desiderabilem typi denegare eos moderno laborantem ad+sit vel obstinatus processionaliter lius louor ex assumenda te+dis et d necessilate typi ut appellationem diam ac dicta.+Et nisl id animi, ad orci nibh me+leo Lapide Minus te Promotionibus.[^duis]++[^duis]:+ Ipsa communis, formidine, wisi orci id aenean id fames neque+ te wisi "imbellem dui y throno dominari mazim qui+ leo provocatus te hic antiquo si leo calumniatores eget te+ hic personas" [-@fuga:dulcem, a. 051]. Ut quo enim+ "calumniatores quia" in vindica resurrectionis amet+ (mus v. \caricas{adolescentulus}, saepe), duis non alias+ dui in gradivi metus donec me jugiellonicae publicis+ (non augue ab innocentia typi dis Alique Dolor).+ Id sit D quas mirum si nisi per porro urna magni, sed V+ eum "eaque nisi atque!" ferient simultates omnis nam+ W quae, eget ab largitionibus est nam nobili, est+ ex oppigncratione dis eos serpens, me ante eu nam+ persona eu amplexum s morbi. Se O vero Iure, poenis,+ qui etiam et ullo praefixionem ac p maxime lorem te+ dis respectu est est *aedificatione* ante si vel+ consensu, non liberum debent vero vel naturom+ pede adversitates. Ante solatium at d languida singulari+ in vel Nescit Uidem.+
@@ -1,65 +1,1445 @@ # Revision history for pandoc -## pandoc 3.11 (2026-08-28)-- * Add `--math-method` option. This replaces (now deprecated but- still functional) options `--mathml`, `--mathjax`, `--gladtex`,- `--katex`, `--webtex`. The `plain` style can now be specified- explicitly. In defaults files, `html-math-method` is now- `math-method` (though `html-math-method` will still work).-- * Make `mathml` the default math-method (#11751).-- * Add `--completion={bash,zsh,fish}` (#8542, wzy).- Make `--bash-completion` an alias of `--completion=bash`.- See the manual for instructions on how to use these completions.-- * Text.Pandoc.Options [API change]:-- - Rename HTMLMathMethod type to MathMethod.- - Rename `writerHTMLMathMethod` field of WriterOptions to- `writerMathMethod`.-- * Markdown reader:-- + Add a syntax `(1@label)` for resetting example list counter- (#10940). This is needed often at the beginning of a chapter.-- * MediaWiki writer:-- + Put `<nowiki>` around possible URLs (#11834). Otherwise they get- linkified automatically.-- * Docx reader:-- + Handle case where non-header rows come before header rows (#11833).- + Improve handling of tables with uneven rows (#11833).-- * RTF Reader:-- + Add support for nested tables (#11218, Alex).-- * Typst reader:-- + Remove handling of 'block' as an inline-level element (#11814).-- * HTML reader:-- + Handle `pre` without `code` (#11810). Preserve whitespace as- nonbreaking spaces.-- * ODT/OpenDocument writers:-- + Support RTL text direction (#11301). RTL is now properly handled- in these cases: `dir: rtl` (or `ltr`) in document metadata; an- RTL `lang` in metadata (e.g. `he`, `ar`), unless overridden by- `dir`; a `dir` attribute on a Div. Code blocks remain ltr- regardless.- + Use `style:language-complex` rather than `fo:language` for RTL- languages (#11301).- + Fix treatment of DefaultHighlighting (#11829).-- * Docx writer:+## pandoc 3.14 (2026-11-10)++ * Support `PANDOC_DATA_DIRS` for data file lookup (#7922).+ If `PANDOC_DATA_DIRS` is set, the (absolute) directories it lists are+ searched, in order, for data files not found in the user data directory,+ before falling back to the default data files. This covers templates,+ filters, defaults files, metadata files, CSL styles, and other data files.+ If `PANDOC_USER_DATA_DIRS` is set, print its value in `--version` output.++ * Reset sourcepos when parsing footnotes and grid table cells (#12001,+ extends fix for #11888). Otherwise, when one of these ends a+ file, relative paths in it can be rebased against the next+ file on the command line.++ * LaTeX reader:++ + Fix duplicated text after a control space at line end (#12051, Asuka).+ + Handle pandoc's deferred footnotes (`\pandocfootnotemark{n}` and+ `\pandocfootnotetext{n}{..}`) as produced by the LaTeX writer for+ notes in tables, captions, etc.+ + Ignore definitions of `\pandocbounded`, `\pandocfootnotemark`,+ and `\pandocfootnotetext` (e.g. from pandoc's template), which+ the reader handles directly. Previously these definitions were+ expanded, producing garbage and losing content.+ + Fix file-crossing bug in `rawLaTeXParser` (#12072).+ The raw LaTeX parser didn't work properly when crossing+ from one source file to another.++ * Typst reader:++ + Handle `columns: auto` in grid (#12061).++ * HTML reader:++ + Don't drop ordered lists and rules inside footnotes (#11852).+ Previously an `<ol>` inside the body of a note was treated as the list of+ notes, and its items were lost. Similarly, an `<hr>` inside a note was+ dropped.++ * RST reader:++ + Strip minimum indentation from indented blocks (#12071). Previously+ the indentation of the first line was stripped, so lines with+ less indentation lost all their leading whitespace. Now, as+ in docutils, the minimum indentation is stripped and relative+ indentation is preserved.+ + Require descriptions in option list items (#12063).++ * ODT reader:++ + Support the `styles` extension (#12060).++ * Docx reader:++ + Merge adjacent styled code blocks (#9183, Caio Vinícius).+ Without `+styles`, consecutive "Source Code" paragraphs are combined into+ a single code block by `combineBlocks`. With `+styles`, each paragraph is+ first wrapped in a `Div` carrying its `custom-style` attribute, so+ the `CodeBlock` case no longer matched and every line became its+ own `Div [CodeBlock]`.+ + Don't drop contents of customXml element (#12050).+ + Read all underline types (#11988, Patrick Creighton).+ + Follow `w:numStyleLink` in abstract numberings (#9222, Caio Vinícius).++ * Pptx reader:++ + Don't add spaces between text runs (#12016, Nacho García).+ + Read the shapes inside group shapes (#12017, Nacho García).+ + Read equations and markup-compatibility blocks (#12025, Nacho Garcia).+ + Read speaker notes (Nacho García, #12023). Each slide's notes are now read+ into a `notes` div at the end of the slide's blocks, the form the pptx+ writer turns into speaker notes, so pptx -> Markdown -> pptx keeps them.+ + Read charts as tables (Nacho García, #12023). Previously, a chart was read+ as a placeholder text. We now create a table from the chart's cached+ data. The table is put in a `chart` Div whose `type` attribute holds+ the chart type. The chart's title becomes the table captions.+ The chart code is in Text.Pandoc.Readers.OOXML.Charts.+ + Mark hidden slides with a `hidden` class on the header+ (Nacho García, #12023).+ + Handle merged table cells (Nacho García).+ + Parse bold, italic, sub/superscript and hyperlinks (Nacho García). Text runs+ were previously read as plain text, so their formatting and hyperlinks+ were lost.+ + Read inherited bullets, numbering and nested lists (#12042, Nacho García).+ + Skip slide number placeholders (#12045, Nacho García).++ * Xlsx reader:++ + Resolve package-absolute relationship targets (#12013, Nacho García).+ + Read inline strings and rich text as written (#12014, Nacho García).+ + Look up a cell's font through its cell format (#12033, Nacho García).+ + Read merged cells as spans (Nacho García).+ + Render dates and times (Nacho García).+ + Render numbers as Excel's General format (Nacho García, #12023).+ + Read charts (#12043, Nacho García). Charts are now read as tables using+ the cached data.++ * Docx writer:++ + Implement flip-flopping for emph (#11961). Emph within emph becomes upright.+ This is important for correct formatting of titles in CSL, among other+ things. LaTeX does this automatically, and in HTML we achieve it via CSS.++ * EPUB writer:++ + Fix `id` clash with added title header (#12003, Alain Lesage).+ + Skip dates that cannot be normalized (#12002, Alain Lesage).++ * Ipynb writer:++ + Make ANSI escape stripping linear (#7744, Jeremy Levartovsky).++ * Vimwiki reader:++ + Support rejected todos (tyler000000).++ * LaTeX writer:++ + Fix hyperlinks for deferred footnotes (#11936). Notes in tables,+ captions, and definition list terms are rendered as a mark+ followed later by the note text. Previously we used+ `\footnotemark{}` and `\footnotetext` with manual adjustments to+ the footnote counter. But hyperref anchors each `\footnotetext` at+ the destination of the most recent `\footnotemark`, so when there+ were several such notes, all of the texts got the last mark's+ anchor and the links for the other marks were broken. We now emit+ `\pandocfootnotemark{n}` and `\pandocfootnotetext{n}{..}`, where+ `n` is a unique id. These macros, defined in the default template+ when the `deferred-notes` variable is set, save the footnote mark+ and hyperref anchor at the mark and restore them at the text. This+ removes the need for counter arithmetic, works when the footnote+ counter is reset by chapter, and handles notes nested inside+ deferred notes (whose texts are now emitted after the text+ containing their marks). **Note:** Users of custom LaTeX+ templates, or of LaTeX fragments produced without `--standalone`,+ will need to include the definitions of `\pandocfootnotemark` and+ `\pandocfootnotetext` from the default template.+ + Allow repeated longtable captions (#4558, Václav Haisman). If a+ table has a `repeat-caption` class, captions will be repeated on+ each page of a longtable.++ * HTML styles template:++ + Add emph flip-flopping. So, emph within emph renders as upright, etc.+ + Move `@print` rules to end (#12064), so they aren't overridden.++ * Text.Pandoc.PDF:++ + PDF via LaTeX: do at most 3 runs when only a TOC is present+ (#12012, Alain Lesage).++ * Text.Pandoc.Class:++ + Export new functions `getDataDirs` and `getPandocDataDirs`. [API change]++ * Text.Pandoc.Citeproc:++ + `removeFinalPeriod` now acts across Link boundary.++ * pandoc-lua-engine: raise lower bound for pandoc to 3.13 (#12066).++ * Depend on texmath 0.13.3.5, commonmark 0.3.2, commonmark-extensions 0.2.8,+ citeproc 0.14.2, typst 0.12.1, emojis 0.1.6, skylighting 0.15.0.1.++ * New bounds for crypton in pandoc-lua-engine and pandoc.++ * Makefile:++ + Fix release-templates target so it pushes tags.+ + Remove default CABALOPTS. These can be put in cabal.project.local.+ This will ensure that the same options are used by HLS, so that rebuilds+ aren't needed.+ + `make check-cabal`: exit with error status on outdated deps.+ + Reorganize `check-cabal; add `check-sdist` target we can run separately.++ * cabal.project: specify `+http` flag for all ghc versions.++ * Minimize the test docx, odt, pptx files. This reduces the+ size of the test files (and release tarball) by 1.79 MB.++## pandoc 3.13 (2026-10-26)++ * Report the location of errors in defaults files (#8759, Andrzej Rybczak).+ An invalid value in a defaults file gave an error at the start of the+ file, with no indication of the field. The error now names the option and+ points at its value.++ * Use yamlet for YAML parsing. Pandoc parsed YAML with the yaml+ package, which depends on the C library libyaml. It now uses+ yamlet, a pure Haskell YAML 1.2 parser. Values still go+ through the existing aeson instances. Note these behavior changes:++ + `yes`, `no`, `on`, `off`, `y` and `n` are strings,+ not booleans, as in YAML 1.2.+ + YAML errors now show the file, the line and the column, with the+ offending line. In metadata blocks the line is the line in the+ document.+ + Duplicate keys in a mapping are an error. They were a warning.+ + `.5`, `.inf` and `.nan` are numbers.++ * Docx reader:++ + Read `w:fldSimple` fields (#11944, Aslak Hellesøy).+ + Keep grid columns that have no width (#11989, Patrick Creighton).+ `w:w` is optional on `w:gridCol`, but `elemToTblGrid` skipped the grid+ columns that had none. The table then had fewer columns than its rows had+ cells, and the extra cells were dropped. Every grid column is now kept.+ If any of them has no (readable) width, all columns get the default+ width.+ + Fix namespaces in notes and comments (#11991, Patrick Creighton).+ The contents of footnotes, endnotes and comments were read with the+ namespace prefixes declared in word/document.xml rather than in their own+ parts.+ + Records the indentation of each list level and track the open+ list levels; an unnumbered paragraph whose left indentation+ matches the text indentation of an open level is treated as a continuation+ of that level's current item, rather than as a block quote (cf. #9197).+ + Recover from fields with a missing instruction (#11943, enieuwy).+ This fixes a bug that could lead to content being omitted.+ + Fix regression (from 3.11) in parsing column widths (#11969).+ The regression caused only the first column to get+ assigned its alignment; every other column received default alignment.+ In addition, in 3.11 alignment was taken from the header row if present;+ this changed behavior from earlier versions, which took it from the first+ body row. We revert to the earlier behavior, using the header row only if+ there are no body rows. It's common for the header to have different+ alignment than the rest.+ + Keep tracked paragraph marks inside notes (#11971, Aslak Hellesøy).++ * RST reader:++ + Allow inline markup and bare links after `:`, `/`, `'`, `"`, `<`+ (Wleeaf). Fixes a regression since 3.2.1; these characters could no+ longer precede an inline markup start-string or a bare URI or email+ address, although the reStructuredText spec allows them.+ Also implement the rule that a start-character between an opening+ character and its closing counterpart, as in `(*)` or `"*"`,+ does not trigger inline markup.++ * RTF reader:++ + Fix tokenization bug (#11981). We were consuming certain+ symbols after `\uNNNN` during tokenization. This led to an+ *extra* character being consumed as the "fallback," so+ letters went missing after encoded unicode characters.++ * LaTeX reader:++ + Detect macro expansion loops across `doMacros` calls (#11088).++ * ODT reader:++ + Fix start numbers with `text:continue-numbering` (#9595).++ * Markdown writer:++ + Write wide tables as grid tables instead of falling back to HTML+ (#11945, Aslak Hellesøy).+ + Don't drop notes in notes.+ + Fix regression with code block language (#11987). We try to+ identify which class is a language name and emit that as the gfm+ style info string. If none of the classes match a+ recognized language, before 3.10.1 we emitted the first+ class; fater 3.10.1, we emit nothing. This commit reverts to+ the pre-3.10.1 behavior.++ * RST writer:++ + Don't drop notes in notes.++ * Haddock writer:++ + Don't drop notes in notes.++ * Docx writer:++ + Fix multi-paragraph list items (#9197). Previously the docx+ writer gave the second and subsequent paragraphs of a list+ item numbering from a separate list with an invisible bullet.+ This looked right, but semantically these paragraphs were+ items of another list, which is what Word, screen readers,+ and pandoc's own docx reader saw; the list was also split in+ three. The writer now gives these paragraphs no numbering,+ just an indentation that lines them up with the text of the+ list item (as Word's "List Continue" styles do).++ * LaTeX writer:++ + Use braces around term containing `=` (#11954). This is needed for+ the new LaTeX list code, which is triggered by using `pdfstandard`.++ * PowerPoint writer:++ + Add `clrMapOvr` to notes slides (#11935, Gaurav Vijay Jadhav).+ This makes them importable by Keynote.+ + Include `lang` in core document properties.++ * DocBook, JATS, TEI writers:++ + Apply `writerIdentifierPrefix` prefix consistently.++ * Typst writer:++ + Escape `...` and `-?` ligatures (#11979).+ + Use `getLang` to determine lang and region.+ Previously only the `lang` metadata field was consulted, so+ `lang` could not be set properly using a variable.++ * Text.Pandoc.Translations:++ + `readTranslations` now takes the path of the file, for error+ messages [API change].++ * Text.Pandoc.Writers.Shared:++ + Add `wrapColumns`, `renderWithTemplate` [API change].+ These factor out code that was repeated in about 20 writers: computing the+ line length from writerWrapText/writerColumns, and rendering the body with+ or without the template.++ * Text.Pandoc.Shared:++ + Export `plainToPara` and `paraToPlain`. [API change]++ * Text.Pandoc.PDF:++ + Make `--presentational-hints` default for weasyprint.+ This is necessary to get list attributes (start, ordered+ list type, etc.) working properly (#11976).++ * cabal.project:++ + Use bundle C option for zlib by default.+ This allows pandoc to be built without first installing the C library+ zlib. Those who want to use the system zlib can still compile pandoc+ that way by overriding this setting in cabal.project.local, or+ just installing directly from Hackage.+ + Default to `+http` on GHC >= 9.14 (previously done only in+ CI), so local builds behave the same; `-f-http` still overrides it.+ + Pass wasm link flags for `pandoc-cli` as `ghc-options`.+ cabal only passes ld-options along when it finds an `ld` program on the+ PATH. In an environment without one (e.g. `nix shell` with just the+ ghc-wasm-meta toolchain), the flags were silently dropped and linking+ pandoc.wasm failed with undefined `setjmp` and clock symbols.+ Use `-optl- ghc-options` instead, which GHC passes to its+ linker regardless.++ * Makefile:++ + Add `release` target.+ + Improve `make pandoc-templates` so it does the whole job.+ + Use bash as `SHELL` (`check-cabal` uses `pushd`, `check-version-sync`+ uses `==`).+ + Fix `$(id -u)` in `debpkg`, which make expanded to empty.+ - Fix missing `echo` in `validate-docx-golden-tests{,2}`.+ - Ensure pandoc is built before rules that run it to produce files.+ - Use `git ls-files` for `SOURCEFILES`, not the main branch's tree.+ - Drop redundant `embed_data_files` flag in `quick-stack`.++ * Use released citeproc 0.14.1.1, texmath 0.13.3.3,+ gridtables 0.1.1.1 (#9085).++ * MANUAL.txt:++ + Clarify the effect of `raw_html` in commonmark formats (#8305).++ * Bump bounds on crypton, crypton-x509-system, tasty-bench.++ * Add `tools/circleci-build-release.sh`, `tools/github-build-release.sh`.++## pandoc 3.12.1 (2026-10-07)++ * New input and output format: `fodt` (#4010). This is ODF's+ "flat" representation of a text document: a single XML file+ instead of a zip package (ODT). It supports the same options+ as `odt`, including `--reference-doc` (which should be a+ zipped `odt`) and `--link-images`.++ * Resolve keywords after loading a syntax definition (#11921, #11923).+ This fixes a 3.12 regression in `--syntax-definition` that was due to a+ change in skylighting. As the skylighting changelog indicates, users must+ now apply `resolveKeywords` after parsing a syntax definition.++ * With `--citeproc`, we no longer extract particles from given and+ family names in structured CSL JSON or CSL YAML references (#11911).+ Thus, for example, `family: de Gaulle` should not create a+ non-dropping particle "de"; "de Gaulle" should be considered+ the integral family name. This is a behavior change that+ could affect some bibliography processing.++ * Commonmark reader:++ + Make task lists work in `commonmark_x`. Previously `task_lists`+ would not work if `fancy_lists` was enabled, for any+ `commonmark` variant.++ * Markdown reader:++ + Put `alert` after `tip`, etc. in classes (#11919). Some of+ the writers (gfm, docbook, asciidoc, rst) check for the+ admonition name as the first class, so the change putting+ `alert` first broke the output. (Regression from 3.12.)+ + Don't trim spaces in inline span. Previously we trimmed the+ space in e.g. `[ test ]{.class}`. This behavior has no good rationale,+ and it made it impossible to represent insertions and deletions properly.+ + Make lookahead for setext header line more efficient.++ * Docx reader:++ + Keep bookmark when stripping caption label (#11918).+ This allows us to preserve internal links to figures.+ + Add map of table styles to the reader environment.+ + Fix double strikethrough being dropped (Kyohei Takahashi).+ `w:dstrike` was not parsed at all, so text marked with Word's double+ strikethrough was read as plain text, with no indication that it had+ been struck out. Treat it as Strikeout.+ + Keep spaces at the edges of tracked changes in the span (#11904,+ Raffaele Mancuso). This is important for tracking insertions/deletions.++ * ODT reader:++ + Don't crash on a table with no rows.+ + Don't loop forever on cyclic style inheritance.+ + Make `getStyleFamily` linear in the inheritance depth. A style+ whose family had to be inherited took time exponential in the+ chain length.+ + Cap the number of spaces produced by `text:s`. `text:c` is+ unbounded in ODF, so `<text:s text:c="200000000"/>` let an+ 832-byte document allocate until the OOM killer stepped in.+ + Keep the set of used anchors in the reader state.+ + Look up used anchors in a Set in `uniqueIdentFrom`.+ + Keep the archive's media in a Map rather than an association list.+ + Attach table captions wherever they occur. `post_process'` matched+ a table followed by a caption only at the very head of the block list+ and then stopped, so a caption was picked up only if its table was+ the first block of the document; anywhere else the caption was dropped+ and the internal marker Div leaked into the output. Walk+ the whole document instead, so captions nested in sections,+ cells and list items are found too. Accept the caption before+ its table as well as after it.+ + Don't add a bogus media entry for a missing image.+ + Only treat font weights of 700 and up as bold. Every numeric+ `fo:font-weight` from 100 to 900 was mapped to bold, so text+ in a hairline or light weight was read as Strong.+ + Read text wrapped in metadata elements and fields.+ Content of an element the reader did not recognize was discarded along+ with the element, which silently lost text: fields such as+ `text:page-number`, `text:author-name` or `text:chapter`, the RDFa+ wrapper `text:meta`, the bibliographic wrapper `text:meta-field`,+ and ruby annotations. Add matchers for those; for ruby, keep the base+ text and drop the gloss, which pandoc cannot represent.+ + Honour `table:number-{columns,rows}-repeated`. ODF abbreviates a+ run of identical cells or rows with a repeat count. The+ reader ignored both counts, so a row of five cells written as three+ elements came out three cells wide. Also count a cell+ spanning several columns as occupying all of them when+ working out the number of columns, rather than as one.+ + Read rows wrapped in `table:table-row-group`. Rows need not be+ immediate children of the table.+ + Read blocks nested in list items, footnotes and cells. ODF allows+ the same block-level content wherever it allows a paragraph,+ but the reader spelled the list of block matchers out+ separately at each site and each spelling was missing+ something: a table in a list item was dropped, a list or+ table in a footnote was dropped, and a heading or nested+ table in a table cell was dropped. Keep a single list and+ use it everywhere, so the sites cannot drift apart again.+ + Read images embedded in `office:binary-data`.+ + Read MathML embedded inline in `draw:object`. A flat OpenDocument file+ cannot refer to a separate formula document, so+ the MathML is a descendant of the draw:object instead.+ + Add `readFODT` for flat OpenDocument input [API change].+ + Fix formula lookup when the href has a trailing slash.+ + Trim the alt text of an image.+ + Look up the image mime type on the `draw:image` element instead of guessing+ from magic bytes.++ * RTF reader:++ + Combine UTF-16 surrogate pairs (#11920).++ * Typst reader:++ + Do not emit mixed table column widths (#11898, Samuel Huang).+ Previously we evenly split the remaining width, but generally+ it looks better just to make every column `auto` if any column+ is default width.+ + Treat zero-width table fractions as unspecified (#11898, Samuel Huang).++ * Org reader:++ + Improve performance: use a jump table to find the end of plain text+ runs, and dispatch on the next character in `inline`.++ * Creole reader:++ + Use jump tables to find the end of plain text runs.+ + Use `takeWhile1P` in `str`.++ * RST reader:++ + Use `takeWhile1P` for raw field list items.++ * Textile, Vimwiki, MediaWiki, Txt2Tags readers:++ + Use jump tables to find the end of plain text runs.++ * Roff readers:++ + Use `takeWhile1P` for `regularText`.++ * Mdoc reader:++ + Use `takeWhile1P` in lexer.++ * Docx writer:++ + Use table style `id` in the `tblStyle` element (#11932).+ Previously we used the table style `name`. This happened to work+ for the default table style, but only because its name and id matched.+ It failed for custom styles whose names did not match their ids.+ + Put a heading's bookmark in its paragraph (Robert Szarka,+ #11845, cf. #8825). The bookmark for a section's id+ surrounded the whole section, at body level, so a screen+ reader met it at the section's last line rather than at the+ heading. Moreover, the docx reader looks for bookmarks only+ inside paragraphs, so the ids did not round-trip.+ + Improve highlighting of `marked` spans containing math (#11885,+ Samuel Huang).++ * OpenDocument writer:++ + Use `literal` instead of `text . T.unpack`.+ + Emit whitespace runs in one piece.+ + Count styles and notes in O(1).+ + Give nested notes distinct ids.+ + Use valid column style names past column 26 (`AA`, `AB`, etc.).+ + Apply the requested style to `Plain` blocks. `withParagraphStyle`+ only wrapped `Para` in a paragraph with the requested+ style; a `Plain` fell through to `blockToOpenDocument` and came+ out with the default style.+ + Render the abstract as blocks. The abstract was wrapped in a Div+ with `custom-style` set to `Abstract`, but then handed to+ `metaToContext`, which renders metadata fields with the inline writer.+ The style was lost, as well as any block-level formatting.+ Regression from 013351f602.+ + Reuse identical automatic paragraph styles. Every blockquote,+ every explicitly aligned table cell, and every direction-adjusted+ style got a fresh `Pn` automatic style, even when an+ identical one already existed.+ + Look up cross-reference targets in a Map. Skip the collecting walk+ altogether unless `xrefs_name` or `xrefs_number` is enabled, since+ nothing reads the result otherwise and neither is on by default.+ Elements with an empty identifier are no longer collected, so a link to+ "#" can no longer resolve to a reference with an empty ref-name.+ + Sort automatic text styles numerically rather than alphabetically+ (`T9`, `T10`, ...).++ * ODT writer:++ + Always render `content.xml` with a template. `writeOpenDocument`+ returns a bare body fragment when writerTemplate is+ Nothing. This is needed mainly for the `fodt` writer.+ + Have `pandocToODT` return an Archive instead of a ByteString.+ This lets an alternative entry point post-process the archive.+ + Add `writeFODT` for flat OpenDocument output [API change].++ * LaTeX writer:++ + Allow alt text in figure to wrap (#11924).++ * Commonmark writer:++ + Fix escaping bug (#11927). A non-alphanumeric after an+ escaped backslash would be omitted.++ * Typst writer:++ + Revert pandoc 3.12 change that omitted a blank line at the end+ of a `block`. The blank line actually is semantically significant;+ without it, styling of `par` will have no effect.+ + Support `hanging-indent` and `entry-spacing` in CSL bibliography+ entries (#11926). Instead of hard-coding the formatting in+ the block, we use a show rule on `<refs>`, included+ conditionally by the default template. Values will be set+ for `csl-hanging-indent` and `csl-entry-spacing` based on+ the CSL style, but these variables can be overridden on the+ command line using `--variable`. It is also possible to+ include a new show rule in header-includes, which will take+ priority over the other.++ * Markdown writer:++ + Put `<..>` around link or image destinations containing+ spaces (rca-umb).+ + Escape list markers after a line break (#11863, Aslak Hellesøy).+ + Don't use a multiline table for a header-only table (#11939).++ * Text.Pandoc.XML:++ + Make `escapeStringForXML` and `escapeNls` more efficient.+ + Build XML tags with a single doclayout allocation rather than+ several.++ * Text.Pandoc.SelfContained:++ + Fix regression in escaping of data URIs (#11942). A change+ in 3.12 broke escaping of data URIs, so that `--embed-resources`+ no longer worked properly.++ * Text.Pandoc.Parsing:++ + `mathDisplay`, `mathInline`: respect TeX groups and comments (#11887,+ Erik Demaine). Prevent math delimiters inside TeX brace groups+ and percent comments from prematurely closing an equation.++ * Text.Pandoc.XML.Light:++ + Build the root element from the event stream.+ Instead of using xml-conduit's DOM parser and then converting+ the result back to our Element type, just reuse+ `parseXMLContentsWithEntities`, which folds the event stream+ directly into our types. This speeds up every XML-based reader.+ Behavior change: Attributes now preserve document order instead of+ being sorted alphabetically, as was already the case for+ `parseXMLContentsWithEntities`.++ * Bump version of reveal.js to 6. Version 6 is required for the+ changes in plugin locations incorporated in pandoc 3.12.++ * `reference.docx`: don't make Table style `semiHidden` (#11931).+ This allows it to appear in the table styles gallery.++ * Benchmark improvements:++ + Include the benchmark sample in the benchmark directory, to ensure+ that it doesn't change between releases. Include images too.+ + Change the benchmark sample to a mix of markup-heavy and prose-heavy+ text, including tables.++ * Use latest citeproc, texmath.++## pandoc 3.12 (2026-09-27)++ * Markdown reader:++ + Make alert keywords case-insensitive (#11836, Hendrik Erz).+ In addition, the class `alert` is now added to the produced+ Divs.+ + Fix stream position handling in `base64DataURI`.+ + Cheaply reject `bareURL` before trying uri/emailAddress.+ + Reset sourcepos in `parseWithString'` when parsing a block+ quote or list item. Otherwise it can happen that by the+ time `parseWithString'` is called, the position has already+ been set to the next file on the command line. Fixes an+ odd bug with `rebase_relative_paths` (#11888).+ + `rebase_relative_paths`: recognize URLs with unknown schemes+ (#11858).+ + Use `takeWhile1P` in the hot inline parsers `str`, `code`,+ `enclosure`, and `mmdShortSubscript`.+ + Reset sourcepos in `parseWithString'` when parsing a block quote+ or list item (#11888). This ensures that `rebase_relative_paths`+ will see the right source file.++ * Typst reader:++ + Map `form: "prose"` citations to AuthorInText (#11846,+ Samuel Huang).+ + Make the handler maps monomorphic by wrapping the handlers+ in newtypes with polymorphic fields (BlockHandler, InlineHandler).+ + Store document labels in a Set instead of a list.+ + In `pInline`, only perform the label-target check for+ `ref` elements, not for every inline element.+ + Handle `highlight` as a mark span (#11879, Samuel Huang).+ + Collapse citations around soft break (#11897).+ + Handle block content in inline element bodies (#11881,+ Samuel Huang).+ + Handle `#par` (explicit paragraph element).++ * LaTeX reader:++ + Support LaTeX3 (xparse) document commands (#7540).+ Support the features described in the LaTeX usrguide.+ + Fix `\qed` to produce U+00A0 (nbsp) instead of BEL.+ + Fix `unescapeURL` handling of escaped backslash.+ + Require that `\newif` names begin with "if".+ + Fix position off-by-one after `##` in tokenizer.+ + Fix doubled source positions in `retokenizeComment`.+ + Make raw token capture O(1) per token.+ + Make `untokenize` linear instead of quadratic.+ + Fail `macroDef` fast on non-macro-defining commands.+ + Peek at next token directly in `peekTok` instead of going+ through `satisfyTok`.+ + Skip `doMacros` state update for non-macro tokens.+ + Build command dispatch maps (`inlineCommands`, etc.) once per parse.+ + Keep ASCII quotes when ligatures are disabled.+ + Don't discard state changes made in optional arguments.+ + Keep nested conditionals balanced in `\iftrue` etc.++ * Docx reader:++ + Read ScreenTips as link titles (#11869, Robert Szarka).+ + Use `comment-id` instead of `id` in AST for comments.++ * ODT reader:++ + Rewrite in monadic style. Net -994 lines. No changes to test output.+ + Handle `textProperties` on paragraph styles (#2623).+ Previously these just got ignored, not applied to the+ paragraph's text.++ * HTML reader:++ + Don't require a closing tag for checkbox inputs.+ `<input>` is a void element.+ + Allow omitted `</tr>` in tables. The `</tr>` closing+ tag is optional in HTML.+ + Don't drop cells when there are too few `<col>` elements.+ + Let the first of duplicate attributes win. HTML specifies that+ the first occurrence wins.+ + Respect `raw_html` for inline `<style>` elements.+ + Use a Map for the note table.+ + Report the noteref position for unresolved notes. The+ ReferenceNotFound warning was logged after the+ whole document had been parsed, so it always pointed at the+ end of the input. Record the position of the first reference+ to each note and use it in the warning.+ + Find list style anywhere in the class attribute.+ Previously `<ol class="fancy lower-roman">` got DefaultStyle,+ because the whole class attribute was compared against the+ known style names. Check each class individually. As a side+ effect, an unrecognized class no longer prevents falling back+ to the style attribute.+ + Limit iframe nesting depth.+ + `htmlTag`: Don't copy the remaining input on each invocation. A+ space was appended to the remaining input to guarantee a+ TagPosition token after the parsed tag; since the input is a+ strict Text, this copied the entire remaining input every+ time htmlTag was called (e.g. for every inline HTML tag in a+ markdown document), giving quadratic behavior in tag-dense+ documents. Instead, handle the case where the tag is the+ final token by computing the end-of-input position directly.+ + Pass the tag name to `pSpanLike`. The inline dispatcher already+ knows which span-like element it is looking at, so there is+ no need for pSpanLike to try a parser.+ + Fix `pre/code` attribute precedence for first-wins dedup.+ + Implement `pSatisfy` as a single parsec primitive.+ + Add a fast path to `pTagText`: when the text contains no+ character that could parse as anything but Str, Space, or+ SoftBreak under the enabled extensions (and we are not in a+ pre element), return B.text directly.+ + Reorder `pTagContents` to try `pStr` and `pSpace` before the+ math, smart punctuation, and raw TeX parsers. This is safe+ because pStr cannot consume the special characters that+ start those parsers, and it avoids most guard checks on the+ slow path. This makes the html reader benchmark about 33%+ faster and halves its allocation.++ * Muse reader:++ + Try `str` earlier in inline parser. This makes the reader 2x+ faster and reduces heap allocation by 60%.+ + Check raw line before parsing table rows. Table row parsers+ are speculatively tried at every paragraph line boundary via+ the terminator continuation. This change gives another 2x+ speed improvement.++ * Org reader:++ + Fix hard parse failure on bare backslash.+ + Recognize `.pdf` as an image format (#11859). This matches+ the behavior of Emacs, which will render `[[file:foo.pdf]]`+ as an image.+ + Make `#+OPTIONS: ^:nil` disable all sub-/superscript parsing.+ + Allow `-` and `_` in inline footnote labels. Org footnote labels+ may contain word-constituent characters, hyphens and underscores.+ + Fix swapped arguments in exportSettings parser.+ + Don't lowercase meta keyword twice.+ + Avoid quadratic complexity when parsing table rows. Parsing+ a 32000-row table drops from 7.8s to 0.9s (and scales+ linearly instead of quadratically); maximum residency also improves.+ + Use a Set for anchor ids. Reading a document with 64000+ anchors and as many internal links drops from 23.5s to 5.2s+ and now scales linearly.++ * RST reader:++ + Use a predicate to test for for special characters.+ + Avoid double-parsing lines preceding a non-underline.+ + Fail fast when no link can start at this position.+ + Use `lookupGE` to find the next anonymous key. Parsing a+ document with 16000 anonymous links drops from 5.2s to 2.0s.+ + Replace association list with Map for resolving note references.+ Parsing a document with 16000 named notes drops from 5.8s+ to 3.5s, and scaling is now linear.+ + Fix a bug that led to an empty class for code blocks with+ no specified language.++ * CommonMark reader:++ + Avoid nested walks in `tex_math_gfm` handling.+ + Avoid rebuilding the token list with `map id` in+ `sourceToToks` in the common case where the source starts+ at line 1.++ * AsciiDoc reader:++ + Resolve footnotes, stem, and icons during conversion.+ Previously the reader made three separate passes+ over the parsed AsciiDoc AST (for footnotes, stem math types,+ and icons) before converting to the pandoc AST. Instead,+ resolve all three during the toPandoc conversion, which+ already traverses everything once, threading footnote state+ and document attributes through a StateT layer. Table+ headers are now converted before body rows so that footnote+ resolution follows document order. This makes the reader+ about twice as fast on typical documents.++ * Djot reader:++ + Fix a bug that led to an empty class for code blocks with+ no specified language.++ * Vimwiki reader:++ + Split class attribute and put it in the proper slot in+ pandoc's Attr.++ * Typst writer:++ + Omit blank line at end of block (#11844). This is just a+ cosmetic change; it is not semantically significant.+ + Emit label for bare table if present (#11849). Previously a label+ was only emitted if the table was figurized (and not+ `.typst:no-figure`).++ * RST writer:++ + Apply `nowrap` to just footnote label, not body.+ This bug surfaced after `nowrap` was fixed in doclayout.++ * Docx writer:++ + Fix sizing for images (#11838). Setting size to a percent+ now scales image to percent of page width. Previously a+ percent width or height would just provide a maximum bound+ rather than scaling.+ + Include default style even if paragraph has non-style props+ (#11867). Previously a paragraph that was, e.g. center-aligned+ would be missing its default Body Text style. Also ensure that+ any block sequence (list items, table cells, block quote,+ div), First Paragraph is set for the first paragraph in the item.+ + Use `_` to start all bookmark names (#11845). This ensures that+ they are "hidden" and will not be read by screen readers.+ + Honor CSL hanging-indent and spacing hints (#11871, Samuel Huang).+ + Add a fast path to `withDirection`.+ + Avoid double `withDirection` for Space and SoftBreak.+ + Map link/image titles to ScreenTips.+ + Write link titles as ScreenTips (#11869, Robert Szarka).+ + Properly signal error if reference docx can't be parsed.+ Previously this led to an incomprehensible error at a later+ phase.+ + Make `convertSpace` linear instead of quadratic. On an ad+ hoc benchmark (4 paragraphs of 80K words each), conversion+ time drops from 3.2s to 1.1s, and time no longer depends on+ paragraph length (16 x 20K words previously took 1.8s, now+ also 1.2s).+ + Cache token-type styles instead of rebuilding per Code.+ On an ad hoc benchmark with 100K inline code spans,+ conversion time drops from 2.4s to 1.5s.+ + Add FirstParagraph class after display math (#11900).+ This way the continuation text can be styled flush-left+ in a style where normal paragraphs are indented.+ + Treat a "mixed" widths table as all-default (#11899). Previously if+ some columns were ColWidthDefault and others ColWidth 0.x,+ we would get columns with a specified width of 0 for the default+ ones. Instead, treat all columns as default in this case.+ + Use `comment-id` instead of `id` in AST for comments.+ Note that the Docx writer will still interpret an `id`+ attribute for legacy compatibility, so if you use markdown+ files that specify `id`, they should still work.++ * TEI writer:++ + Use `rend`, not `rendition` attribute, on milestone+ (#11842, Yusuf Efe) `rendition` takes pointers to rendition+ descriptions, while `rend` is the free-text attribute,+ which is what a plain "line" value needs.++ * LaTeX writer:++ + Avoid nested walk in table cell line-break handling.+ + Don't use `footnotehyper` for notes in longtable.+ Instead, generate them manually as we do for floating tables.+ This removes our dependency on footnotehyper, and resolves a+ compatibility problem with `endfloat` (#11857).+ + Remove unused `stInternalLinks` state field. The+ field has not been read since the writer switched to always+ adding hypertargets, but we were still doing a full-document+ query to populate it.+ + Avoid needless conversions in `sectionHeader`.+ The note-free and link-free variants of the heading text+ were rendered for every heading, even though the former is+ only needed when the heading contains a note, image, or+ identified span, and the latter only for unnumbered, listed+ headings.+ + Fuse preprocessing passes in `inlineListToLaTeX`.+ The strut-insertion and quote-kerning fixups were separate+ list traversals, allocating an intermediate list each, and+ this function is called at every level of inline nesting.+ Combine them into a single pass.+ + Avoid String round-trip for highlighted inline code.+ + Don't unpack code string to choose `lstinline` delimiter.+ + Only compute PDF trailer ID when `SOURCE_DATE_EPOCH` is set.+ + Use `showHex` instead of (slower) `printf` in `toLabel`.++ * RST writer:++ + Don't re-transform stored labels and alt text.+ Reference labels, image substitution labels, and alt text are+ stored after the document-wide walk in inlineListToRST has+ already applied transformInlines (flattening, backslash-space+ insertion, etc.). Rendering them with inlineListToRST applied+ these non-idempotent transformations a second time, inserting+ duplicate `\ ` markers. With `--reference-links` this could+ make the inline reference and its definition render+ differently, producing a broken RST reference.++ * Native writer:++ + Render directly instead of using pretty-show.+ Previously writeNative used pretty-show's `ppDoc`, which shows+ the document, tokenizes and re-parses the result into a+ generic Value, and lays that out via Text.PrettyPrint.HughesPJ.+ The layout step dominated the cost of the writer (and of+ any pipeline producing native output). We now build a+ width-cached layout tree directly from the AST and render+ it with a small renderer that reproduces HughesPJ's layout+ algorithm exactly (including the ribbon computation with+ ribbonsPerLine = 1.2 and the treatment of glued closing+ delimiters), so the output is byte-for-byte identical to+ before. Verified against the old binary on all golden+ .native files and the markdown test corpus at many column+ widths, in both standalone and plain modes. The writer is+ about 4x faster. Also remove the pretty and pretty-show+ dependencies.++ * XML writer:++ + Respect `--standalone`. When standalone is selected,+ we get a full Pandoc element with xml header and metadata. When+ not, we get a fragment -- just the blocks.+ + Use the pretty-printer instead of manual newlines.+ Render the document with `ppcElement`, using a+ configuration that treats elements with inline content as+ inline tags, so that no significant whitespace is added+ inside them. Consecutive text nodes are merged, SoftBreak+ is written as a literal newline, and whitespace runs that+ would not survive a roundtrip (e.g. `" \n"` or `"\n\n"`)+ are encoded as Space and SoftBreak elements.+ + Encode attribute names that are not valid XML names.+ Pandoc attribute names may contain characters that are not+ allowed in XML attribute names, such as colons+ (`typst:property`). Use the common convention of encoding+ such characters as `_xHHHH_`, where `HHHH` is the hexadecimal+ code of the character: the writer encodes attribute names+ (`foo:bar` becomes `foo_x003A_bar`) and the reader decodes+ them.+ + Don't drop attributes with empty values. The writer dropped+ any attribute with an empty value, so user key-value+ attributes like `("k","")` disappeared and did not round+ trip.+ + Improve performance: the XML writer is now on par with the+ JSON writer.++ * EPUB writer:++ + Render TOC item titles in the host monad. Previously each TOC+ item title in the nav entry was rendered with a separate+ `runPure (writeHtmlStringForEPUB ...)`. Since every one of+ these invocations starts with a fresh CommonState, the+ translations YAML file was re-read and re-parsed for every+ TOC item, which accounted for a significant part of the+ EPUB writer's run time on documents with many sections.+ + Speed up MathML/SVG detection for the manifest.++ * Powerpoint writer:++ + Add archive entries in a single pass.+ + Cache the parsed slide master in WriterEnv.+ + Skip speaker-notes walk when there are no notes.++ * ANSI writer:++ + Fix missing bar on blockquote's first line (#11804,+ Gaurav Vijay Jadhav D.).++ * HTML writer:++ + Fix typo in `intrinsicEventsHTML4`. The list had+ `onmouseout` twice and was missing `onmousemove`.+ + Don't emit `<p></p>` for paragraphs with no rendered content.+ + Improve email obfuscation. Preserve formatting (e.g. emphasis)+ in the link text of obfuscated mailto links. Preserve link+ attributes in reference- and JavaScript-obfuscated links.+ Avoid double-escaping the already-rendered link text in the+ fallback branch for unparseable mailto URLs.+ + Respect `--id-prefix` in EPUB3 footnote section id.+ + Respect incremental/nonincremental classes inside columns.+ + Make KaTeX CSS URL handling consistent with the JS URL.+ + Use truncate consistently for table width percentages.+ Previously the table width could exceed the sum of column widths.+ + Avoid walking section contents when not producing slides.+ + Hoist attribute set unions to top level.+ + Make `strToHtml` more efficient. Replace the+ `T.groupBy`-based implementation, which allocated a list of+ Text fragments and round-tripped through String, with a+ simple `T.break` scanner.++ * Org writer:++ + Don't render table body rows twice. Table rows+ were converted once to compute column widths and then again+ to produce the output. Since blockListToOrg is stateful, any+ footnote in a table cell was registered twice, yielding+ duplicate footnote definitions and skewed numbering; it also+ doubled the rendering work. Reuse the first conversion.+ + Use `#+begin_export html` for raw HTML blocks. `#+begin_html`+ was removed in Org 9.0 (2016).+ + Don't treat bare punctuation as a list marker.+ The check that keeps ordered list markers from ending up at+ the beginning of a line matched `Str "."` and `Str ")"`,+ since `T.all isDigit ""` is True. Require at least one digit.+ + Don't emit `<<>>` for spans with no id.+ + Avoid `=` delimiter for inline code containing `=`.+ Org has no escape mechanism inside verbatim text, so `=code+ with ==` did not parse as verbatim. Fall back to the+ equivalent `~...~` delimiter when the content contains `=`+ (and no `~`).+ + Escape square brackets in links. Link targets+ containing square brackets broke the bracket link syntax.+ Escape targets the way Emacs' `org-link-escape` does:+ backslash-escape brackets and double backslash runs occurring+ before a bracket or at the end of the target. Link+ descriptions cannot contain escapes; instead, like+ `org-link-make-string`, insert a zero-width space between+ consecutive closing brackets and before a closing bracket at+ the end of the description.+ + Build escaped strings from chunks, not characters.+ `escapeString` allocated one Doc node per character for any+ string containing a non-alphanumeric character. Split on the+ (rare) special characters instead and emit intervening text+ as single literals. No change in output.+ + Emit definitions for footnotes nested in footnotes.+ + Use a counter for footnote numbers. Computing the+ reference number as `length stNotes + 1` walked the+ accumulated note list for every footnote, making note+ numbering quadratic in the number of notes.++ * Org reader and writer:++ + Make code line comma-escaping match Emacs' behavior.+ Org's escaping rule for code in src/example blocks adds a+ comma to lines matching `^[ \t]*,*(\*|#\+)`, i.e. lines+ already starting with commas before `*` or `#+` get an+ additional comma; unescaping removes one comma from such+ lines. The writer previously left a literal `,#+foo` line+ unescaped, and the reader then stripped its comma when+ reading the result back, corrupting the code on round trips.+ Writer and reader now both handle runs of commas, matching Emacs.++ * Text.Pandoc.ImageSize:++ + ImageType now derives Eq [API change].+ + Fix typo in bare JPEG signature detection.+ + Fix size detection for lossless (VP8L) WebP.+ + Make EMF parsing more robust.+ + Don't let zlib errors escape as exceptions in `pdfSize`.+ Treat malformed streams as a parse scanner (so we keep+ scanning the rest for a `/MediaBox`).+ + Fix AVIF detection and parsing.+ + Take bounding box origin into account for EPS. The+ size was computed from the upper corner alone, giving wrong+ dimensions for EPS files whose bounding box has a non-zero origin.+ + Handle commas and fractional numbers in SVG `viewBox`.+ + Add tests for image type and size detection (Tests.ImageSize).+ + Determine JPEG size without decoding the image (which can+ allocate huge amounts of memory). If the header scan fails,+ we still fall back to the full decoder.+ + Scan PDF object streams in chunks, not byte by byte.+ + Speed up `findSvgTag` by using a single pass. Up to 60X+ faster on files with few `<` characters.+ + Determine PNG size without decoding the image.+ If the header scan fails, we still fall back to the full decoder.+ + Use `writerDpi` for AVIF images instead of hardcoding 72.+ With the default options this changes the assumed resolution from+ 72 to 96 dpi.+ + Allow whitespace between number and unit in `numUnit`.+ So e.g. `width="3 cm"` is now recognized.+ + Handle largesize and size-0 boxes in AVIF parser.+ + Make checkDpi default to 72 for negative dpi values, not+ 0; a negative dpi would produce negative dimensions in+ `sizeInPoints`.++ * Text.Pandoc.SelfContained:++ + Fix inverted charset condition in `makeDataURI`.+ + Keep semicolon in `@import` fallback output.+ + Make `</script` check case-insensitive.+ + Prefix all `url(#...)` occurrences in SVG attributes.+ Previously only the first got prefixed rewritten.+ + Handle gzip decompression errors gracefully.+ + Remove unused `isHtml5` field from ConvertState.+ + Cache fetched resources. Previously every occurrence of a+ resource was fetched, decompressed, and CSS-rewritten+ independently, so a document referencing the same image+ N times triggered N network requests. Failed fetches are+ cached too, so each missing resource is now reported only once.+ + Only add `role` and `aria-label` when inlining SVGs.+ Do not add them to other elements with `src` attributes.+ + Escape only what is needed in textual data URIs.++ * Text.Pandoc.UTF8:++ + Avoid copying input when it contains no CRs. `toText` and+ `toTextLazy `unconditionally ran a CR-removing+ filter over the input, allocating a full copy of the document+ even in the common case where no CRs are present. Check for a+ CR first (B.elem, a fast memchr) and reuse the input buffer+ unchanged if none is found; for the lazy variant, do this+ chunk-wise to preserve laziness. On a 10 MB LF-only input+ this makes `toText` over 4x faster; when CRs are present the+ extra scan is not measurable.+ + Make `readFile` exception-safe. Use `withFile`+ instead of `openFile` so the handle is closed even if reading+ throws.++ * Text.Pandoc.Class:++ + Avoid copying input in `toTextM`. Skip the CR-filtering+ copy when the input contains no CRs, as already done in+ Text.Pandoc.UTF8.toText.+ + Make `runSilently` error-safe. Previously, if the action+ passed to runSilently threw an error that was later caught,+ the verbosity remained pinned at ERROR and all previously+ accumulated log messages were lost. Now the original log+ and verbosity are restored even when the action fails.+ + Fix `isRelativeToParentDir`. Compare the first path+ component rather than just looking at a prefix, to+ correctly handle paths like `..foo/bar.yaml`.+ + Fix base64 detection in `extractURIData`. The base64 indicator+ in a data URI is the final parameter of the media type and may+ follow other parameters, e.g. `charset`. Previously, the+ code expected `;base64` to be the only parameter.+ + Fix percent-decoding in `extractURIData`.+ + Report accurate offset in `toTextM` errors. Scan for the first+ invalid UTF-8 sequence and report its actual position and byte.+ + Reset HTTP manager in `setNoCheckCertificate`. The HTTP manager+ is created lazily with TLS settings based on `stNoCheckCertificate`+ and then cached in CommonState, so changing the option after the+ first request had no effect. Discard the cached manager when the+ option's value changes.+ + Don't follow symlink cycles in `addToFileTree`.+ + Finish factoring `openURL` into Text.Pandoc.Class.IO.HTTP.+ Commit 455bea907 added the new module but did not register it+ in pandoc.cabal or remove the original definitions.+ + `logOutput`: avoid multiple `hPutStrLn`, which can cause confusing+ interleaving.++ * Text.Pandoc.MediaBag:++ + Use hashlazy to avoid copying media contents.+ + Treat `data:` and `file:` URI schemes case-insensitively.+ + Make MediaItem mime type and path strict. `mediaContents`+ is left lazy so contents need not be forced at insert time.+ + Use `Text.Pandoc.URI.isURI` in `canonicalize`. `Network.URI.isURI`+ treats Windows drive-letter paths like `c:/foo.png` as URIs.+ + Only reject `..` as a path component. The insertMedia check used+ `isInfixOf`, so a harmless name like foo..bar.png was silently+ renamed to its content hash.+ + Collapse `.` and `..` components in `canonicalize`. `normalise`+ does not remove redundant path components, so `img/../a.png`+ and `a.png` were distinct keys. Use `makeCanonical` (as+ PandocPure's FileTree already does for its path-indexed map),+ which also handles duplicate and trailing slashes, replacing+ backslashes with slashes first.+ + Prevent `mediaPath` collisions between keys. The friendly+ mediaPath was derived by percent-unescaping the key, so+ distinct keys like `a%20b.png` and `a b.png` produced the same+ mediaPath ("a b.png") and silently clobbered each other on+ extraction (and inside docx/epub archives). Now the original name+ is only kept if the key contains no percent sign, so `mediaPath`+ equals the key and distinct keys yield distinct paths; anything+ percent-encoded gets a content-hash name. Hashed names can+ only coincide for identical contents, which is harmless.++ * Text.Pandoc.XML.Light:++ + Fix escaping of repeated `]]>` in `CDATA`.+ + Make `escStr` more efficient.+ + Avoid round-trips in `ppCDataS` prettify path.+ This makes `showCData` and `ppcCData` unused, so they are+ removed.+ + Parse XML fragments from the event stream instead of using+ xml-conduit's document parser, which requires a single root+ element. Our earlier woraround with a wrapper element was+ fragile. Behavior changes:++ - Content fragments with an XML declaration or DOCTYPE+ followed by multiple root elements, and text-only or empty+ input, now parse instead of erroring.+ - Attributes now preserve document order instead of being sorted+ alphabetically (the document parser stored them in a Map).+ - Errors for unresolved entities and mismatched tags now+ report source positions.++ + Use `text-builder` package for rendering, instead of text's+ lazy Text builder. On a large document this cuts docx conversion+ time by about 13%; output is byte-for-byte identical.+ + Expose `ppcTopElement` from Output.+ + ConfigPP now has a field `inlineTag` that checks for inline+ tags. Inline tags are printed on one line and not indented,+ by default. Export `useInlineTags`, `prettyConfigPP`.++ * Text.Pandoc.Sources:++ + Add bulk `takeWhileP`/`takeWhile1P` combinators [API change].+ Character streams over Sources previously had to be+ consumed one character at a time via `satisfy`, at a cost of+ several allocations and monadic binds per character. The new+ combinators scan a whole run of matching characters with a+ single parser invocation using `T.span`, while replicating the+ exact semantics of `T.pack <$> many/many1 (satisfy f)`,+ including empty-chunk handling, position updates at chunk+ boundaries, and parsec's error messages.+ + Use the new bulk `takeWhileP`/`takeWhile1P` combinators across+ readers.++ * Text.Pandoc.Translations:++ + `setTranslations` now keeps an already-loaded translation+ table when the language is unchanged, instead of+ unconditionally clearing the cache and forcing a re-read and+ re-parse of the translations YAML file on the next `translateTerm`.+ + Term names in translation files are now parsed with a+ precomputed Map lookup instead of the derived Read instance,+ which is very slow for a 22-constructor enum.++ * Text.Pandoc.Chunks:++ + Remove vestigial `nav-path` attribute and `rmNavAttrs` walk.+ This no longer did anything; output is unaffected.+ + Use `compactifyTable` for tables produced by all readers.+ Remove old ad hoc `paraToPlain` at the table cell level.+ This should ensure that we don't get tables that mix Plain+ and Para (#11864). Such tables tend to look funny when rendered in+ docx and other formats.++ * Text.Pandoc.Data:++ + Fix `getDataFileNames` with `-embed_data_files`. Previously+ it was not looking in the right directory and not+ recursing.++ * Text.Pandoc.Shared:++ + `taskListItemFromAscii`: Fix incorrect treatment of `[ ]` as+ checked.+ + Add `stringifyInlines`, a single-pass `stringify` for+ inlines [API change]. This is about 6x faster than `stringify`+ for long inline sequences+ + Speed up `stringify` by making it accumulate `[Text]`+ and concatenate once at the end, instead of mappending at every node.+ + Use `stringifyInlines` instead of `stringify` where possible.+ + Add new function `compactifyTable`. [API change]+ This converts cells that consist in a single Para block to a+ Plain, provided the table contains only such cells (or empty cells).+ + Drop expired entries in `decrementTrailingRowSpans`.+ Entries whose RowSpan fell to 0 were left in the map, relying+ on every consumer to guard against them. Delete them instead.+ + Recognize empty task list items in `toTaskListItem`.+ + Make `endsWithPlain` look inside DefinitionList.+ endsWithPlain recursed into the last item of BulletList and+ OrderedList but ignored DefinitionList, so list items ending+ with a compact definition list were treated as loose by the+ RST, Org, and Haddock writers.+ + Avoid Text -> String round-trips in `htmlAttrs`.+ This adds a FromText constraint to `htmlAttrs` and+ `tagWithAttrs` [API change].+ + Fuse traversals in `ensureValidXmlIdentifiers`.++ * Text.Pandoc.Writers.Shared:++ + Fix logic bug in `splitSentences`.+ + `lookupMetaBool`: treat empty block or inline list as False.+ + `htmlAttrs`: escape id and class attributes, like the others.+ + `stripLeadingTrailingSpace` - strip multiple Space, if present.+ + `toSubscript`: handle minus sign.+ + Fix `ensureValidXmlIdentifiers` for Figure and table+ sub-elements, resolving a bug that produced broken internal+ links in the HTML4/XHTML, EPUB, DocBook, TEI, ICML, FB2, and+ ODT writers.++ * Text.Pandoc.Parsing:++ + Improve performance of `uriScheme` by using a trie. Up to 15%+ faster in URL-heavy documents with `autolink_bare_uris`.+ + Minor code cleanup.+ + Remove `$` checks in math when delim is `\(` or `\\(`.+ + Make `anyOrderedListMarker` more efficient.++ * HTML template:++ + Dark mode support (#11831, Clar Fon).++ * reveal.js template:++ + Fix plugin paths (#11907).++ * flake.nix: parse allow-newer and allow-newer-deps in+ stack.yaml.++ * Make `embed_data_files` flag default to True. Remove flag+ settings from cabal.project. This makes it possible to+ override it on the command line.++ * Depend on commonmark 0.3.1, commonmark-extensions 0.2.7.3,+ commonmark-pandoc 0.3.0.2 (major performance improvements).++ * Depend on released asciidoc 0.1.1 (major performance+ improvements).++ * Use released texmath 0.13.3 (major performance improvements).++ * Depend on released djot 0.1.4.3 (major performance+ improvements).++ * Use released citeproc 0.14 (major performance improvements).++ * Use released doclayout 0.6.++ * Use released zip-archive 0.5 (major performance+ improvements).++ * Use released skylighting-0.15 (major performance+ improvements).++ * Depend on released doctemplates 0.11.1.++ * Depend on released typst 0.12.++ * Require text >= 2.0.++ * Bump upper bound for unicode-data.++ * Allow crypton 2.0.x.++ * Allow Diff 2.0.++ * Add `tools/diff-golden-tests.sh`.++ * Fix `tools/diff-zip.sh` on non-Darwin.++ * Add `tools/benchplot.js`. This creates a nice graph comparing two+ benchmarks.++ * `typst-properties.md`: fix fill syntax in Typst property+ examples (#11855, zenor0).++ * Remove tested-with from cabal file. We tend not to keep it up to date.++ * Fix typo in Lua filter example (#11875, Andonome).++ * Fix a bug in jats-reader.xml (duplicate attribute)++## pandoc 3.11 (2026-08-28)++ * Add `--math-method` option. This replaces (now deprecated but+ still functional) options `--mathml`, `--mathjax`, `--gladtex`,+ `--katex`, `--webtex`. The `plain` style can now be specified+ explicitly. In defaults files, `html-math-method` is now+ `math-method` (though `html-math-method` will still work).++ * Make `mathml` the default math-method (#11751).++ * Add `--completion={bash,zsh,fish}` (#8542, wzy).+ Make `--bash-completion` an alias of `--completion=bash`.+ See the manual for instructions on how to use these completions.++ * Text.Pandoc.Options [API change]:++ - Rename HTMLMathMethod type to MathMethod.+ - Rename `writerHTMLMathMethod` field of WriterOptions to+ `writerMathMethod`.++ * Markdown reader:++ + Add a syntax `(1@label)` for resetting example list counter+ (#10940). This is needed often at the beginning of a chapter.++ * MediaWiki writer:++ + Put `<nowiki>` around possible URLs (#11834). Otherwise they get+ linkified automatically.++ * Docx reader:++ + Handle case where non-header rows come before header rows (#11833).+ + Improve handling of tables with uneven rows (#11833).++ * RTF Reader:++ + Add support for nested tables (#11218, Alex).++ * Typst reader:++ + Remove handling of 'block' as an inline-level element (#11814).++ * HTML reader:++ + Handle `pre` without `code` (#11810). Preserve whitespace as+ nonbreaking spaces.++ * ODT/OpenDocument writers:++ + Support RTL text direction (#11301). RTL is now properly handled+ in these cases: `dir: rtl` (or `ltr`) in document metadata; an+ RTL `lang` in metadata (e.g. `he`, `ar`), unless overridden by+ `dir`; a `dir` attribute on a Div. Code blocks remain ltr+ regardless.+ + Use `style:language-complex` rather than `fo:language` for RTL+ languages (#11301).+ + Fix treatment of DefaultHighlighting (#11829).++ * Docx writer:++ + Honor CSL bibliography formatting hints emitted by citeproc (#11871).+ Bibliographies from styles with `hanging-indent="true"` (e.g. APA)+ now get a hanging indent, and CSL `line-spacing`/`entry-spacing`+ are mapped to paragraph spacing properties. + Initialize envLang from `lang` metadata (#11301). This ensures that setting `lang` will affect the whole document.
@@ -591,8 +591,6 @@ <w:style w:type="table" w:default="1" w:styleId="Table"> <w:name w:val="Table" /> <w:basedOn w:val="TableNormal" />- <w:semiHidden />- <w:unhideWhenUsed /> <w:qFormat /> <w:tblPr> <w:tblInd w:w="0" w:type="dxa" />
@@ -88,10 +88,28 @@ \makeatletter \patchcmd\longtable{\par}{\if@noskipsec\mbox{}\fi\par}{}{} \makeatother-% Allow footnotes in longtable head/foot-\IfFileExists{footnotehyper.sty}{\usepackage{footnotehyper}}{\usepackage{footnote}}-\makesavenoteenv{longtable} $endif$+$endif$+$--+$-- deferred footnotes (in tables, captions, etc.)+$--+$if(deferred-notes)$+% \pandocfootnotemark{n} saves the footnote mark and hyperref anchor;+% \pandocfootnotetext{n}{text} restores them, so the footnote text+% gets the right number and link target.+\makeatletter+\protected\def\pandocfootnotemark#1{\footnotemark{}%+ \expandafter\xdef\csname pandoc@fn@#1\endcsname{%+ \noexpand\pandoc@fnrestore+ {\unexpanded\expandafter{\@thefnmark}}%+ {\ifdefined\Hy@footnote@currentHref+ \unexpanded\expandafter{\Hy@footnote@currentHref}\fi}}}+\def\pandoc@fnrestore#1#2{%+ \gdef\@thefnmark{#1}%+ \ifdefined\Hy@footnote@currentHref\gdef\Hy@footnote@currentHref{#2}\fi}+\newcommand\pandocfootnotetext[1]{%+ \csname pandoc@fn@#1\endcsname\@footnotetext}+\makeatother $endif$ $-- $-- graphics
@@ -30,7 +30,7 @@ <link rel="stylesheet" href="$revealjs-url$/dist/theme/black.css" id="theme"> $endif$ $if(highlight-js)$- <link rel="stylesheet" href="$revealjs-url$/plugin/highlight/$highlightjs-theme$.css">+ <link rel="stylesheet" href="$revealjs-url$/dist/plugin/highlight/$highlightjs-theme$.css"> $endif$ $for(css)$ <link rel="stylesheet" href="$css$"/>@@ -84,14 +84,14 @@ <script src="$revealjs-url$/dist/reveal.js"></script> <!-- reveal.js plugins -->- <script src="$revealjs-url$/plugin/notes/notes.js"></script>- <script src="$revealjs-url$/plugin/search/search.js"></script>- <script src="$revealjs-url$/plugin/zoom/zoom.js"></script>+ <script src="$revealjs-url$/dist/plugin/notes.js"></script>+ <script src="$revealjs-url$/dist/plugin/search.js"></script>+ <script src="$revealjs-url$/dist/plugin/zoom.js"></script> $if(mathjax)$- <script src="$revealjs-url$/plugin/math/math.js"></script>+ <script src="$revealjs-url$/dist/plugin/math.js"></script> $endif$ $if(highlight-js)$- <script src="$revealjs-url$/plugin/highlight/highlight.js"></script>+ <script src="$revealjs-url$/dist/plugin/highlight.js"></script> $endif$ <script>
@@ -22,6 +22,18 @@ $highlighting-definitions$ $endif$+$if(csl-settings)$+// formatting called for by CSL style+#show <refs>: set par(+$if(csl-hanging-indent)$+ hanging-indent: $csl-hanging-indent$,+$endif$+$if(csl-entry-spacing)$+ spacing: $csl-entry-spacing$,+$endif$+)++$endif$ $if(template)$ #import "$template$": conf $else$
@@ -13,7 +13,10 @@ line-height: $linestretch$; $endif$ color: $if(fontcolor)$$fontcolor$$else$#1a1a1a$endif$;+ color: light-dark($if(fontcolor)$$fontcolor$$else$#1a1a1a$endif$, $if(fontcolordark)$$fontcolordark$$else$#fdfdfd$endif$); background-color: $if(backgroundcolor)$$backgroundcolor$$else$#fdfdfd$endif$;+ background-color: light-dark($if(backgroundcolor)$$backgroundcolor$$else$#fdfdfd$endif$, $if(backgroundcolordark)$$backgroundcolordark$$else$#1a1a1a$endif$);+ color-scheme:$if(fontcolor)$ light$elseif(backgroundcolor)$ light$elseif(fontcolordark)$$elseif(backgroundcolordark)$$else$ light$endif$$if(fontcolordark)$ dark$elseif(backgroundcolordark)$ dark$elseif(fontcolor)$$elseif(backgroundcolor)$$else$ dark$endif$; } body { margin: 0 auto;@@ -36,30 +39,16 @@ font-size: 1.8em; } }-@media print {- html {- background-color: $if(backgroundcolor)$$backgroundcolor$$else$white$endif$;- }- body {- background-color: transparent;- color: black;- }- p, h2, h3 {- orphans: 3;- widows: 3;- }- h2, h3, h4 {- page-break-after: avoid;- }-} p { margin: 1em 0; }-a {- color: $if(linkcolor)$$linkcolor$$else$#1a1a1a$endif$;-}-a:visited {- color: $if(linkcolor)$$linkcolor$$else$#1a1a1a$endif$;+a, a:visited {+ color: $if(linkcolor)$$linkcolor$$else$inherit$endif$;+$if(linkcolor)$+ color: light-dark($linkcolor$, $if(linkcolordark)$$linkcolordark$$else$inherit$endif$);+$elseif(linkcolordark)$+ color: light-dark(inherit, $linkcolordark$);+$endif$ } img { max-width: 100%;@@ -88,8 +77,11 @@ blockquote { margin: 1em 0 1em 1.7em; padding-left: 1em;- border-left: 2px solid #e6e6e6;- color: #606060;+ border-left: 2px solid;+ border-color: $if(quotebordercolor)$$quotebordercolor$$else$#e6e6e6$endif$;+ border-color: light-dark($if(quotebordercolor)$$quotebordercolor$$else$#e6e6e6$endif$, $if(quotebordercolordark)$$quotebordercolordark$$else$#4c4c4c$endif$);+ color: $if(quotecolor)$$quotecolor$$else$#606060$endif$;+ color: light-dark($if(quotecolor)$$quotecolor$$else$#606060$endif$, $if(quotecolordark)$$quotecolordark$$else$#b6b6b6$endif$); } $if(abstract)$ div.abstract {@@ -109,7 +101,12 @@ font-family: $if(monofont)$$monofont$$else$Menlo, Monaco, Consolas, 'Lucida Console', monospace$endif$; $if(monobackgroundcolor)$ background-color: $monobackgroundcolor$;+ background-color: light-dark($monobackgroundcolor$, $if(monobackgroundcolordark)$$monobackgroundcolordark$$else$inherit$endif$); padding: .2em .4em;+$elseif(monobackgroundcolordark)$+ background-color: inherit;+ background-color: light-dark(inherit, $monobackgroundcolordark$);+ padding: .2em .4em; $endif$ font-size: 85%; margin: 0;@@ -119,7 +116,12 @@ margin: 1em 0; $if(monobackgroundcolor)$ background-color: $monobackgroundcolor$;+ background-color: light-dark($monobackgroundcolor$, $if(monobackgroundcolordark)$$monobackgroundcolordark$$else$inherit$endif$); padding: 1em;+$elseif(monobackgroundcolordark)$+ background-color: inherit;+ background-color: light-dark(inherit, $monobackgroundcolordark$);+ padding: 1em; $endif$ overflow: auto; }@@ -134,7 +136,7 @@ } hr { border: none;- border-top: 1px solid #1a1a1a;+ border-top: 1px solid currentColor; height: 1px; margin: 1em 0; }@@ -156,11 +158,11 @@ } tbody { margin-top: 0.5em;- border-top: 1px solid $if(fontcolor)$$fontcolor$$else$#1a1a1a$endif$;- border-bottom: 1px solid $if(fontcolor)$$fontcolor$$else$#1a1a1a$endif$;+ border-top: 1px solid currentColor;+ border-bottom: 1px solid currentColor; } th {- border-top: 1px solid $if(fontcolor)$$fontcolor$$else$#1a1a1a$endif$;+ border-top: 1px solid currentColor; padding: 0.25em 0.5em 0.25em 0.5em; } td {@@ -184,6 +186,12 @@ } $endif$ span.smallcaps{font-variant: small-caps;}+em, em em em, em em em em em {+ font-style: italic;+}+em em, em em em em {+ font-style: normal;+} div.columns{display: flex; gap: 1.5em;} div.column{flex: auto;} @media screen {@@ -213,3 +221,24 @@ $if(csl-css)$ $styles.citations.html()$ $endif$+@media print {+ html {+ color-scheme: light only;+ }+ html, body, pre, code {+ background-color: transparent;+ }+ body, a, a:visited, blockquote {+ color: black;+ }+ pre, code {+ padding: 0;+ }+ p, h2, h3 {+ orphans: 3;+ widows: 3;+ }+ h2, h3, h4 {+ page-break-after: avoid;+ }+}
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: pandoc-version: 3.11+version: 3.14 build-type: Simple license: GPL-2.0-or-later license-file: COPYING.md@@ -11,25 +11,25 @@ stability: alpha homepage: https://pandoc.org category: Text-tested-with: GHC == 9.6.7, GHC == 9.8.4, GHC == 9.10.3, GHC == 9.12.2 synopsis: Conversion between markup formats description: Pandoc is a Haskell library for converting from one markup format to another. The formats it can handle include . - light markup formats (many variants of Markdown, reStructuredText, AsciiDoc, Org-mode, Muse, Textile,- txt2tags, djot)- - HTML formats (HTML 4 and 5)+ txt2tags, djot, bbcode)+ - HTML formats (HTML 4 and 5, chunked HTML) - Ebook formats (EPUB v2 and v3, FB2) - Documentation formats (GNU TexInfo, Haddock, Vimdoc)- - Roff formats (man, ms)+ - Roff formats (man, ms, mdoc) - TeX formats (LaTeX, ConTeXt) - Typst+ - ANSI-formatted text - XML formats (DocBook 4 and 5, JATS, TEI Simple, OpenDocument) - Outline formats (OPML) - Bibliography formats (BibTeX, BibLaTeX, CSL JSON, CSL YAML, RIS)- - Word processor formats (Docx, RTF, ODT)+ - Word processor formats (Docx, RTF, ODT, FODT) - Interactive notebook formats (Jupyter notebook ipynb) - Page layout formats (InDesign ICML) - Wiki markup formats (MediaWiki, DokuWiki, TikiWiki, TWiki,@@ -37,6 +37,8 @@ - Slide show formats (LaTeX Beamer, PowerPoint, Slidy, reveal.js, Slideous, S5, DZSlides) - Data formats (CSV and TSV tables, Excel spreadsheets)+ - Serialization formats (Haskell, JSON, or XML representation+ of the AST) - PDF (via external programs such as pdflatex or wkhtmltopdf) . Pandoc can convert mathematical content in documents@@ -219,6 +221,7 @@ test/command/*.md test/command/*.csl test/command/*.svg+ test/command/2623.odt test/command/7691.docx test/command/9391.docx test/command/9358.docx@@ -227,6 +230,13 @@ test/command/11113.docx test/command/11689.docx test/command/11833.docx+ test/command/11944.docx+ test/command/11989.docx+ test/command/11991.docx+ test/command/11988.docx+ test/command/9222.docx+ test/command/9183.docx+ test/command/11942.js test/command/biblio.bib test/command/averroes.bib test/command/A.txt@@ -281,6 +291,14 @@ test/command/11486/scroll.revealjs test/command/11498.png test/command/11301-styles.opendocument+ test/command/opendocument-abstract.opendocument+ test/command/opendocument-body.opendocument+ test/command/11888/section1/lab.md+ test/command/11888/section2/lab.md+ test/command/11888/section3/lab.md+ test/command/12001/note/doc.md+ test/command/12001/grid/doc.md+ test/command/12001/next/doc.md test/asciidoc-reader.adoc test/asciidoc-reader.native test/asciidoc-reader-include.rb@@ -438,8 +456,8 @@ test/pptx/*.pptx test/pptx/**/*.pptx test/pptx/**/*.native- test/pptx-reader/basic.pptx- test/pptx-reader/basic.native+ test/pptx-reader/*.pptx+ test/pptx-reader/*.native test/ipynb/*.native test/ipynb/*.in.native test/ipynb/*.out.native@@ -450,20 +468,25 @@ test/twiki-reader.twiki test/tikiwiki-reader.tikiwiki test/odt/odt/*.odt+ test/odt/fodt/*.fodt test/odt/markdown/*.md test/odt/native/*.native+ test/odt/fodt-native/*.native test/xlsx-reader/*.xlsx test/xlsx-reader/*.native test/pod-reader.pod test/vimdoc/*.markdown test/vimdoc/*.vimdoc+ benchmark/sample.md+ benchmark/movie.jpg+ benchmark/lalune.jpg source-repository head type: git location: https://github.com/jgm/pandoc.git flag embed_data_files Description: Embed data files in binary for relocatable executable.- Default: False+ Default: True flag http Description: Support for fetching resources using HTTP.@@ -498,11 +521,13 @@ library xml-light import: common-options- build-depends: xml >= 1.3.12 && < 1.4,+ build-depends: conduit >= 1.3 && < 1.4,+ xml >= 1.3.12 && < 1.4, xml-conduit >= 1.9.1.1 && < 1.11, xml-types >= 0.3 && < 0.4, containers >= 0.6.0.1 && < 0.9,- text >= 1.1.1.0 && < 2.2+ text >= 2.0 && < 2.2,+ text-builder >= 1.0 && < 1.1 hs-source-dirs: xml-light exposed-modules: Text.Pandoc.XML.Light,@@ -525,22 +550,22 @@ blaze-markup >= 0.8 && < 0.9, bytestring >= 0.9 && < 0.13, case-insensitive >= 1.2 && < 1.3,- citeproc >= 0.13.0.1 && < 0.14,- commonmark >= 0.3 && < 0.4,- commonmark-extensions >= 0.2.7.1 && < 0.3,- commonmark-pandoc >= 0.3 && < 0.4,+ citeproc >= 0.14.2 && < 0.15,+ commonmark >= 0.3.2 && < 0.4,+ commonmark-extensions >= 0.2.8 && < 0.3,+ commonmark-pandoc >= 0.3.0.2 && < 0.4, containers >= 0.6.0.1 && < 0.9,- crypton >= 0.30 && < 1.2,+ crypton >= 1.0 && < 2.3, data-default >= 0.4 && < 0.9, deepseq >= 1.3 && < 1.6,- directory >= 1.2.3 && < 1.4,- doclayout >= 0.5.0.3 && < 0.6,- doctemplates >= 0.11 && < 0.12,- emojis >= 0.1.5 && < 0.2,+ directory >= 1.2.5 && < 1.4,+ doclayout >= 0.6 && < 0.7,+ doctemplates >= 0.11.1 && < 0.12,+ emojis >= 0.1.6 && < 0.2, exceptions >= 0.8 && < 0.11, file-embed >= 0.0 && < 0.1, filepath >= 1.1 && < 1.6,- gridtables >= 0.1 && < 0.2,+ gridtables >= 0.1.1.1 && < 0.2, haddock-library >= 1.10 && < 1.12, http-types >= 0.8 && < 0.13, ipynb >= 0.2 && < 0.3,@@ -550,34 +575,32 @@ network-uri >= 2.6 && < 2.8, pandoc-types >= 1.23.1.2 && < 1.24, parsec >= 3.1 && < 3.2,- pretty >= 1.1 && < 1.2,- pretty-show >= 1.10 && < 1.11, process >= 1.2.3 && < 1.7, random >= 1.2 && < 1.4, safe >= 0.3.18 && < 0.4, scientific >= 0.3 && < 0.4,- skylighting >= 0.14.7 && < 0.15,- skylighting-core >= 0.14.7 && < 0.15,+ skylighting >= 0.15.0.1 && < 0.16,+ skylighting-core >= 0.15.0.1 && < 0.16, split >= 0.2 && < 0.3, syb >= 0.1 && < 0.8, tagsoup >= 0.14.6 && < 0.15, temporary >= 1.1 && < 1.4,- texmath >= 0.13.2.2 && < 0.14,- text >= 1.1.1.0 && < 2.2,+ texmath >= 0.13.3.5 && < 0.14,+ text >= 2.0 && < 2.2, text-conversions >= 0.3 && < 0.4, time >= 1.5 && < 1.17, unicode-collation >= 0.1.1 && < 0.2,- unicode-data >= 0.6 && < 0.9,+ unicode-data >= 0.6 && < 0.10, unicode-transforms >= 0.3 && < 0.5,- yaml >= 0.11 && < 0.12,- libyaml >= 0.1.4 && < 0.2,- zip-archive >= 0.4.3.1 && < 0.5,- zlib >= 0.5 && < 0.8,+ yamlet >= 1.0.0.1 && < 1.1,+ yamlet-aeson >= 1.0.0.1 && < 1.1,+ zip-archive >= 0.5 && < 0.6,+ zlib >= 0.6 && < 0.8, xml >= 1.3.12 && < 1.4,- typst >= 0.11.0.1 && < 0.12,+ typst >= 0.12.1 && < 0.13, vector >= 0.12 && < 0.14,- djot >= 0.1.4.2 && < 0.2,- asciidoc >= 0.1.0.5 && < 0.2+ djot >= 0.1.4.3 && < 0.2,+ asciidoc >= 0.1.1 && < 0.2 if !os(windows) build-depends: unix >= 2.4 && < 2.9@@ -588,11 +611,11 @@ cpp-options: -DPANDOC_HTTP_SUPPORT build-depends: crypton-connection >= 0.3.1 && < 0.5,- crypton-x509-system >= 1.6.7 && < 1.10,+ crypton-x509-system >= 1.6.7 && < 1.11, http-client >= 0.4.30 && < 0.8, http-client-tls >= 0.2.4 && < 0.5, network >= 2.6 && < 3.3,- tls >= 2.0.1 && < 2.5+ tls >= 2.0.1 && < 2.6 hs-source-dirs: src exposed-modules: Text.Pandoc,@@ -724,6 +747,7 @@ Text.Pandoc.App.Opt, Text.Pandoc.App.OutputSettings, Text.Pandoc.Class.CommonState,+ Text.Pandoc.Class.IO.HTTP, Text.Pandoc.Class.PandocMonad, Text.Pandoc.Class.PandocIO, Text.Pandoc.Class.PandocPure,@@ -747,6 +771,7 @@ Text.Pandoc.Readers.Docx.Symbols, Text.Pandoc.Readers.Docx.Fields, Text.Pandoc.Readers.OOXML.Shared,+ Text.Pandoc.Readers.OOXML.Charts, Text.Pandoc.Readers.Pptx.Parse, Text.Pandoc.Readers.Pptx.Shapes, Text.Pandoc.Readers.Pptx.Slides,@@ -771,7 +796,6 @@ Text.Pandoc.Readers.Mdoc.Standards, Text.Pandoc.Readers.Typst.Parsing, Text.Pandoc.Readers.Typst.Math,- Text.Pandoc.Readers.ODT.Base, Text.Pandoc.Readers.ODT.Namespaces, Text.Pandoc.Readers.ODT.StyleReader, Text.Pandoc.Readers.ODT.ContentReader,@@ -780,8 +804,6 @@ Text.Pandoc.Readers.ODT.Generic.Utils, Text.Pandoc.Readers.ODT.Generic.Namespaces, Text.Pandoc.Readers.ODT.Generic.XMLConverter,- Text.Pandoc.Readers.ODT.Arrows.State,- Text.Pandoc.Readers.ODT.Arrows.Utils, Text.Pandoc.Readers.Org.BlockStarts, Text.Pandoc.Readers.Org.Blocks, Text.Pandoc.Readers.Org.DocumentTree,@@ -843,12 +865,12 @@ main-is: test-pandoc.hs hs-source-dirs: test build-depends: pandoc,- Diff >= 0.2 && < 1.1,+ Diff >= 0.2 && < 2.1, Glob >= 0.7 && < 0.11, bytestring >= 0.9 && < 0.13, containers >= 0.4.2.1 && < 0.9,- directory >= 1.2.3 && < 1.4,- doctemplates >= 0.11 && < 0.12,+ directory >= 1.2.5 && < 1.4,+ doctemplates >= 0.11.1 && < 0.12, filepath >= 1.1 && < 1.6, mtl >= 2.2 && < 2.4, pandoc-types >= 1.23.1 && < 1.24,@@ -857,17 +879,20 @@ tasty-golden >= 2.3 && < 2.4, tasty-hunit >= 0.9 && < 0.11, tasty-quickcheck >= 0.8 && < 0.12,- text >= 1.1.1.0 && < 2.2,+ text >= 2.0 && < 2.2, temporary >= 1.1 && < 1.4, time >= 1.5 && < 1.17, xml >= 1.3.12 && < 1.4,- zip-archive >= 0.4.3 && < 0.5+ xml-light,+ zip-archive >= 0.5 && < 0.6 other-modules: Tests.Old Tests.Command Tests.Helpers Tests.Shared Tests.MediaBag+ Tests.Data Tests.XML+ Tests.ImageSize Tests.Readers.LaTeX Tests.Readers.HTML Tests.Readers.JATS@@ -914,6 +939,7 @@ Tests.Writers.AsciiDoc Tests.Writers.LaTeX Tests.Writers.Docx+ Tests.Writers.ODT Tests.Writers.RST Tests.Writers.TEI Tests.Writers.Markua@@ -932,9 +958,10 @@ main-is: benchmark-pandoc.hs hs-source-dirs: benchmark build-depends: bytestring,- tasty-bench >= 0.4 && <= 0.5,+ tasty-bench >= 0.4 && <= 0.6, mtl >= 2.2 && < 2.4,- text >= 1.1.1.0 && < 2.2,+ text >= 2.0 && < 2.2,+ temporary, deepseq -- we increase heap size to avoid benchmarking garbage collection: ghc-options: -rtsopts -with-rtsopts=-A8m -threaded
@@ -42,7 +42,7 @@ import Safe (tailDef) import Skylighting (Syntax (..), defaultSyntaxMap) import System.Console.GetOpt-import System.Environment (getArgs, getProgName)+import System.Environment (getArgs, getProgName, lookupEnv) import System.Exit (exitSuccess) import System.FilePath import System.IO (stdout)@@ -1457,10 +1457,12 @@ let scriptingLine = case mbScriptingEngineName of Nothing -> [] Just name -> ["Scripting engine: " ++ name]+ mbPandocDirs <- System.Environment.lookupEnv "PANDOC_DATA_DIRS" UTF8.putStr $ T.unlines $ map T.pack $ ["pandoc " ++ showVersion pandocVersion ++ suffix] ++ featuresLine ++ scriptingLine ++- ["User data directory: " ++ defaultDatadir,- copyrightMessage]+ ["User data directory: " ++ defaultDatadir] +++ maybe [] (\d -> ["PANDOC_DATA_DIRS: " ++ d]) mbPandocDirs +++ [copyrightMessage] exitSuccess
@@ -16,6 +16,7 @@ import Control.Monad ((>=>), when) import Control.Monad.Except (throwError, catchError)+import Data.Char (toLower) import Data.Text (Text) import Network.URI (URI (..), parseURI) import Text.Pandoc.Transforms (adjustLinksAndIds)@@ -89,8 +90,9 @@ readSource "-" = (,Nothing) <$> readStdinStrict readSource src = case parseURI src of- Just u | uriScheme u `elem` ["http:","https:"] -> openURL (T.pack src)- | uriScheme u == "file:" ->+ Just u | map toLower (uriScheme u) `elem` ["http:","https:"] ->+ openURL (T.pack src)+ | map toLower (uriScheme u) == "file:" -> (,Nothing) <$> readFileStrict (uriPathToPath $ T.pack $ uriPath u) _ -> (,Nothing) <$> readFileStrict src
@@ -1,6 +1,5 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE TemplateHaskell #-}@@ -53,7 +52,8 @@ CiteMethod (Citeproc), pattern DefaultHighlightingString) import Text.Pandoc.Class (readFileStrict, fileExists, setVerbosity, report,- PandocMonad(lookupEnv), getUserDataDir)+ PandocMonad(lookupEnv), getUserDataDir,+ getPandocDataDirs) import Text.Pandoc.Error (PandocError (PandocParseError, PandocSomeError)) import Data.Containers.ListUtils (nubOrd) import Text.Pandoc.Data (defaultUserDataDir)@@ -67,11 +67,15 @@ import qualified Data.Map as M import qualified Data.ByteString.Char8 as B8 import Text.Pandoc.Definition (Meta(..), MetaValue(..))-import Data.Aeson (defaultOptions, Options(..), Result(..),- genericToJSON, fromJSON, camelTo2, eitherDecodeStrict) import Data.Aeson.TH (deriveJSON) import Control.Applicative ((<|>))-import Data.Yaml+import Data.Aeson+import Data.Aeson.Types+import qualified Data.Aeson.Key as K+import qualified Data.List as L+import qualified Data.List.NonEmpty as NE+import qualified Yamlet as Y+import Yamlet.Aeson -- | The type of line-endings to be used when writing plain-text. data LineEnding = LF | CRLF | Native deriving (Show, Generic)@@ -357,11 +361,11 @@ Success (opts :: M.Map Text Value) -> do dataDir <- case M.lookup "data-dir" opts of Nothing -> return Nothing- Just v -> Just . unpack <$> parseJSON v+ Just v -> Just . unpack <$> parseJSON v <?> Key "data-dir" f <- parseOptions (M.toList opts) case M.lookup "defaults" opts of Just v -> do- g <- parseDefaults v dataDir+ g <- parseDefaults v dataDir <?> Key "defaults" return $ g >=> f >=> resolveVarsInOpt Nothing -> return $ f >=> resolveVarsInOpt parseJSON _ = fail "Expected a mapping"@@ -541,7 +545,7 @@ _ -> doOpt (k,v) doOpt :: (Text, Value) -> Parser (Opt -> Opt)-doOpt (k,v) = do+doOpt (k,v) = (<?> Key (K.fromText k)) $ do case k of "tab-stop" -> parseJSON v >>= \x -> return (\o -> o{ optTabStop = x })@@ -938,11 +942,11 @@ Left err' -> throwError $ PandocParseError $ T.pack $ "Error parsing " <> file <> ":\n" <> err' else- case decodeEither' (B8.unlines . takeWhile (/= "...") . B8.lines $ inp) of- Right f -> f opt- Left err' -> throwError $ PandocParseError $ T.pack $+ case Y.decode (B8.unlines . takeWhile (/= "...") . B8.lines $ inp) of+ Right (ViaAeson f) -> f opt+ Left errs -> throwError $ PandocParseError $ T.pack $ "Error parsing " <> file <> ":\n" <>- Data.Yaml.prettyPrintParseException err'+ L.intercalate "\n" (map (Y.prettyError file) (NE.toList errs)) fullDefaultsPath :: (PandocMonad m, MonadIO m) => Maybe FilePath@@ -950,17 +954,16 @@ -> m FilePath fullDefaultsPath dataDir file = do defaultDataDir <- liftIO defaultUserDataDir- let ddir = fromMaybe defaultDataDir dataDir </> "defaults"+ pandocDataDirs <- getPandocDataDirs+ let ddirs = map (</> "defaults") (fromMaybe defaultDataDir dataDir : pandocDataDirs) let findFile [] = return file findFile (fp:fps) = do fpExists <- fileExists fp if fpExists then return fp else findFile fps- findFile [file,- file <.> "yaml",- ddir </> file,- ddir </> file <.> "yaml"]+ findFile $ [file, file <.> "yaml"] +++ concatMap (\ddir -> [ddir </> file, ddir </> file <.> "yaml"]) ddirs -- | In a list of lists, append another list in front of every list which -- starts with specific element.
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-}@@ -32,7 +31,8 @@ import Data.List (find) import Data.Maybe (catMaybes, fromMaybe) import Skylighting (defaultSyntaxMap)-import Skylighting.Parser (addSyntaxDefinition, parseSyntaxDefinition)+import Skylighting.Parser (addSyntaxDefinition, parseSyntaxDefinition,+ resolveKeywords) import System.Directory (getCurrentDirectory) import System.Exit (exitSuccess) import System.FilePath@@ -161,10 +161,13 @@ res <- liftIO (parseSyntaxDefinition f) case res of Left errstr -> throwError $ PandocSyntaxMapError $ T.pack errstr- Right syn -> return $ addSyntaxDefinition syn existingmap+ Right syn -> return $+ addSyntaxDefinition syn existingmap - syntaxMap <- foldM addSyntaxMap defaultSyntaxMap- (optSyntaxDefinitions opts)+ syntaxMap <- case optSyntaxDefinitions opts of+ [] -> return defaultSyntaxMap+ sdefs -> (\m -> M.map (resolveKeywords m) m) <$>+ foldM addSyntaxMap defaultSyntaxMap sdefs hlStyle <- case optSyntaxHighlighting opts of NoHighlightingString -> pure NoHighlighting
@@ -28,8 +28,8 @@ ) where import Text.Pandoc.Definition-import Text.Pandoc.Shared (makeSections, stringify, inlineListToIdentifier,- tshow, uniqueIdent)+import Text.Pandoc.Shared (makeSections, stringifyInlines,+ inlineListToIdentifier, tshow, uniqueIdent) import Text.Pandoc.Walk (Walkable(..), query) import Data.Aeson (FromJSON, ToJSON) import Data.Text (Text)@@ -57,8 +57,7 @@ splitIntoChunks pathTemplate numberSections mbBaseLevel chunklev (Pandoc meta blocks) = addNav .- fixInternalReferences .- walk rmNavAttrs $+ fixInternalReferences $ ChunkedDoc{ chunkedMeta = meta , chunkedChunks = chunks , chunkedTOC = tocTree }@@ -214,12 +213,11 @@ , chunkPrev = Nothing , chunkUnlisted = "unlisted" `elem` classes , chunkContents =- [Div (divid,"section":classes,kvs') (h : bs)]+ [Div (divid,"section":classes,kvs) (h : bs)] }- where kvs' = kvs ++ [("nav-path", T.pack chunkpath)]- secnum = lookup "number" kvs+ where secnum = lookup "number" kvs chunkpath = resolvePathTemplate pathTemplate chunknum- (stringify ils)+ (stringifyInlines ils) divid (fromMaybe "" secnum) toChunk chunknum (Div ("",["preamble"],[]) bs) =@@ -238,7 +236,7 @@ } where chunkpath = resolvePathTemplate pathTemplate chunknum- (stringify (docTitle meta))+ (stringifyInlines (docTitle meta)) chunkid "0" chunkid = inlineListToIdentifier mempty (docTitle meta) <>@@ -246,14 +244,6 @@ toChunk _ b = error $ "toChunk called on inappropriate block " <> show b -- should not happen ---- Remove some attributes we added just to construct chunkNext etc.-rmNavAttrs :: Block -> Block-rmNavAttrs (Div (ident,classes,kvs) bs) =- Div (ident,classes,filter (not . isNavAttr) kvs) bs- where- isNavAttr (k,_) = "nav-" `T.isPrefixOf` k-rmNavAttrs b = b resolvePathTemplate :: PathTemplate -> Int -- ^ Chunk number
@@ -24,13 +24,14 @@ import qualified Text.Pandoc.Builder as B import Text.Pandoc.Definition as Pandoc import Text.Pandoc.Class (PandocMonad(..), getResourcePath, getUserDataDir,+ getPandocDataDirs, fetchItem, report, setResourcePath, toTextM) import Text.Pandoc.Data (readDataFile) import Text.Pandoc.Error (PandocError(..)) import Text.Pandoc.Extensions (pandocExtensions) import Text.Pandoc.Logging (LogMessage(..)) import Text.Pandoc.Options (ReaderOptions(..))-import Text.Pandoc.Shared (stringify, tshow, makeSections)+import Text.Pandoc.Shared (stringifyInlines, tshow, makeSections) import Data.Containers.ListUtils (nubOrd) import Text.Pandoc.Walk (query, walk, walkM) import Control.Applicative ((<|>))@@ -132,9 +133,10 @@ let getFile defaultExtension fp = do oldRp <- getResourcePath mbUdd <- getUserDataDir- setResourcePath $ oldRp ++ maybe []- (\u -> [u <> "/csl",- u <> "/csl/dependent"]) mbUdd+ pandocDataDirs <- getPandocDataDirs+ setResourcePath $ oldRp +++ concatMap (\u -> [u <> "/csl", u <> "/csl/dependent"])+ (maybe id (:) mbUdd pandocDataDirs) let fp' = if T.any (=='.') fp || "data:" `T.isPrefixOf` fp then fp else fp <> defaultExtension@@ -422,7 +424,7 @@ mvPunct moveNotes locale (q : s : x@(Cite _ [il]) : ys) | isSpacy s , isNote il- = let spunct = T.takeWhile isPunct $ stringify ys+ = let spunct = T.takeWhile isPunct $ stringifyInlines ys in if moveNotes then if T.null spunct then q : x : mvPunct moveNotes locale ys@@ -436,7 +438,7 @@ | isNote (last ils) , startWithPunct ys , moveNotes- = let s = stringify ys+ = let s = stringifyInlines ys spunct = T.takeWhile isPunct s in Cite cs (movePunctInsideQuotes locale $ init ils@@ -451,7 +453,7 @@ mvPunct moveNotes locale (s : x@(Cite _ (Superscript _ : _)) : ys) | isSpacy s = x : mvPunct moveNotes locale ys mvPunct moveNotes locale (Cite cs ils : Str "." : ys)- | "." `T.isSuffixOf` (stringify ils)+ | "." `T.isSuffixOf` (stringifyInlines ils) = Cite cs ils : mvPunct moveNotes locale ys mvPunct moveNotes locale (x:xs) = x : mvPunct moveNotes locale xs mvPunct _ _ [] = []@@ -464,7 +466,7 @@ endWithPunct :: Bool -> [Inline] -> Bool endWithPunct _ [] = False endWithPunct onlyFinal xs@(_:_) =- case reverse (T.unpack $ stringify xs) of+ case reverse (T.unpack $ stringifyInlines xs) of [] -> True -- covers .), .", etc.: (d:c:_) | isPunct d@@ -478,15 +480,15 @@ startWithPunct :: [Inline] -> Bool startWithPunct ils =- case T.uncons (stringify ils) of+ case T.uncons (stringifyInlines ils) of Just (c,_) -> c `elem` (".,;:!?" :: [Char]) Nothing -> False truish :: MetaValue -> Bool truish (MetaBool t) = t truish (MetaString s) = isYesValue (T.toLower s)-truish (MetaInlines ils) = isYesValue (T.toLower (stringify ils))-truish (MetaBlocks [Plain ils]) = isYesValue (T.toLower (stringify ils))+truish (MetaInlines ils) = isYesValue (T.toLower (stringifyInlines ils))+truish (MetaBlocks [Plain ils]) = isYesValue (T.toLower (stringifyInlines ils)) truish _ = False isYesValue :: Text -> Bool@@ -637,6 +639,8 @@ case lastMay ils of Just (Span attr ils') -> initSafe ils ++ [Span attr (removeFinalPeriod ils')]+ Just (Link attr ils' tgt)+ -> initSafe ils ++ [Link attr (removeFinalPeriod ils') tgt] Just (Emph ils') -> initSafe ils ++ [Emph (removeFinalPeriod ils')] Just (Strong ils')
@@ -29,7 +29,7 @@ import Text.Pandoc.Extensions (Extension(..), extensionsFromList) import Text.Pandoc.Options (ReaderOptions(..), WriterOptions) import Text.Pandoc.Error (PandocError)-import Text.Pandoc.Shared (stringify)+import Text.Pandoc.Shared (stringify, stringifyInlines) import Text.Pandoc.Writers.LaTeX (writeLaTeX) import Text.Pandoc.Class (runPure) import qualified Text.Pandoc.Walk as Walk@@ -262,7 +262,7 @@ getVariable v = lookupVariable (toVariable v) ref - getVariableAsText v = (stringify . valToInlines) <$> getVariable v+ getVariableAsText v = (stringifyInlines . valToInlines) <$> getVariable v getYear val = case val of@@ -340,7 +340,7 @@ getContentsFor x = getVariable x >>= if isURL x- then Just . literal . stringify . valToInlines+ then Just . literal . stringifyInlines . valToInlines else toLaTeX . (if x == "title" then titlecase@@ -381,7 +381,7 @@ -- hyphenation: let getLangId = do langid <- T.strip . T.toLower <$> getRawField "langid"- idopts <- T.strip . T.toLower . stringify <$>+ idopts <- T.strip . T.toLower . stringifyInlines <$> getField "langidopts" <|> return "" case (langid, idopts) of ("english","variant=british") -> return "british"@@ -842,14 +842,11 @@ updateState (\(l,m) -> (l, Map.insert k v m)) return () -take1WhileP :: Monad m => (Char -> Bool) -> ParsecT Sources u m Text-take1WhileP f = T.pack <$> many1 (satisfy f)- inBraces :: BibParser Text inBraces = do char '{' res <- manyTill- ( take1WhileP (\c -> c /= '{' && c /= '}' && c /= '\\')+ ( takeWhile1P (\c -> c /= '{' && c /= '}' && c /= '\\') <|> (char '\\' >> T.cons '\\' . T.singleton <$> anyChar) <|> (braced <$> inBraces) ) (char '}')@@ -862,14 +859,14 @@ inQuotes = do char '"' T.concat <$> manyTill- ( take1WhileP (\c -> c /= '{' && c /= '"' && c /= '\\')+ ( takeWhile1P (\c -> c /= '{' && c /= '"' && c /= '\\') <|> (char '\\' >> T.cons '\\' . T.singleton <$> anyChar) <|> braced <$> inBraces ) (char '"') fieldName :: BibParser Text fieldName = resolveAlias . T.toLower- <$> take1WhileP (\c ->+ <$> takeWhile1P (\c -> isAlphaNum c || c == '-' || c == '_' || c == ':' || c == '+') isBibtexKeyChar :: Char -> Bool@@ -883,11 +880,11 @@ bibItem = do char '@' pos <- getPosition- enttype <- T.toLower <$> take1WhileP isLetter+ enttype <- T.toLower <$> takeWhile1P isLetter spaces' char '{' spaces'- entid <- take1WhileP isBibtexKeyChar+ entid <- takeWhile1P isBibtexKeyChar spaces' char ',' spaces'@@ -913,7 +910,7 @@ resolveAlias s = s rawWord :: BibParser Text-rawWord = take1WhileP isAlphaNum+rawWord = takeWhile1P isAlphaNum expandString :: BibParser Text expandString = do@@ -1039,17 +1036,17 @@ getOldDate :: Text -> Bib Date getOldDate prefix = do- year' <- (readMay . T.unpack . fixLeadingDash . stringify+ year' <- (readMay . T.unpack . fixLeadingDash . stringifyInlines <$> getField (prefix <> "year")) <|> return Nothing month' <- (parseMonth <$> getRawField (prefix <> "month")) <|> return Nothing day' <- (readMay . T.unpack <$> getRawField (prefix <> "day")) <|> return Nothing- endyear' <- (readMay . T.unpack . fixLeadingDash . stringify+ endyear' <- (readMay . T.unpack . fixLeadingDash . stringifyInlines <$> getField (prefix <> "endyear")) <|> return Nothing- endmonth' <- (parseMonth . stringify+ endmonth' <- (parseMonth . stringifyInlines <$> getField (prefix <> "endmonth")) <|> return Nothing- endday' <- (readMay . T.unpack . stringify <$>+ endday' <- (readMay . T.unpack . stringifyInlines <$> getField (prefix <> "endday")) <|> return Nothing let toDateParts (y', m', d') = DateParts $
@@ -14,7 +14,7 @@ import qualified Data.List as L import Text.Pandoc.Definition import Text.Pandoc.Parsing-import Text.Pandoc.Shared (stringify)+import Text.Pandoc.Shared (stringify, stringifyInlines) import Control.Monad (mzero) import qualified Data.Map as M import Data.Char (isSpace, isPunctuation, isDigit)@@ -152,7 +152,7 @@ -- the pathological case is "p.3" t <- anyToken ts <- manyTill anyToken (try $ lookAhead lim)- let s = acc <> stringify (t:ts)+ let s = acc <> stringifyInlines (t:ts) case M.lookup (T.toCaseFold $ T.strip s) (unLocatorMap locMap) of -- try to find a longer one, or return this one Just l -> go s <|> return (s, l, False)@@ -239,7 +239,7 @@ notFollowedBy pLocatorPunct >> notFollowedBy pMath >> anyToken)- let s = stringify ts+ let s = stringifyInlines ts -- otherwise look for actual digits or -s return (T.any isDigit s, s)
@@ -9,7 +9,7 @@ import Citeproc.Types import Text.Pandoc.Definition import Text.Pandoc.Builder as B-import Text.Pandoc.Shared (stringify, blocksToInlines')+import Text.Pandoc.Shared (stringify, stringifyInlines, blocksToInlines') import Data.Maybe import Safe import qualified Data.Set as Set@@ -21,7 +21,7 @@ metaValueToText :: MetaValue -> Maybe Text metaValueToText (MetaString t) = Just t-metaValueToText (MetaInlines ils) = Just $ stringify ils+metaValueToText (MetaInlines ils) = Just $ stringifyInlines ils metaValueToText (MetaBlocks bls) = Just $ stringify bls metaValueToText (MetaList xs) = T.unwords <$> mapM metaValueToText xs metaValueToText _ = Nothing@@ -31,7 +31,7 @@ metaValueToBool (MetaString "true") = Just True metaValueToBool (MetaString "false") = Just False metaValueToBool (MetaInlines ils) =- metaValueToBool (MetaString (stringify ils))+ metaValueToBool (MetaString (stringifyInlines ils)) metaValueToBool _ = Nothing referenceToMetaValue :: Reference Inlines -> MetaValue@@ -188,7 +188,7 @@ metaValueToNames x = maybeToList $ metaValueToName x metaValueToName :: MetaValue -> Maybe Name-metaValueToName (MetaMap m) = extractParticles <$>+metaValueToName (MetaMap m) = Just Name { nameFamily = family , nameGiven = given@@ -212,7 +212,7 @@ staticordering = fromMaybe False $ M.lookup "static-ordering" m >>= metaValueToBool literal = M.lookup "literal" m >>= metaValueToText-metaValueToName x = extractParticles <$>+metaValueToName x = case metaValueToText x of Nothing -> Nothing Just lit -> Just Name
@@ -23,7 +23,7 @@ where import Text.Pandoc.Definition-import Text.Pandoc.Shared (stringify)+import Text.Pandoc.Shared (stringifyInlines) import Citeproc.Types import Citeproc.Pandoc () import Text.Pandoc.Citeproc.Util (splitStrWhen)@@ -62,7 +62,7 @@ toName _ [Str "others"] = return emptyName{ nameLiteral = Just "others" } toName _ [Span ("",[],[]) ils] = -- corporate author- return emptyName{ nameLiteral = Just $ stringify ils }+ return emptyName{ nameLiteral = Just $ stringifyInlines ils } -- extended BibLaTeX name format - see #266 toName _ ils@(Str ys:_) | T.any (== '=') ys = do let commaParts = splitWhen (== Str ",")@@ -70,17 +70,17 @@ $ ils let addPart ag (Str "given" : Str "=" : xs) = ag{ nameGiven = case nameGiven ag of- Nothing -> Just $ stringify xs- Just t -> Just $ t <> " " <> stringify xs }+ Nothing -> Just $ stringifyInlines xs+ Just t -> Just $ t <> " " <> stringifyInlines xs } addPart ag (Str "family" : Str "=" : xs) =- ag{ nameFamily = Just $ stringify xs }+ ag{ nameFamily = Just $ stringifyInlines xs } addPart ag (Str "prefix" : Str "=" : xs) =- ag{ nameDroppingParticle = Just $ stringify xs }+ ag{ nameDroppingParticle = Just $ stringifyInlines xs } addPart ag (Str "useprefix" : Str "=" : Str "true" : _) = ag{ nameNonDroppingParticle = nameDroppingParticle ag , nameDroppingParticle = Nothing } addPart ag (Str "suffix" : Str "=" : xs) =- ag{ nameSuffix = Just $ stringify xs }+ ag{ nameSuffix = Just $ stringifyInlines xs } addPart ag (Space : xs) = addPart ag xs addPart ag _ = ag return $ L.foldl' addPart emptyName commaParts@@ -117,10 +117,10 @@ case break isCapitalized vonlast of (vs@(_:_), []) -> (init vs, [last vs]) (vs, ws) -> (vs, ws)- let prefix = T.unwords $ map stringify von- let family = T.unwords $ map stringify lastname- let suffix = T.unwords $ map stringify jr- let given = T.unwords $ map stringify first+ let prefix = T.unwords $ map stringifyInlines von+ let family = T.unwords $ map stringifyInlines lastname+ let suffix = T.unwords $ map stringifyInlines jr+ let given = T.unwords $ map stringifyInlines first return Name { nameFamily = if T.null family then Nothing
@@ -41,35 +41,16 @@ import Data.Text (Text, pack, unpack) import Data.Time (TimeZone, UTCTime) import Data.Unique (hashUnique)-#ifdef PANDOC_HTTP_SUPPORT-import Data.ByteString.Lazy (toChunks)-import System.Environment (getEnv)-import Data.Default (def)-import Network.Connection (TLSSettings(..))-import qualified Network.TLS as TLS-import qualified Network.TLS.Extra as TLS-import System.X509 (getSystemCertificateStore)-import Network.HTTP.Client- (httpLbs, Manager, responseBody, responseHeaders,- Request(port, host, requestHeaders), parseUrlThrow, newManager, HttpException)-import Network.HTTP.Client.Internal (addProxy)-import Network.HTTP.Client.TLS (mkManagerSettings)-import Network.HTTP.Types.Header ( hContentType )-import Network.Socket (withSocketsDo)-import Text.Pandoc.Class.CommonState (CommonState (..))-import Text.Pandoc.Class.PandocMonad ( getsCommonState, modifyCommonState )-import qualified Data.CaseInsensitive as CI-#endif-import Network.URI (URI(..), parseURI, unEscapeString)+import Network.URI (unEscapeString) import System.Directory (createDirectoryIfMissing) import System.FilePath ((</>), takeDirectory, takeFileName, normalise, takeExtension) import qualified System.FilePath.Posix as Posix import System.IO (stderr) import System.IO.Error import System.Random (StdGen)+import Text.Pandoc.Class.IO.HTTP (openURL) import Text.Pandoc.Class.PandocMonad- (PandocMonad,- getMediaBag, report, extractURIData)+ (PandocMonad, getMediaBag, report) import Text.Pandoc.Definition (Pandoc, Inline (Image)) import Text.Pandoc.Error (PandocError (..)) import Text.Pandoc.Logging (LogMessage (..), messageVerbosity, showLogMessage)@@ -129,70 +110,6 @@ newUniqueHash :: MonadIO m => m Int newUniqueHash = hashUnique <$> liftIO Data.Unique.newUnique -#ifdef PANDOC_HTTP_SUPPORT-getManager :: (PandocMonad m, MonadIO m) => m Manager-getManager = do- mbManager <- getsCommonState stManager- disableCertificateValidation <- getsCommonState stNoCheckCertificate- case mbManager of- Just manager -> pure manager- Nothing -> do- manager <- liftIO $ do- certificateStore <- getSystemCertificateStore- let tlsSettings = TLSSettings $- (TLS.defaultParamsClient "localhost.localdomain" "80")- { TLS.clientSupported = def{ TLS.supportedCiphers =- TLS.ciphersuite_default- , TLS.supportedExtendedMainSecret =- TLS.AllowEMS }- , TLS.clientShared = def- { TLS.sharedCAStore = certificateStore- , TLS.sharedValidationCache =- if disableCertificateValidation- then TLS.ValidationCache- (\_ _ _ -> return TLS.ValidationCachePass)- (\_ _ _ -> return ())- else def- }- }- let tlsManagerSettings = mkManagerSettings tlsSettings Nothing- newManager tlsManagerSettings- modifyCommonState $ \st -> st{ stManager = Just manager }- pure manager-#endif--openURL :: (PandocMonad m, MonadIO m) => Text -> m (B.ByteString, Maybe MimeType)-openURL u- | Just (URI{ uriScheme = "data:",- uriPath = upath }) <- parseURI (T.unpack u)- = pure $ extractURIData upath-#ifdef PANDOC_HTTP_SUPPORT- | otherwise = do- let toReqHeader (n, v) = (CI.mk (UTF8.fromText n), UTF8.fromText v)- customHeaders <- map toReqHeader <$> getsCommonState stRequestHeaders- report $ Fetching u- manager <- getManager- res <- liftIO $ E.try $ withSocketsDo $ do- proxy <- tryIOError (getEnv "http_proxy")- let addProxy' x = case proxy of- Left _ -> return x- Right pr -> parseUrlThrow pr >>= \r ->- return (addProxy (host r) (port r) x)- req <- parseUrlThrow (unpack u) >>= addProxy'- let req' = req{requestHeaders = customHeaders ++ requestHeaders req}- resp <- httpLbs req' manager- return (B.concat $ toChunks $ responseBody resp,- UTF8.toText `fmap` lookup hContentType (responseHeaders resp))-- case res of- Right r -> return r- Left (e :: HttpException)- -> throwError $ PandocHttpError u (T.pack (show e))-#else- | otherwise =- throwError $ PandocHttpError u "pandoc was compiled without HTTP support"-#endif- -- | Read the lazy ByteString contents from a file path, raising an error on -- failure. readFileLazy :: (PandocMonad m, MonadIO m) => FilePath -> m BL.ByteString@@ -232,19 +149,9 @@ -- | Output a log message. logOutput :: (PandocMonad m, MonadIO m) => LogMessage -> m () logOutput msg = liftIO $ do- UTF8.hPutStr stderr $- "[" <> T.pack (show (messageVerbosity msg)) <> "] "- alertIndent $ T.lines $ showLogMessage msg---- | Prints the list of lines to @stderr@, indenting every but the first--- line by two spaces.-alertIndent :: [Text] -> IO ()-alertIndent [] = return ()-alertIndent (l:ls) = do- UTF8.hPutStrLn stderr l- mapM_ go ls- where go l' = do UTF8.hPutStr stderr " "- UTF8.hPutStrLn stderr l'+ UTF8.hPutStrLn stderr $+ "[" <> T.pack (show (messageVerbosity msg)) <> "] " <>+ T.intercalate ("\n ") (T.lines (showLogMessage msg)) -- | Extract media from the mediabag into a directory (or a zip archive if the -- path supplied ends in @.zip@.
@@ -0,0 +1,115 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}+{- |+Module : Text.Pandoc.Class.IO.HTTP+Copyright : Copyright (C) 2025 John MacFarlane+License : GNU GPL, version 2 or above++Maintainer : John MacFarlane <jgm@berkeley.edu>+Stability : alpha+Portability : portable++HTTP fetching functionality for pandoc.+-}+module Text.Pandoc.Class.IO.HTTP+ ( openURL+ ) where++import Network.URI (URI(..), parseURI)+import Data.Text (Text)+import Control.Monad.IO.Class (MonadIO)+import Text.Pandoc.Class.PandocMonad (PandocMonad, extractURIData)+import Text.Pandoc.Error (PandocError (..))+import Text.Pandoc.MIME (MimeType)+import qualified Data.ByteString as B+import qualified Data.Text as T+import Control.Monad.Except (throwError)+#ifdef PANDOC_HTTP_SUPPORT+import Data.ByteString.Lazy (toChunks)+import Control.Monad.IO.Class (liftIO)+import System.Environment (getEnv)+import Data.Default (def)+import Network.Connection (TLSSettings(..))+import qualified Network.TLS as TLS+import qualified Network.TLS.Extra as TLS+import System.X509 (getSystemCertificateStore)+import Network.HTTP.Client+ (httpLbs, Manager, responseBody, responseHeaders,+ Request(port, host, requestHeaders), parseUrlThrow, newManager, HttpException)+import Network.HTTP.Client.Internal (addProxy)+import Network.HTTP.Client.TLS (mkManagerSettings)+import Network.HTTP.Types.Header ( hContentType )+import Network.Socket (withSocketsDo)+import Text.Pandoc.Class.CommonState (CommonState (..))+import Text.Pandoc.Class.PandocMonad ( getsCommonState, modifyCommonState, report )+import qualified Data.CaseInsensitive as CI+import System.IO.Error+import Text.Pandoc.Logging (LogMessage (..))+import qualified Control.Exception as E+import qualified Text.Pandoc.UTF8 as UTF8+#endif++#ifdef PANDOC_HTTP_SUPPORT+getManager :: (PandocMonad m, MonadIO m) => m Manager+getManager = do+ mbManager <- getsCommonState stManager+ disableCertificateValidation <- getsCommonState stNoCheckCertificate+ case mbManager of+ Just manager -> pure manager+ Nothing -> do+ manager <- liftIO $ do+ certificateStore <- getSystemCertificateStore+ let tlsSettings = TLSSettings $+ (TLS.defaultParamsClient "localhost.localdomain" "80")+ { TLS.clientSupported = def{ TLS.supportedCiphers =+ TLS.ciphersuite_default+ , TLS.supportedExtendedMainSecret =+ TLS.AllowEMS }+ , TLS.clientShared = def+ { TLS.sharedCAStore = certificateStore+ , TLS.sharedValidationCache =+ if disableCertificateValidation+ then TLS.ValidationCache+ (\_ _ _ -> return TLS.ValidationCachePass)+ (\_ _ _ -> return ())+ else def+ }+ }+ let tlsManagerSettings = mkManagerSettings tlsSettings Nothing+ newManager tlsManagerSettings+ modifyCommonState $ \st -> st{ stManager = Just manager }+ pure manager+#endif++openURL :: (PandocMonad m, MonadIO m) => Text -> m (B.ByteString, Maybe MimeType)+openURL u+ | Just (URI{ uriScheme = "data:",+ uriPath = upath }) <- parseURI (T.unpack u)+ = pure $ extractURIData upath+#ifdef PANDOC_HTTP_SUPPORT+ | otherwise = do+ let toReqHeader (n, v) = (CI.mk (UTF8.fromText n), UTF8.fromText v)+ customHeaders <- map toReqHeader <$> getsCommonState stRequestHeaders+ report $ Fetching u+ manager <- getManager+ res <- liftIO $ E.try $ withSocketsDo $ do+ proxy <- tryIOError (getEnv "http_proxy")+ let addProxy' x = case proxy of+ Left _ -> return x+ Right pr -> parseUrlThrow pr >>= \r ->+ return (addProxy (host r) (port r) x)+ req <- parseUrlThrow (T.unpack u) >>= addProxy'+ let req' = req{requestHeaders = customHeaders ++ requestHeaders req}+ resp <- httpLbs req' manager+ return (B.concat $ toChunks $ responseBody resp,+ UTF8.toText `fmap` lookup hContentType (responseHeaders resp))++ case res of+ Right r -> return r+ Left (e :: HttpException)+ -> throwError $ PandocHttpError u (T.pack (show e))+#else+ | otherwise =+ throwError $ PandocHttpError u "pandoc was compiled without HTTP support"+#endif
@@ -1,3 +1,4 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE FlexibleContexts #-}@@ -59,11 +60,16 @@ , makeCanonical , findFileWithDataFallback , checkUserDataDir+ , getDataDirs+ , getPandocDataDirs ) where import Control.Monad.Except (MonadError (catchError, throwError)) import Control.Monad.Trans (MonadTrans, lift) import Control.Monad (when)+import Data.Char (chr, digitToInt, isHexDigit, toLower)+import Data.List (intercalate)+import Data.Word (Word8) import Data.Time (UTCTime) import Data.Time.Clock.POSIX (POSIXTime, utcTimeToPOSIXSeconds, posixSecondsToUTCTime)@@ -71,8 +77,9 @@ import Network.URI ( escapeURIString, nonStrictRelativeTo, unEscapeString, parseURIReference, isAllowedInURI, parseURI, URI(..) )-import System.FilePath ((</>), takeExtension, dropExtension,- isRelative, makeRelative)+import System.FilePath ((</>), takeExtension, dropExtension, isAbsolute,+ isRelative, makeRelative, splitDirectories,+ splitSearchPath) import System.Random (StdGen) import Text.Collate.Lang (Lang(..), parseLang) import Text.Pandoc.Class.CommonState (CommonState (..))@@ -85,10 +92,10 @@ import Text.Pandoc.URI (uriPathToPath, pBase64DataURI) import qualified Data.Attoparsec.Text as A import Text.Pandoc.Walk (walkM)-import qualified Text.Pandoc.UTF8 as UTF8 import Data.ByteString.Base64 (decodeLenient) import Text.Parsec (ParsecT, getPosition, sourceLine, sourceName) import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as BL import qualified Data.Text as T import qualified Debug.Trace@@ -140,7 +147,6 @@ getCommonState :: m CommonState -- | Set the value of the 'CommonState' used by all instances -- of 'PandocMonad'.- -- | Get the value of a specific field of 'CommonState'. putCommonState :: CommonState -> m () -- | Get the value of a specific field of 'CommonState'. getsCommonState :: (CommonState -> a) -> m a@@ -201,13 +207,15 @@ -- get current settings origLog <- getsCommonState stLog origVerbosity <- getVerbosity+ let restore = modifyCommonState+ (\st -> st { stVerbosity = origVerbosity, stLog = origLog }) -- reset log level and set verbosity to the minimum modifyCommonState (\st -> st { stVerbosity = ERROR, stLog = []})- result <- action+ -- restore the original log and verbosity even if the action fails+ result <- action `catchError` (\e -> restore *> throwError e) -- get log messages reported while running `action` newLog <- getsCommonState stLog- modifyCommonState (\st -> st { stVerbosity = origVerbosity, stLog = origLog})-+ restore return (result, newLog) -- | Set request header to use in HTTP requests.@@ -221,7 +229,16 @@ -- | Determine whether certificate validation is disabled setNoCheckCertificate :: PandocMonad m => Bool -> m ()-setNoCheckCertificate noCheckCertificate = modifyCommonState $ \st -> st{stNoCheckCertificate = noCheckCertificate}+setNoCheckCertificate noCheckCertificate = modifyCommonState $ \st ->+ st{ stNoCheckCertificate = noCheckCertificate+#ifdef PANDOC_HTTP_SUPPORT+ -- discard any cached HTTP manager, since it was created with+ -- TLS settings based on the previous value of this option+ , stManager = if stNoCheckCertificate st == noCheckCertificate+ then stManager st+ else Nothing+#endif+ } -- | Initialize the media bag. setMediaBag :: PandocMonad m => MediaBag -> m ()@@ -281,7 +298,7 @@ setRequestHeaders :: PandocMonad m => [(T.Text, T.Text)] -> m () setRequestHeaders hs = modifyCommonState $ \st -> st{ stRequestHeaders = hs } --- | Get the absolute UL or directory of first source file.+-- | Get the absolute URL or directory of first source file. getSourceURL :: PandocMonad m => m (Maybe T.Text) getSourceURL = getsCommonState stSourceURL @@ -372,7 +389,7 @@ => T.Text -> m (B.ByteString, Maybe MimeType) downloadOrRead s- | "data:" `T.isPrefixOf` s,+ | T.toLower (T.take 5 s) == "data:", Right (bs, mt) <- A.parseOnly (pBase64DataURI <* A.endOfInput) s = pure (bs, Just mt) | otherwise = do@@ -389,9 +406,10 @@ Nothing -> openURL s' -- will throw error (Nothing, s') -> case parseURI (T.unpack s') of -- requires absolute URI- Just URI{ uriScheme = "file:", uriPath = upath}+ Just URI{ uriScheme = sch, uriPath = upath}+ | map toLower sch == "file:" -> readLocalFile $ uriPathToPath (T.pack upath)- Just URI{ uriScheme = "data:", uriPath = upath}+ | map toLower sch == "data:" -> pure $ extractURIData upath -- We don't want to treat C:/ as a scheme: Just u' | length (uriScheme u') > 2 -> openURL (T.pack $ show u')@@ -420,18 +438,40 @@ -- Extract data from a data URI's path component. extractURIData :: String -> (B.ByteString, Maybe MimeType) extractURIData upath =- case break (== ';') (filter (/= ' ') mimespec) of- (mime', ";base64") -> (decodeLenient contents, Just (T.pack mime'))- (mime', _) -> (contents, Just (T.pack mime'))+ if isBase64+ then (decodeLenient contents, Just mime)+ else (contents, Just mime) where- (mimespec, rest) = break (== ',') $ unEscapeString upath- contents = UTF8.fromString $ drop 1 rest+ (mimespec, rest) = break (== ',') upath+ -- The base64 indicator is the final parameter of the media type+ -- and may follow other parameters, e.g.+ -- data:text/plain;charset=utf-8;base64,...+ metaParts = splitParts (filter (/= ' ') (unEscapeString mimespec))+ splitParts s = case break (== ';') s of+ (x, []) -> [x]+ (x, _:s') -> x : splitParts s'+ isBase64 = length metaParts > 1 && last metaParts == "base64"+ mime = T.pack $ intercalate ";" $+ if isBase64 then init metaParts else metaParts+ -- Percent-escapes in a data URI represent raw octets (RFC 2397),+ -- so we decode them to bytes directly. (unEscapeString cannot be+ -- used here: it UTF-8-decodes consecutive escapes, which corrupts+ -- binary data when the result is re-encoded.)+ contents = unEscapeBytes $ drop 1 rest+ unEscapeBytes = B8.pack . go+ where+ go ('%':x:y:cs)+ | isHexDigit x, isHexDigit y+ = chr (digitToInt x * 16 + digitToInt y) : go cs+ go (c:cs) = c : go cs+ go [] = [] -- | Checks if the file path is relative to a parent directory. isRelativeToParentDir :: FilePath -> Bool isRelativeToParentDir fname =- let canonical = makeCanonical fname- in length canonical >= 2 && take 2 canonical == ".."+ case splitDirectories (makeCanonical fname) of+ "..":_ -> True+ _ -> False -- | Returns possible user data directory if the file path refers to a file or -- subdirectory within it.@@ -441,6 +481,27 @@ then getUserDataDir else return Nothing +-- | Returns the directories to search for a data file, in order of+-- priority: the user data directory (if any), followed by the+-- directories returned by 'getPandocDataDirs'. Returns an empty list+-- if the file path is absolute or relative to a parent directory.+getDataDirs :: PandocMonad m => FilePath -> m [FilePath]+getDataDirs fname =+ if isRelative fname && not (isRelativeToParentDir fname)+ then do+ mbUserDataDir <- getUserDataDir+ pandocDataDirs <- getPandocDataDirs+ return $ maybe id (:) mbUserDataDir pandocDataDirs+ else return []++-- | Returns the directories listed in the @PANDOC_DATA_DIRS@+-- environment variable, in order. Relative paths (including+-- empty entries) in @PANDOC_DATA_DIRS@ are ignored.+getPandocDataDirs :: PandocMonad m => m [FilePath]+getPandocDataDirs =+ maybe [] (filter isAbsolute . splitSearchPath . T.unpack)+ <$> lookupEnv "PANDOC_DATA_DIRS"+ -- | Read metadata file from the working directory or, if not found there, from -- the metadata subdirectory of the user data directory. readMetadataFile :: PandocMonad m => FilePath -> m B.ByteString@@ -468,9 +529,9 @@ toTextM fp bs = case TSE.decodeUtf8' . filterCRs . dropBOM $ bs of Left (TSE.DecodeError _ (Just w)) ->- case B.elemIndex w bs of- Just offset ->- throwError $ PandocUTF8DecodingError (T.pack fp) offset w+ case findDecodingError bs of+ Just (offset, w') ->+ throwError $ PandocUTF8DecodingError (T.pack fp) offset w' Nothing -> throwError $ PandocUTF8DecodingError (T.pack fp) 0 w Left e -> throwError $ PandocAppError (tshow e) Right t -> return t@@ -479,32 +540,70 @@ if "\xEF\xBB\xBF" `B.isPrefixOf` bs' then B.drop 3 bs' else bs'- filterCRs = B.filter (/=13)+ -- Only allocate a filtered copy if a CR is actually present;+ -- B.elem compiles to a fast memchr.+ filterCRs bs' = if 13 `B.elem` bs'+ then B.filter (/=13) bs'+ else bs' +-- Find the offset and value of the first byte at which UTF-8 decoding+-- fails (RFC 3629). Used to give an accurate position in decoding+-- error messages. (The BOM and CR bytes stripped before decoding are+-- themselves valid UTF-8, so scanning the unstripped input finds the+-- same error, at its offset in the original file.)+findDecodingError :: B.ByteString -> Maybe (Int, Word8)+findDecodingError = go 0+ where+ go !i bs = case B.uncons bs of+ Nothing -> Nothing+ Just (w, rest)+ | w < 0x80 -> go (i + 1) rest+ | w < 0xC2 -> Just (i, w) -- continuation byte or overlong lead+ | w == 0xE0 -> cont i w rest [(0xA0,0xBF),(0x80,0xBF)]+ | w == 0xED -> cont i w rest [(0x80,0x9F),(0x80,0xBF)] -- no surrogates+ | w < 0xE0 -> cont i w rest [(0x80,0xBF)]+ | w < 0xF0 -> cont i w rest [(0x80,0xBF),(0x80,0xBF)]+ | w == 0xF0 -> cont i w rest [(0x90,0xBF),(0x80,0xBF),(0x80,0xBF)]+ | w == 0xF4 -> cont i w rest [(0x80,0x8F),(0x80,0xBF),(0x80,0xBF)]+ | w < 0xF4 -> cont i w rest [(0x80,0xBF),(0x80,0xBF),(0x80,0xBF)]+ | otherwise -> Just (i, w) -- above U+10FFFF+ -- check that the bytes following the lead byte w at offset i fall+ -- into the given ranges; report the first byte that does not+ cont i w = go' (i + 1)+ where+ go' !j rest [] = go j rest+ go' !j rest ((lo,hi):ranges) =+ case B.uncons rest of+ Just (b, rest')+ | b >= lo && b <= hi -> go' (j + 1) rest' ranges+ | otherwise -> Just (j, b)+ -- input ends in the middle of a sequence: report the lead byte+ Nothing -> Just (i, w)+ -- | Returns @fp@ if the file exists in the current directory; otherwise--- searches for the data file relative to @/subdir/@. Returns @Nothing@--- if neither file exists.+-- searches for the data file relative to @/subdir/@ in the user data+-- directory and then in the system data directories (see 'getDataDirs').+-- Returns @Nothing@ if the file is not found. findFileWithDataFallback :: PandocMonad m => FilePath -- ^ subdir -> FilePath -- ^ fp -> m (Maybe FilePath) findFileWithDataFallback subdir fp = do -- First we check to see if the file is found. If not, and if it's not- -- an absolute path, we check to see whether it's in @userdir/@. If- -- not, we leave it unchanged.+ -- an absolute path, we check to see whether it's in @subdir@ of+ -- one of the data directories. If not, we return Nothing. existsInWorkingDir <- fileExists fp if existsInWorkingDir then return $ Just fp- else do- mbDataDir <- checkUserDataDir fp- case mbDataDir of- Nothing -> return Nothing- Just datadir -> do- let datafp = datadir </> subdir </> fp- existsInDataDir <- fileExists datafp- return $ if existsInDataDir- then Just datafp- else Nothing+ else getDataDirs fp >>= findFirst+ where+ findFirst [] = return Nothing+ findFirst (d:ds) = do+ let datafp = d </> subdir </> fp+ exists <- fileExists datafp+ if exists+ then return $ Just datafp+ else findFirst ds -- | Traverse tree, filling media bag for any images that -- aren't already in the media bag.
@@ -47,7 +47,8 @@ import Data.Time.Clock.POSIX ( posixSecondsToUTCTime ) import Data.Time.LocalTime (TimeZone, utc) import Data.Word (Word8)-import System.Directory (doesDirectoryExist, getDirectoryContents)+import System.Directory (canonicalizePath, doesDirectoryExist,+ getDirectoryContents) import System.FilePath ((</>)) import System.FilePath.Glob (match, compile) import System.Random (StdGen, mkStdGen)@@ -57,6 +58,7 @@ import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.Map as M+import qualified Data.Set as Set import qualified Data.Text as T import qualified System.Directory as Directory (getModificationTime) @@ -143,20 +145,27 @@ -- | Add the specified file to the FileTree. If file -- is a directory, add its contents recursively. addToFileTree :: FileTree -> FilePath -> IO FileTree-addToFileTree tree fp = do- isdir <- doesDirectoryExist fp- if isdir- then do -- recursively add contents of directories- let isSpecial ".." = True- isSpecial "." = True- isSpecial _ = False- fs <- map (fp </>) . filter (not . isSpecial) <$> getDirectoryContents fp- foldM addToFileTree tree fs- else do- contents <- B.readFile fp- mtime <- Directory.getModificationTime fp- return $ insertInFileTree fp FileInfo{ infoFileMTime = mtime- , infoFileContents = contents } tree+addToFileTree = go Set.empty+ where+ go ancestors tree fp = do+ isdir <- doesDirectoryExist fp+ if isdir+ then do -- recursively add contents of directories+ canonical <- canonicalizePath fp+ if canonical `Set.member` ancestors+ then return tree -- don't follow symlink cycles+ else do+ let isSpecial ".." = True+ isSpecial "." = True+ isSpecial _ = False+ fs <- map (fp </>) . filter (not . isSpecial) <$>+ getDirectoryContents fp+ foldM (go (Set.insert canonical ancestors)) tree fs+ else do+ contents <- B.readFile fp+ mtime <- Directory.getModificationTime fp+ return $ insertInFileTree fp FileInfo{ infoFileMTime = mtime+ , infoFileContents = contents } tree -- | Insert an ersatz file into the 'FileTree'. insertInFileTree :: FilePath -> FileInfo -> FileTree -> FileTree
@@ -19,12 +19,13 @@ , getDataFileNames , defaultUserDataDir ) where-import Text.Pandoc.Class (PandocMonad(..), checkUserDataDir, getTimestamp,- getUserDataDir, getPOSIXTime)+import Text.Pandoc.Class (PandocMonad(..), getDataDirs, getTimestamp,+ getPOSIXTime) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString as B import Codec.Archive.Zip+import Data.List (sort) import qualified Data.Text as T import Control.Monad.Except (throwError) import Text.Pandoc.Error (PandocError(..))@@ -38,15 +39,31 @@ import Paths_pandoc (getDataDir) #endif --- | Read file from the default data files.+-- | Read file from the default data files, ignoring the user data+-- directory and @PANDOC_DATA_DIRS@. readDefaultDataFile :: PandocMonad m => FilePath -> m B.ByteString-readDefaultDataFile "reference.docx" =- B.concat . BL.toChunks . fromArchive <$> getDefaultReferenceDocx-readDefaultDataFile "reference.pptx" =- B.concat . BL.toChunks . fromArchive <$> getDefaultReferencePptx-readDefaultDataFile "reference.odt" =- B.concat . BL.toChunks . fromArchive <$> getDefaultReferenceODT readDefaultDataFile fname =+ case buildReferenceDoc readDefaultDataFile fname of+ Just getArchive -> archiveToByteString <$> getArchive+ Nothing -> readDefaultDataFile' fname++-- | If the file is a reference document, returns an action that builds+-- it from its component files, which are read using the supplied function.+buildReferenceDoc :: PandocMonad m+ => (FilePath -> m B.ByteString) -> FilePath+ -> Maybe (m Archive)+buildReferenceDoc readComponent fname =+ case fname of+ "reference.docx" -> Just $ getDefaultReferenceDocx readComponent+ "reference.pptx" -> Just $ getDefaultReferencePptx readComponent+ "reference.odt" -> Just $ getDefaultReferenceODT readComponent+ _ -> Nothing++archiveToByteString :: Archive -> B.ByteString+archiveToByteString = B.concat . BL.toChunks . fromArchive++readDefaultDataFile' :: PandocMonad m => FilePath -> m B.ByteString+readDefaultDataFile' fname = #ifdef EMBED_DATA_FILES case lookup (makeCanonical fname) dataFiles of Nothing -> throwError $ PandocCouldNotFindDataFileError $ T.pack fname@@ -65,22 +82,28 @@ else throwError $ PandocCouldNotFindDataFileError $ T.pack fn #endif ---- | Read file from user data directory or,---- if not found there, from the default data files.+--- | Read file from user data directory or, if not found there,+--- from one of the directories listed in @PANDOC_DATA_DIRS@,+--- or, if not found there, from the default+--- data files. If a reference document (e.g. @reference.docx@)+--- is not found, it is built from component files (e.g.+--- @docx/word/styles.xml@), which are looked up in the same way. readDataFile :: PandocMonad m => FilePath -> m B.ByteString-readDataFile fname = do- datadir <- checkUserDataDir fname- case datadir of- Nothing -> readDefaultDataFile fname- Just userDir -> do- exists <- fileExists (userDir </> fname)- if exists- then readFileStrict (userDir </> fname)- else readDefaultDataFile fname+readDataFile fname = getDataDirs fname >>= go+ where+ go [] = case buildReferenceDoc readDataFile fname of+ Just getArchive -> archiveToByteString <$> getArchive+ Nothing -> readDefaultDataFile' fname+ go (d:ds) = do+ exists <- fileExists (d </> fname)+ if exists+ then readFileStrict (d </> fname)+ else go ds -- | Retrieve default reference.docx.-getDefaultReferenceDocx :: PandocMonad m => m Archive-getDefaultReferenceDocx = do+getDefaultReferenceDocx :: PandocMonad m+ => (FilePath -> m B.ByteString) -> m Archive+getDefaultReferenceDocx readComponent = do let paths = ["[Content_Types].xml", "_rels/.rels", "docProps/app.xml",@@ -100,24 +123,14 @@ let toLazy = BL.fromChunks . (:[]) let pathToEntry path = do epochtime <- floor . utcTimeToPOSIXSeconds <$> getTimestamp- contents <- toLazy <$> readDataFile ("docx/" ++ path)+ contents <- toLazy <$> readComponent ("docx/" ++ path) return $ toEntry path epochtime contents- datadir <- getUserDataDir- mbArchive <- case datadir of- Nothing -> return Nothing- Just d -> do- exists <- fileExists (d </> "reference.docx")- if exists- then return (Just (d </> "reference.docx"))- else return Nothing- case mbArchive of- Just arch -> toArchive <$> readFileLazy arch- Nothing -> foldr addEntryToArchive emptyArchive <$>- mapM pathToEntry paths+ foldr addEntryToArchive emptyArchive <$> mapM pathToEntry paths -- | Retrieve default reference.odt.-getDefaultReferenceODT :: PandocMonad m => m Archive-getDefaultReferenceODT = do+getDefaultReferenceODT :: PandocMonad m+ => (FilePath -> m B.ByteString) -> m Archive+getDefaultReferenceODT readComponent = do let paths = ["mimetype", "manifest.rdf", "styles.xml",@@ -126,24 +139,14 @@ "META-INF/manifest.xml"] let pathToEntry path = do epochtime <- floor `fmap` getPOSIXTime contents <- (BL.fromChunks . (:[])) `fmap`- readDataFile ("odt/" ++ path)+ readComponent ("odt/" ++ path) return $ toEntry path epochtime contents- datadir <- getUserDataDir- mbArchive <- case datadir of- Nothing -> return Nothing- Just d -> do- exists <- fileExists (d </> "reference.odt")- if exists- then return (Just (d </> "reference.odt"))- else return Nothing- case mbArchive of- Just arch -> toArchive <$> readFileLazy arch- Nothing -> foldr addEntryToArchive emptyArchive <$>- mapM pathToEntry paths+ foldr addEntryToArchive emptyArchive <$> mapM pathToEntry paths -- | Retrieve default reference.pptx.-getDefaultReferencePptx :: PandocMonad m => m Archive-getDefaultReferencePptx = do+getDefaultReferencePptx :: PandocMonad m+ => (FilePath -> m B.ByteString) -> m Archive+getDefaultReferencePptx readComponent = do -- We're going to narrow this down substantially once we get it -- working. let paths = [ "[Content_Types].xml"@@ -200,30 +203,29 @@ let toLazy = BL.fromChunks . (:[]) let pathToEntry path = do epochtime <- floor <$> getPOSIXTime- contents <- toLazy <$> readDataFile ("pptx/" ++ path)+ contents <- toLazy <$> readComponent ("pptx/" ++ path) return $ toEntry path epochtime contents- datadir <- getUserDataDir- mbArchive <- case datadir of- Nothing -> return Nothing- Just d -> do- exists <- fileExists (d </> "reference.pptx")- if exists- then return (Just (d </> "reference.pptx"))- else return Nothing- case mbArchive of- Just arch -> toArchive <$> readFileLazy arch- Nothing -> foldr addEntryToArchive emptyArchive <$>- mapM pathToEntry paths+ foldr addEntryToArchive emptyArchive <$> mapM pathToEntry paths getDataFileNames :: IO [FilePath] getDataFileNames = do #ifdef EMBED_DATA_FILES let allDataFiles = map fst dataFiles #else- allDataFiles <- filter (\x -> x /= "." && x /= "..") <$>- (getDataDir >>= getDirectoryContents)+ let listDirectoryRecursive d = do+ xs <- listDirectory d+ concat <$>+ mapM (\f -> do+ isdir <- doesDirectoryExist (d </> f)+ if isdir+ then map (f </>) <$> listDirectoryRecursive (d </> f)+ else pure [f]) xs+ ddir <- getDataDir+ allDataFiles <- ("MANUAL.txt" :) <$>+ listDirectoryRecursive (ddir </> "data") #endif- return $ "reference.docx" : "reference.odt" : "reference.pptx" : allDataFiles+ return $ sort $+ "reference.docx" : "reference.odt" : "reference.pptx" : allDataFiles -- | Return appropriate user data directory for platform. We use -- XDG_DATA_HOME (or its default value), but for backwards compatibility,
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-}@@ -542,7 +541,9 @@ , Ext_xrefs_name , Ext_xrefs_number ]- getAll "odt" = getAll "opendocument" <> autoIdExtensions+ getAll "odt" = getAll "opendocument" <> autoIdExtensions <>+ extensionsFromList [ Ext_styles ]+ getAll "fodt" = getAll "odt" getAll "muse" = autoIdExtensions <> extensionsFromList [ Ext_amuse ]
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {- |
@@ -187,6 +187,7 @@ ".dokuwiki" -> defFlavor "dokuwiki" ".epub" -> defFlavor "epub" ".fb2" -> defFlavor "fb2"+ ".fodt" -> defFlavor "fodt" ".htm" -> defFlavor "html" ".html" -> defFlavor "html" ".icml" -> defFlavor "icml"
@@ -1,4 +1,4 @@-{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, CPP #-}+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} {- | Module : Text.Pandoc.Image Copyright : Copyright (C) 2020-2024 John MacFarlane
@@ -38,8 +38,8 @@ import qualified Data.ByteString.Lazy as BL import Data.Binary.Get import Data.Bits ((.&.), shiftR, shiftL)-import Data.Word (bitReverse32, Word32)-import Data.Maybe (isJust, fromJust)+import Data.Word (Word32)+import Data.Maybe (fromMaybe) import Data.Char (isDigit) import Control.Monad import Text.Pandoc.Shared (safeRead)@@ -57,13 +57,13 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Codec.Picture.Metadata as Metadata import Codec.Picture (decodeImageWithMetadata)-import Codec.Compression.Zlib (decompress)+import qualified Codec.Compression.Zlib.Internal as Zlib -- import Debug.Trace -- quick and dirty functions to get image sizes data ImageType = Png | Gif | Jpeg | Svg | Pdf | Eps | Emf | Tiff | Webp | Avif- deriving Show+ deriving (Show, Eq) data Direction = Width | Height instance Show Direction where show Width = "width"@@ -118,7 +118,7 @@ "\x47\x49\x46\x38" -> return Gif "\x49\x49\x2a\x00" -> return Tiff "\x4D\x4D\x00\x2a" -> return Tiff- "\xff\xd8\xff\xbd" -> return Jpeg -- JPEG without application segment -- see p.32 in https://www.w3.org/Graphics/JPEG/itu-t81.pdf (and https://gist.github.com/leommoore/f9e57ba2aa4bf197ebc5?permalink_comment_id=3863054#gistcomment-3863054)+ "\xff\xd8\xff\xdb" -> return Jpeg -- JPEG without application segment -- see p.32 in https://www.w3.org/Graphics/JPEG/itu-t81.pdf (and https://gist.github.com/leommoore/f9e57ba2aa4bf197ebc5?permalink_comment_id=3863054#gistcomment-3863054) _ | B.take 3 img == "\xff\xd8\xff" && (let byte4 = B.take 1 (B.drop 3 img) in byte4 >= "\xe0" && byte4 <= "\xef") -- JPEG with application segment@@ -139,18 +139,39 @@ "RIFF" | B.take 4 (B.drop 8 img) == "WEBP" -> return Webp- _ | B.take 4 (B.drop 4 img) == "ftyp" -> return Avif+ _ | B.take 4 (B.drop 4 img) == "ftyp"+ -- require the AVIF brand, so that other+ -- ISO media (mp4, mov, heic...) is excluded:+ && (B.take 4 (B.drop 8 img) == "avif" ||+ B.take 4 (B.drop 8 img) == "avis")+ -> return Avif _ -> mzero +-- | Check for the presence of an @<svg@ (or @<SVG@) tag, in a+-- single pass over the file. findSvgTag :: ByteString -> Bool-findSvgTag img = "<svg" `B.isInfixOf` img || "<SVG" `B.isInfixOf` img+findSvgTag img = case B.elemIndex '<' img of+ Nothing -> False+ Just i ->+ case B.drop (i + 1) img of+ rest | "svg" `B.isPrefixOf` rest -> True+ | "SVG" `B.isPrefixOf` rest -> True+ | otherwise -> findSvgTag rest imageSize :: WriterOptions -> ByteString -> Either T.Text ImageSize imageSize opts img = checkDpi <$> case imageType img of- Just Png -> getSize img+ Just Png -> case pngSize img of+ Just sz -> Right sz+ -- fall back to the full decoder if the header+ -- scan fails:+ Nothing -> getSize img Just Gif -> getSize img- Just Jpeg -> getSize img+ Just Jpeg -> case jpegSize img of+ Just sz -> Right sz+ -- fall back to the full decoder if the header+ -- scan fails:+ Nothing -> getSize img Just Tiff -> getSize img Just Svg -> mbToEither "could not determine SVG size" $ svgSize opts img Just Eps -> mbToEither "could not determine EPS size" $ epsSize img@@ -162,10 +183,10 @@ where mbToEither msg Nothing = Left msg mbToEither _ (Just x) = Right x -- see #6880, some defective JPEGs may encode dpi 0, so default to 72- -- if that value is 0+ -- if that value is 0 or negative checkDpi size =- size{ dpiX = if dpiX size == 0 then 72 else dpiX size- , dpiY = if dpiY size == 0 then 72 else dpiY size }+ size{ dpiX = if dpiX size <= 0 then 72 else dpiX size+ , dpiY = if dpiY size <= 0 then 72 else dpiY size } sizeInPixels :: ImageSize -> (Integer, Integer)@@ -236,11 +257,12 @@ showInPixel _ (Percent _) = "" showInPixel opts dim = T.pack $ show $ inPixel opts dim --- | Maybe split a string into a leading number and trailing unit, e.g. "3cm" to Just (3.0, "cm")+-- | Maybe split a string into a leading number and trailing unit, e.g. "3cm" to Just (3.0, "cm").+-- Whitespace between the number and the unit is ignored. numUnit :: T.Text -> Maybe (Double, T.Text) numUnit s = let (nums, unit) = T.span (\c -> isDigit c || ('.'==c)) s- in (\n -> (n, unit)) <$> safeRead nums+ in (\n -> (n, T.stripStart unit)) <$> safeRead nums -- | Scale a dimension by a factor. scaleDimension :: Double -> Dimension -> Dimension@@ -287,12 +309,14 @@ case ls' of [] -> mzero (x:_) -> case B.words x of- [_, _, _, ux, uy] -> do- ux' <- safeRead $ TE.decodeUtf8 ux- uy' <- safeRead $ TE.decodeUtf8 uy+ [_, llx, lly, urx, ury] -> do+ llx' <- safeRead $ TE.decodeUtf8Lenient llx+ lly' <- safeRead $ TE.decodeUtf8Lenient lly+ urx' <- safeRead $ TE.decodeUtf8Lenient urx+ ury' <- safeRead $ TE.decodeUtf8Lenient ury return ImageSize{- pxX = ux'- , pxY = uy'+ pxX = urx' - llx'+ , pxY = ury' - lly' , dpiX = 72 , dpiY = 72 } _ -> mzero@@ -330,13 +354,10 @@ A.skipSpace A.string "/ObjStm" _ <- A.manyTill pLine (A.string "stream" *> pEol)- stream <- BL.pack <$> A.manyTill- (AW.satisfy (const True))- (pEol *> A.string "endstream" *> pEol)- let contents = BL.toStrict (decompress stream)- case A.parseOnly pPdfSize contents of- Left _ -> pPdfSize- Right is -> pure is)+ stream <- pTakeUntil "endstream"+ case A.parseOnly pPdfSize <$> safeDecompress stream of+ Just (Right is) -> pure is+ _ -> pPdfSize) <|> (A.char '/' *> pPdfSize) where@@ -346,6 +367,192 @@ pEol = A.satisfy iseol *> A.skipMany (A.satisfy iseol) pLine = A.takeWhile (not . iseol) <* pEol +-- | Consume input up to and including the first occurrence of the+-- (non-empty) terminator, returning what precedes it. Unlike+-- @manyTill anyWord8@, this consumes chunks at a time.+pTakeUntil :: ByteString -> A.Parser ByteString+pTakeUntil terminator = B.concat . reverse <$> go []+ where+ go acc = do+ chunk <- A.takeWhile (/= B.head terminator)+ let acc' = chunk : acc+ (A.string terminator *> pure acc')+ <|> (do c <- A.take 1+ go (c : acc'))++-- | Decompress a zlib stream, returning Nothing on malformed input.+-- ('Codec.Compression.Zlib.decompress' would instead throw an+-- exception from pure code when the corrupt part of its lazy result+-- is forced.)+safeDecompress :: ByteString -> Maybe ByteString+safeDecompress bs = fmap B.concat $+ Zlib.foldDecompressStreamWithInput+ (\chunk rest -> (chunk :) <$> rest)+ (const (Just []))+ (const Nothing)+ (Zlib.decompressST Zlib.zlibFormat Zlib.defaultDecompressParams)+ (BL.fromStrict bs)++-- | Extract PNG size from the IHDR and pHYs chunks, without+-- decoding any image data. (For paletted PNGs, JuicyPixels decodes+-- the whole image before returning metadata.)+pngSize :: ByteString -> Maybe ImageSize+pngSize img =+ case runGetOrFail pPngSize (BL.fromStrict img) of+ Left _ -> Nothing+ Right (_, _, sz) -> Just sz+ where+ pPngSize = do+ skip 8 -- signature+ -- the IHDR chunk always comes first:+ ihdrLen <- getWord32be+ ihdr <- getByteString 4+ when (ihdr /= "IHDR" || ihdrLen < 13) $ fail "IHDR chunk not found"+ w <- getWord32be+ h <- getWord32be+ skip (fromIntegral ihdrLen - 8 + 4) -- rest of chunk and CRC+ (dx, dy) <- findPhys+ return ImageSize{ pxX = toInteger w, pxY = toInteger h+ , dpiX = dx, dpiY = dy }+ -- scan the following chunks for pHYs:+ findPhys = do+ done <- isEmpty+ if done+ then return (72, 72)+ else do+ len <- getWord32be+ typ <- getByteString 4+ case typ of+ "pHYs" -> do+ ppuX <- getWord32be+ ppuY <- getWord32be+ unit <- getWord8+ return $ if unit == 1 -- pixels per meter+ then (dpmToDpi (toInteger ppuX),+ dpmToDpi (toInteger ppuY))+ else (72, 72)+ "IDAT" -> return (72, 72) -- pHYs must precede image data+ "IEND" -> return (72, 72)+ _ -> skip (fromIntegral len + 4) *> findPhys++-- | Convert dots per meter to dots per inch, using the same integer+-- arithmetic as JuicyPixels for consistency.+dpmToDpi :: Integer -> Integer+dpmToDpi z = z * 254 `div` 10000++-- | Extract JPEG size from the header, without decoding any image+-- data. Scans the marker segments preceding the entropy-coded data+-- for a start-of-frame marker (which gives the dimensions in pixels)+-- and JFIF APP0 and Exif APP1 segments (which give the resolution).+jpegSize :: ByteString -> Maybe ImageSize+jpegSize img =+ case runGetOrFail (skip 2 *> scanSegments Nothing Nothing)+ (BL.fromStrict img) of+ Left _ -> Nothing+ Right (_, _, sz) -> Just sz+ where+ scanSegments jfifDpi exifDpi = do+ ff <- getWord8+ when (ff /= 0xff) $ fail "malformed JPEG segment"+ marker <- skipFill+ scanSegment marker jfifDpi exifDpi++ -- extra 0xff bytes before a marker are padding:+ skipFill = do+ b <- getWord8+ if b == 0xff then skipFill else return b++ scanSegment marker jfifDpi exifDpi+ -- start of frame (baseline, progressive, etc.); C4, C8, and CC+ -- in this range are entropy-coding markers, not SOF:+ | marker >= 0xc0 && marker <= 0xcf+ && marker `notElem` [0xc4, 0xc8, 0xcc] = do+ skip 3 -- segment length and sample precision+ h <- getWord16be+ w <- getWord16be+ -- as in JuicyPixels, Exif resolution overrides JFIF:+ let (dx, dy) = fromMaybe (fromMaybe (72, 72) jfifDpi) exifDpi+ return ImageSize{ pxX = toInteger w, pxY = toInteger h+ , dpiX = dx, dpiY = dy }+ | marker == 0xd9 || marker == 0xda =+ fail "no SOF marker before image data" -- EOI or SOS+ | (marker >= 0xd0 && marker <= 0xd7) || marker == 0x01 =+ scanSegments jfifDpi exifDpi -- markers without a payload+ | otherwise = do+ len <- getWord16be+ when (len < 2) $ fail "invalid segment length"+ let n = fromIntegral len - 2+ case marker of+ 0xe0 -> do -- APP0 (JFIF)+ body <- getByteString n+ scanSegments (jfifDensity body <|> jfifDpi) exifDpi+ 0xe1 -> do -- APP1 (Exif)+ body <- getByteString n+ scanSegments jfifDpi (exifDensity body <|> exifDpi)+ _ -> skip n *> scanSegments jfifDpi exifDpi++-- | Pixel density from the body of a JFIF APP0 segment.+jfifDensity :: ByteString -> Maybe (Integer, Integer)+jfifDensity body = do+ guard $ "JFIF\0" `B.isPrefixOf` body+ -- after the identifier and 2-byte version: density units,+ -- horizontal density, vertical density+ (units, x, y) <- getAt 7 ((,,) <$> getWord8 <*> getWord16be <*> getWord16be)+ body+ case units of+ 1 -> Just (toInteger x, toInteger y) -- dots per inch+ 2 -> Just (dpcmToDpi (toInteger x), dpcmToDpi (toInteger y)) -- per cm+ _ -> Nothing++-- | Resolution from the TIFF structure in the body of an Exif APP1+-- segment.+exifDensity :: ByteString -> Maybe (Integer, Integer)+exifDensity body = do+ guard $ "Exif\0\0" `B.isPrefixOf` body+ let tiff = B.drop 6 body -- offsets are relative to the TIFF header+ (w16, w32) <- case B.take 2 tiff of+ "II" -> Just (getWord16le, getWord32le)+ "MM" -> Just (getWord16be, getWord32be)+ _ -> Nothing+ ifd <- fromIntegral <$> getAt 4 w32 tiff+ n <- fromIntegral <$> getAt ifd w16 tiff+ entries <- mapM (\i -> do let off = ifd + 2 + 12 * i+ tag <- getAt off w16 tiff+ return (tag, off))+ [0 .. n - 1 :: Int]+ -- resolution unit (SHORT, stored inline in the value field):+ -- 2 = inches, 3 = centimeters+ unit <- lookup 0x0128 entries >>= \off -> getAt (off + 8) w16 tiff+ toDpi <- case unit of+ 2 -> Just id+ 3 -> Just dpcmToDpi+ _ -> Nothing+ let resolution tag = do+ off <- lookup tag entries+ -- the value field holds the offset of the RATIONAL value:+ valOff <- fromIntegral <$> getAt (off + 8) w32 tiff+ num <- getAt valOff w32 tiff+ den <- getAt (valOff + 4) w32 tiff+ guard $ den /= 0+ return $ toDpi (toInteger num `div` toInteger den)+ x <- resolution 0x011a -- XResolution+ y <- resolution 0x011b -- YResolution+ return (x, y)++-- | Convert dots per centimeter to dots per inch, using the same+-- integer arithmetic as JuicyPixels for consistency.+dpcmToDpi :: Integer -> Integer+dpcmToDpi z = z * 254 `div` 100++-- | Run a 'Get' parser at the given offset in a strict ByteString,+-- returning Nothing if it fails (e.g. by running out of input).+getAt :: Int -> Get a -> ByteString -> Maybe a+getAt off g bs+ | off < 0 = Nothing+ | otherwise = case runGetOrFail (skip off *> g) (BL.fromStrict bs) of+ Left _ -> Nothing+ Right (_, _, x) -> Just x+ getSize :: ByteString -> Either T.Text ImageSize getSize img = case decodeImageWithMetadata img of@@ -369,8 +576,11 @@ $ TL.fromStrict $ UTF8.toText $ dropBOM img let viewboxSize = do vb <- findAttrBy (== QName "viewBox" Nothing Nothing) doc- [_,_,w,h] <- mapM safeRead (T.words vb)- return (w,h)+ -- per the SVG spec, the numbers are separated by whitespace+ -- and/or a comma, and may be fractional:+ [_,_,w,h] <- mapM safeRead $ T.words $+ T.map (\c -> if c == ',' then ' ' else c) vb+ return (floor (w :: Double), floor (h :: Double)) let dpi = fromIntegral $ writerDpi opts let dirToInt dir = do dim <- findAttrBy (== QName dir Nothing Nothing) doc >>= lengthToDim@@ -391,26 +601,34 @@ let parseheader = runGetOrFail $ do skip 0x18 -- 0x00- frameL <- getWord32le -- 0x18 measured in 1/100 of a millimetre- frameT <- getWord32le -- 0x1C- frameR <- getWord32le -- 0x20- frameB <- getWord32le -- 0x24+ -- the frame bounds are signed, measured in 1/100 of a millimetre:+ frameL <- getInt32le -- 0x18+ frameT <- getInt32le -- 0x1C+ frameR <- getInt32le -- 0x20+ frameB <- getInt32le -- 0x24 skip 0x20 -- 0x28 deviceX <- getWord32le -- 0x48 pixels of reference device deviceY <- getWord32le -- 0x4C mmX <- getWord32le -- 0x50 real mm of reference device (always 320*240?)- mmY <- getWord32le -- 0x58+ mmY <- getWord32le -- 0x54 -- end of header+ -- guard against division by zero below; since the ImageSize+ -- fields are lazy, the exception would escape runGetOrFail+ when (mmX == 0 || mmY == 0) $+ fail "EMF header has zero-size reference device"+ -- compute with Integers to avoid Word32 overflow: let- w = (deviceX * (frameR - frameL)) `quot` (mmX * 100)- h = (deviceY * (frameB - frameT)) `quot` (mmY * 100)- dpiW = (deviceX * 254) `quot` (mmX * 10)- dpiH = (deviceY * 254) `quot` (mmY * 10)+ w = (toInteger deviceX * (toInteger frameR - toInteger frameL))+ `quot` (toInteger mmX * 100)+ h = (toInteger deviceY * (toInteger frameB - toInteger frameT))+ `quot` (toInteger mmY * 100)+ dpiW = (toInteger deviceX * 254) `quot` (toInteger mmX * 10)+ dpiH = (toInteger deviceY * 254) `quot` (toInteger mmY * 10) return $ ImageSize- { pxX = fromIntegral w- , pxY = fromIntegral h- , dpiX = fromIntegral dpiW- , dpiY = fromIntegral dpiH+ { pxX = w+ , pxY = h+ , dpiX = dpiW+ , dpiY = dpiH } in case parseheader . BL.fromStrict $ img of@@ -446,24 +664,23 @@ AW.word8 0x2a width16 <- AW.take 2 height16 <- AW.take 2- let w = toInteger <$> decode lossySize width16- h = toInteger <$> decode lossySize height16- guard $ isJust w && isJust h- return (fromJust w, fromJust h)- losslessSizes = runGetOrFail $ do- bitReverse32 <$> getWord32le+ case (decode lossySize width16, decode lossySize height16) of+ (Just w, Just h) -> return (toInteger w, toInteger h)+ _ -> empty+ -- The VP8L bitstream is read starting from the least significant+ -- bit of each byte, so after reading the 4 bytes as a little-endian+ -- word, width - 1 is in bits 0-13 and height - 1 in bits 14-27.+ losslessSizes = runGetOrFail getWord32le losslessSize word = 1 + (word .&. 0x3FFF) lossless = do AW.string "VP8L" AW.take 4 -- length in bytes of VP8 Lossless chunk size AW.word8 0x2f -- webp lossless stream magic sizes <- AW.take 4- let mbword = decode losslessSizes sizes- guard $ isJust mbword- let word = fromJust mbword- let w = toInteger $ losslessSize word- h = toInteger $ losslessSize (word `shiftR` 14)- return (w, h)+ case decode losslessSizes sizes of+ Just word -> return ( toInteger $ losslessSize word+ , toInteger $ losslessSize (word `shiftR` 14) )+ Nothing -> empty extendedSize = runGetOrFail $ do low <- toInteger <$> getWord16le high <- toInteger <$> getWord8@@ -473,10 +690,9 @@ AW.take 8 -- VP8X chunk length, flags and reserved area width24 <- AW.take 3 height24 <- AW.take 3- let w = decode extendedSize width24- h = decode extendedSize height24- guard $ isJust w && isJust h- return (fromJust w, fromJust h)+ case (decode extendedSize width24, decode extendedSize height24) of+ (Just w, Just h) -> return (w, h)+ _ -> empty webpSize :: WriterOptions -> ByteString -> Maybe ImageSize webpSize opts img =@@ -485,32 +701,49 @@ Right sz -> Just sz { dpiX = fromIntegral $ writerDpi opts, dpiY = fromIntegral $ writerDpi opts} avifSize :: WriterOptions -> ByteString -> Maybe ImageSize-avifSize _opts img =+avifSize opts img = case runGetOrFail (verifyFtyp >> findAvifDimensions) (BL.fromStrict img) of Left (_, _, _err) -> Nothing Right (_, _, (width, height)) -> Just $ ImageSize { pxX = fromIntegral width , pxY = fromIntegral height- , dpiX = 72- , dpiY = 72 }+ , dpiX = fromIntegral $ writerDpi opts+ , dpiY = fromIntegral $ writerDpi opts } ---- AVIF parsing: +-- | Read an ISO BMFF box header, returning the box type, the size of+-- the box contents, and the size of the header itself. Handles the+-- 64-bit "largesize" field (size == 1) and boxes that extend to the+-- end of the file (size == 0).+getBoxHeader :: Get (B.ByteString, Int, Int)+getBoxHeader = do+ boxSize <- getWord32be+ boxType <- getByteString 4+ case boxSize of+ 0 -> do -- the box extends to the end of the file+ rest <- lookAhead getRemainingLazyByteString+ return (boxType, fromIntegral (BL.length rest), 8)+ 1 -> do -- a 64-bit size follows the box type+ largeSize <- getWord64be+ when (largeSize < 16 || largeSize > 0x7fffffff) $+ fail "invalid box largesize"+ return (boxType, fromIntegral largeSize - 16, 16)+ _ -> do+ when (boxSize < 8) $ fail "invalid box size"+ return (boxType, fromIntegral boxSize - 8, 8)+ verifyFtyp :: Get () verifyFtyp = do- ftypSize <- getWord32be- when (ftypSize < 16) $ fail "Invalid ftyp size"-- ftyp <- getByteString 4- unless (ftyp == "ftyp") $ fail "ftyp signature not found"+ (boxType, contentSize, _) <- getBoxHeader+ unless (boxType == "ftyp") $ fail "ftyp signature not found"+ when (contentSize < 8) $ fail "Invalid ftyp size" brand <- getByteString 4 unless (brand == "avif" || brand == "avis") $ fail "Not an AVIF file" -- Skip minor version and compatible brands- -- (we've read 12 bytes: size+type+brand)- let remaining_ftyp = fromIntegral ftypSize - 12- when (remaining_ftyp > 0) $ skip remaining_ftyp+ skip (contentSize - 4) findAvifDimensions :: Get (Word32, Word32) findAvifDimensions = searchAvifBoxes []@@ -521,10 +754,7 @@ if isempty then fail $ "No dimensions found. Searched: " ++ show (reverse path) else do- boxSize <- getWord32be- boxType <- getByteString 4-- let contentSize = fromIntegral boxSize - 8+ (boxType, contentSize, _) <- getBoxHeader let newPath = boxType : path -- If it's a container box, search inside it@@ -535,10 +765,8 @@ result <- tryParseDimensions boxType contentSize case result of Just dims -> return dims- Nothing -> do- -- Skip this box and continue- when (contentSize > 0 && contentSize < 10000000) $- skip contentSize+ Nothing ->+ -- Don't skip here - tryParseDimensions already handled it searchAvifBoxes path tryParseDimensions :: B.ByteString -> Int -> Get (Maybe (Word32, Word32))@@ -575,8 +803,12 @@ version <- getWord8 skip 3 -- flags - -- Skip to width/height based on version- let skipBytes = if version == 1 then 76 else 64+ -- Skip to width/height based on version:+ -- creation/modification times, track ID, reserved, duration+ -- (20 bytes with 32-bit times, 32 bytes with 64-bit times),+ -- then 8 reserved, layer, alternate_group, volume, reserved+ -- (2 bytes each), and the 36-byte transformation matrix+ let skipBytes = if version == 1 then 84 else 72 skip skipBytes width <- getWord32be@@ -631,13 +863,11 @@ searchAvifBoxesInRange remaining' path | remaining' < 8 = searchAvifBoxes path | otherwise = do- boxSize <- getWord32be- boxType <- getByteString 4+ (boxType, contentSize, headerSize) <- getBoxHeader - let contentSize = fromIntegral boxSize - 8 let newPath = boxType : path - when (contentSize < 0 || fromIntegral boxSize > remaining') $ do+ when (contentSize + headerSize > remaining') $ fail $ "Malformed box at path: " ++ show (reverse newPath) if isContainerBox boxType@@ -648,7 +878,8 @@ Just dims -> return dims Nothing -> do -- Don't skip here - tryParseDimensions already handled it- searchAvifBoxesInRange (remaining' - fromIntegral boxSize) path+ searchAvifBoxesInRange+ (remaining' - contentSize - headerSize) path isContainerBox :: B.ByteString -> Bool isContainerBox boxType = boxType `elem`
@@ -154,6 +154,7 @@ ,("fbdoc","application/x-maker") ,("fch","chemical/x-gaussian-checkpoint") ,("fchk","chemical/x-gaussian-checkpoint")+ ,("fodt","application/vnd.oasis.opendocument.text-flat-xml") ,("frm","application/x-maker") ,("fs","text/plain") ,("gal","chemical/x-gaussian-log")
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}@@ -23,26 +22,29 @@ mediaDirectory, mediaItems ) where-import Crypto.Hash (hashWith, SHA1(SHA1))+import Crypto.Hash (hashlazy, Digest, SHA1) import qualified Data.ByteString.Lazy as BL+import Data.Char (toLower) import Data.Data (Data)-import qualified Data.Map as M+import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isNothing) import Data.Typeable (Typeable) import System.FilePath import qualified System.FilePath.Posix as Posix import qualified System.FilePath.Windows as Windows import Text.Pandoc.MIME (MimeType, getMimeTypeDef, extensionFromMimeType)+import Text.Pandoc.Shared (makeCanonical) import Data.Text (Text) import qualified Data.Text as T-import Network.URI (URI (..), isURI, parseURI, unEscapeString)-import Data.List (isInfixOf)+import Network.URI (URI (..), parseURI, unEscapeString)+import Text.Pandoc.URI (isURI) data MediaItem = MediaItem- { mediaMimeType :: MimeType- , mediaPath :: FilePath+ { mediaMimeType :: !MimeType+ , mediaPath :: !FilePath , mediaContents :: BL.ByteString+ -- ^ Left lazy so that contents need not be forced at insert time. } deriving (Eq, Ord, Show, Data, Typeable) -- | A container for a collection of binary resources, with names and@@ -55,14 +57,22 @@ instance Show MediaBag where show bag = "MediaBag " ++ show (mediaDirectory bag) --- | We represent paths with /, in normalized form. Percent-encoding--- is not resolved.+-- | Check for the (case-insensitive) @data:@ URI scheme.+isDataURI :: FilePath -> Bool+isDataURI = (== "data:") . map toLower . take 5++-- | We represent paths with /, in canonical form (redundant @.@ and+-- @..@ components removed). Percent-encoding is not resolved. canonicalize :: FilePath -> Text--- avoid an expensive call to isURI for data URIs:-canonicalize fp@('d':'a':'t':'a':':':_) = T.pack fp canonicalize fp- | isURI fp = T.pack fp- | otherwise = T.replace "\\" "/" . T.pack . normalise $ fp+ -- avoid an expensive call to isURI for data URIs:+ | isDataURI fp = fp'+ | isURI fp' = fp'+ | otherwise = T.pack . makeCanonical . map slashify $ fp+ where+ fp' = T.pack fp+ slashify '\\' = '/'+ slashify c = c -- | Delete a media item from a 'MediaBag', or do nothing if no item corresponds -- to the given path.@@ -80,7 +90,7 @@ -> MediaBag -> MediaBag insertMedia fp mbMime contents (MediaBag mediamap)- | 'd':'a':'t':'a':':':_ <- fp+ | isDataURI fp , Just mt' <- mbMime = MediaBag (M.insert fp' MediaItem{ mediaPath = hashpath@@ -94,14 +104,24 @@ fp' = canonicalize fp fp'' = unEscapeString $ T.unpack fp' uri = parseURI fp- hashpath = show (hashWith SHA1 (BL.toStrict contents)) <> ext+ hashpath = show (hashlazy contents :: Digest SHA1) <> ext+ -- We only keep the original name if the key contains no+ -- percent-encoding, i.e., unescaping is the identity; otherwise+ -- distinct keys (e.g. "a%20b.png" and "a b.png") could unescape+ -- to the same mediaPath and clobber each other on extraction. newpath = if Posix.isRelative fp'' && Windows.isRelative fp'' && isNothing uri- && not (".." `isInfixOf` fp'')- && '%' `notElem` fp''+ && not containsParentRef+ && not (T.any (== '%') fp') then fp'' else hashpath+ -- Check for a ".." path component (treating both / and \ as+ -- separators, since the unescaped path may contain backslashes+ -- from percent-encoding); a mere ".." substring (as in+ -- "foo..bar.png") is harmless.+ containsParentRef = ".." `elem`+ Posix.splitDirectories (map (\c -> if c == '\\' then '/' else c) fp'') fallback = case takeExtension fp'' of ".gz" -> getMimeTypeDef $ dropExtension fp'' _ -> getMimeTypeDef fp''
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE LambdaCase #-}
@@ -49,7 +49,7 @@ import Text.Pandoc.Extensions (disableExtension, Extension(Ext_smart)) import Text.Pandoc.Process (pipeProcess) import System.Process (readProcessWithExitCode)-import Text.Pandoc.Shared (inDirectory, stringify, tshow)+import Text.Pandoc.Shared (inDirectory, stringify, stringifyInlines, tshow) import qualified Text.Pandoc.UTF8 as UTF8 import Text.Pandoc.Walk (walkM) import Text.Pandoc.Writers.Shared (getField, metaToContext)@@ -112,7 +112,7 @@ >>= writer opts' verbosity <- getVerbosity- let compileHTML mkOutArgs = do+ let compileHTML defaultOpts mkOutArgs = do -- check to see if there is anything in mediabag, and if so, -- make the HTML self contained mediabag <- getMediaBag@@ -120,12 +120,12 @@ [] -> pure source _ -> makeSelfContained source liftIO $- toPdfViaTempFile verbosity program pdfargs mkOutArgs ".html" source'+ toPdfViaTempFile verbosity program (defaultOpts ++ pdfargs) mkOutArgs ".html" source' case takeBaseName program of "wkhtmltopdf" -> makeWithWkhtmltopdf program pdfargs writer opts' doc- "pagedjs-cli" -> compileHTML (\f -> ["-o", f])- "prince" -> compileHTML (\f -> ["-o", f])- "weasyprint" -> compileHTML (:[])+ "pagedjs-cli" -> compileHTML [] (\f -> ["-o", f])+ "prince" -> compileHTML [] (\f -> ["-o", f])+ "weasyprint" -> compileHTML ["--presentational-hints"] (\f -> [f]) "typst" -> liftIO $ toPdfViaTempFile verbosity program ("compile":pdfargs) (:[]) ".typ" source "pdfroff" -> do@@ -196,7 +196,7 @@ _ -> [] meta' <- metaToContext opts (return . literal . stringify)- (return . literal . stringify)+ (return . literal . stringifyInlines) meta let toArgs (f, mbd) = maybe [] (\d -> ["--" <> f, T.unpack d]) mbd let args = mathArgs ++ concatMap toArgs@@ -456,7 +456,8 @@ let rerunWarnings = checkForRerun logContents tocFileExists <- fileExists (replaceExtension outfile ".toc") -- if we have a TOC we always need 3 runs, see #10308- let rerunWarnings' = rerunWarnings ++ ["TOC is present" | tocFileExists]+ let rerunWarnings' = rerunWarnings +++ ["TOC is present" | tocFileExists, runNumber < 3] if not (null rerunWarnings') && runNumber < maxruns then do report $ MakePDFInfo "Rerun needed"
@@ -188,7 +188,6 @@ ParsecT, SourcePos, SourceName,- setSourceName, Column, Line, incSourceLine,
@@ -1,4 +1,5 @@ {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE BangPatterns #-} {- | Module : Text.Pandoc.Parsing Copyright : © 2006-2024 John MacFarlane@@ -121,7 +122,9 @@ -- | Update the position on which the last string ended. updateLastStrPos :: (Stream s m a, HasLastStrPosition st) => ParsecT s st m ()-updateLastStrPos = getPosition >>= updateState . setLastStrPos . Just+updateLastStrPos = do+ !pos <- getPosition+ updateState $ setLastStrPos $ Just pos -- | Whether we are right after the end of a string. notAfterString :: (Stream s m a, HasLastStrPosition st) => ParsecT s st m Bool
@@ -17,10 +17,8 @@ ) where -import Prelude hiding (Applicative(..))-import Control.Applicative (Applicative(..)) import Control.Monad.Reader- ( asks, runReader, MonadReader(ask), Reader, ReaderT(ReaderT) )+ ( asks, runReader, MonadReader(ask), Reader ) -- | Reader monad wrapping the parser state. This is used to possibly -- delay evaluation until all relevant information has been parsed and@@ -31,9 +29,8 @@ instance Semigroup a => Semigroup (Future s a) where (<>) = liftA2 (<>) -instance (Semigroup a, Monoid a) => Monoid (Future s a) where+instance Monoid a => Monoid (Future s a) where mempty = return mempty- mappend = (<>) -- | Run a delayed action with the given state. runF :: Future s a -> s -> a
@@ -64,7 +64,6 @@ import Control.Monad ( join- , liftM , unless , void , when@@ -129,8 +128,6 @@ , setInput , setPosition , skipMany- , sourceColumn- , sourceName , tokenPrim , try , unexpected@@ -142,6 +139,7 @@ import Text.Pandoc.Parsing.Capabilities import Text.Pandoc.Parsing.State import Text.Pandoc.Parsing.Future (Future (..))+import qualified Data.Map as M import qualified Data.Set as Set import qualified Data.Text as T import qualified Text.Pandoc.Builder as B@@ -151,7 +149,7 @@ -- | Remove whitespace from start and end; just like @'trimInlines'@, -- but lifted into the 'Future' type. trimInlinesF :: Future s Inlines -> Future s Inlines-trimInlinesF = liftM trimInlines+trimInlinesF = fmap trimInlines -- | Like @count@, but packs its result countChar :: (Stream s m Char, UpdateSourcePos s Char, Monad m)@@ -311,13 +309,15 @@ => [Text] -> ParsecT s st m Text oneOfStringsCI = oneOfStrings' ciMatch where ciMatch x y = toLower' x == toLower' y- -- this optimizes toLower by checking common ASCII case- -- first, before calling the expensive unicode-aware- -- function:- toLower' c | isAsciiUpper c = chr (ord c + 32)- | isAscii c = c- | otherwise = toLower c +-- | Optimized 'toLower': checks the common ASCII case+-- first, before calling the expensive unicode-aware+-- function.+toLower' :: Char -> Char+toLower' c | isAsciiUpper c = chr (ord c + 32)+ | isAscii c = c+ | otherwise = toLower c+ -- | Parses a space or tab. spaceChar :: (Stream s m Char, UpdateSourcePos s Char) => ParsecT s st m Char@@ -356,7 +356,7 @@ => Int -> ParsecT Sources st m () gobbleSpaces 0 = return () gobbleSpaces n- | n < 0 = error "gobbleSpaces called with negative number"+ | n < 0 = Prelude.fail "gobbleSpaces called with negative number" | otherwise = try $ do char ' ' <|> eatOneSpaceOfTab gobbleSpaces (n - 1)@@ -369,12 +369,11 @@ -- replace the tab on the input stream with spaces let numSpaces = tabstop - ((sourceColumn pos - 1) `mod` tabstop) inp <- getInput- setInput $- case inp of- Sources [] -> error "eatOneSpaceOfTab - empty Sources list"- Sources ((fp,t):rest) ->- -- drop the tab and add spaces- Sources ((fp, T.replicate numSpaces " " <> T.drop 1 t):rest)+ case inp of+ Sources [] -> Prelude.fail "eatOneSpaceOfTab - empty Sources list"+ Sources ((fp,t):rest) -> setInput $+ -- drop the tab and add spaces+ Sources ((fp, T.replicate numSpaces " " <> T.drop 1 t):rest) char ' ' -- | Gobble up to n spaces; if tabs are encountered, expand them@@ -383,7 +382,7 @@ => Int -> ParsecT Sources st m Int gobbleAtMostSpaces 0 = return 0 gobbleAtMostSpaces n- | n < 0 = error "gobbleAtMostSpaces called with negative number"+ | n < 0 = Prelude.fail "gobbleAtMostSpaces called with negative number" | otherwise = option 0 $ do char ' ' <|> eatOneSpaceOfTab (+ 1) <$> gobbleAtMostSpaces (n - 1)@@ -487,8 +486,36 @@ emailPunctChars :: Set.Set Char emailPunctChars = Set.fromList "!\"#$%&'*+-/=?^_{|}~;" +-- | Trie over 'Char', used for efficient matching against the+-- (large, static) set of known URI schemes.+data CharTrie = CharTrie !Bool !(M.Map Char CharTrie)++trieInsert :: Text -> CharTrie -> CharTrie+trieInsert t (CharTrie terminal m) =+ case T.uncons t of+ Nothing -> CharTrie True m+ Just (c, rest) -> CharTrie terminal $+ M.alter (Just . trieInsert rest . fromMaybe (CharTrie False mempty)) c m++-- | Trie of known URI schemes; keys are case-folded with 'toLower''.+schemeTrie :: CharTrie+schemeTrie = foldr trieInsert (CharTrie False mempty)+ (map (T.map toLower') (Set.toList schemes))++-- | Parses a known URI scheme, case-insensitively, preferring the+-- longest matching scheme. Returns the scheme as written in the input. uriScheme :: (Stream s m Char, UpdateSourcePos s Char) => ParsecT s st m Text-uriScheme = oneOfStringsCI (Set.toList schemes)+uriScheme = TL.toStrict . TB.toLazyText <$> try (go mempty schemeTrie)+ where+ go acc (CharTrie _ m) = do+ c <- anyChar+ case M.lookup (toLower' c) m of+ Nothing -> Prelude.fail "not a URI scheme"+ Just subtrie@(CharTrie terminal _) ->+ let !acc' = acc <> TB.singleton c+ in if terminal+ then option acc' (try (go acc' subtrie))+ else go acc' subtrie -- | Parses a URI. Returns pair of original and URI-escaped version. uri :: (Stream s m Char, UpdateSourcePos s Char) => ParsecT s st m (Text, Text)@@ -661,8 +688,7 @@ let id'' = if Ext_ascii_identifiers `extensionEnabled` exts then toAsciiText id' else id'- updateState $ updateIdentifierList $ Set.insert id'- updateState $ updateIdentifierList $ Set.insert id''+ updateState $ updateIdentifierList (Set.insert id' . Set.insert id'') return (id'',classes,kvs) else do unless (T.null ident) $ do
@@ -38,8 +38,8 @@ import Text.Pandoc.Parsing.Capabilities import Text.Pandoc.Parsing.General import Text.Pandoc.Sources-import Text.Parsec (Stream (..), ParsecT, optional, sepEndBy1, try)-+import Text.Parsec (Stream (..), ParsecT, getPosition, optional, sepEndBy1,+ setPosition, try) import Data.Maybe (mapMaybe) import qualified Data.Text as T import qualified Text.GridTable as GT@@ -135,9 +135,10 @@ -> ParsecT Sources st m (mf Blocks) -- ^ Block list parser -> ParsecT Sources st m (mf TableComponents) gridTableWith' normalization blocks = do+ pos <- getPosition tbl <- GT.gridTable <* optional blanklines let blkTbl = GT.mapCells- (\lns -> parseFromString' blocks+ (\lns -> parseFromString' (setPosition pos >> blocks) . flip T.snoc '\n' -- ensure proper block parsing . T.unlines . removeOneLeadingSpace@@ -145,7 +146,7 @@ tbl let rows = GT.rows blkTbl let toPandocCell (GT.Cell c (GT.RowSpan rs) (GT.ColSpan cs)) =- fmap (B.cell AlignDefault (B.RowSpan rs) (B.ColSpan cs) . plainify) <$> c+ fmap (B.cell AlignDefault (B.RowSpan rs) (B.ColSpan cs)) <$> c rows' <- mapM (mapM toPandocCell) rows columns <- getOption readerColumns let colspecs = zipWith (\cs w -> (convAlign $ fst cs, B.ColWidth w))@@ -186,11 +187,6 @@ where startsWithSpace t = case T.uncons t of Nothing -> True Just (c, _) -> c == ' '--plainify :: B.Blocks -> B.Blocks-plainify blks = case B.toList blks of- [Para x] -> B.fromList [Plain x]- _ -> blks convAlign :: GT.Alignment -> B.Alignment convAlign GT.AlignLeft = B.AlignLeft
@@ -24,9 +24,11 @@ import Data.Char ( isAsciiUpper , isAsciiLower+ , isDigit , ord , toLower )+import Control.Monad (mzero) import Data.Maybe (fromMaybe) import Text.Pandoc.Definition ( ListNumberDelim(..)@@ -160,10 +162,29 @@ -- | Parses an ordered list marker and returns list attributes. anyOrderedListMarker :: (Stream s m Char, UpdateSourcePos s Char) => ParsecT s ParserState m ListAttributes-anyOrderedListMarker = choice- [delimParser numParser | delimParser <- [inPeriod, inOneParen, inTwoParens],- numParser <- [decimal, exampleNum, defaultNum, romanOne,- lowerAlpha, lowerRoman, upperAlpha, upperRoman]]+anyOrderedListMarker = try $ do+ openParen <- option False $ True <$ char '('+ c <- lookAhead anyChar+ let withDelim f = try $ do+ (style, start) <- f+ delim <-+ ((if style == DefaultStyle+ then DefaultDelim+ else Period) <$ char '.')+ <|>+ ((if openParen+ then TwoParens+ else OneParen) <$ char ')')+ return (start, style, delim)+ case c of+ '@' -> withDelim exampleNum+ '#' -> withDelim defaultNum+ _ | isDigit c -> withDelim exampleNum <|> withDelim decimal+ | isAsciiLower c ->+ withDelim romanOne <|> withDelim lowerAlpha <|> withDelim lowerRoman+ | isAsciiUpper c ->+ withDelim romanOne <|> withDelim upperAlpha <|> withDelim upperRoman+ _ -> mzero -- | Parses a list number (num) followed by a period, returns list attributes. inPeriod :: (Stream s m Char, UpdateSourcePos s Char)
@@ -15,9 +15,10 @@ ) where -import Control.Monad (mzero, when)+import Control.Monad (when, guard) import Data.Text (Text)-import Text.Parsec ((<|>), ParsecT, Stream(..), notFollowedBy, many1, try)+import Text.Parsec ((<|>), ParsecT, Stream(..), notFollowedBy, manyTill,+ many1, try) import Text.Pandoc.Options ( Extension(Ext_tex_math_dollars, Ext_tex_math_single_backslash, Ext_tex_math_double_backslash) )@@ -25,78 +26,80 @@ import Text.Pandoc.Parsing.General import Text.Pandoc.Shared (trimMath) import Text.Pandoc.Sources- (UpdateSourcePos, anyChar, char, digit, newline, satisfy, space, string)+ (UpdateSourcePos, anyChar, char, digit, newline, satisfy, space) import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TB -mathInlineWith :: (Stream s m Char, UpdateSourcePos s Char) => Text -> Text -> ParsecT s st m Text-mathInlineWith op cl = try $ do+mathWith :: (Stream s m Char, UpdateSourcePos s Char)+ => Text -> Text -> ParsecT s st m Text+mathWith op cl = try $ do textStr op when (op == "$") $ notFollowedBy space words' <- many1Till (- (T.singleton <$>- satisfy (\c -> not (isSpaceChar c || c == '\\')))- <|> (char '\\' >>- -- This next clause is needed because \text{..} can- -- contain $, \(\), etc.- (try (string "text" >>- (("\\text" <>) <$> inBalancedBraces 0 ""))- <|> (\c -> T.pack ['\\',c]) <$> anyChar))- <|> ("\n" <$ blankline <* notFollowedBy' blankline <* notFollowedBy (char '$'))- <|> (T.pack <$> many1 spaceChar <* notFollowedBy (char '$'))+ mathComment+ <|> mathGroup+ <|> (T.singleton <$>+ satisfy (\c -> not+ (isSpaceChar c || c == '\\' || c == '{')))+ <|> (T.pack <$> many1 spaceChar <*+ (guard (op /= "$") <|> notFollowedBy (char '$')))+ <|> ("\n" <$ newline <* notFollowedBy' blankline <*+ (guard (op /= "$") <|> notFollowedBy (char '$')))+ <|> (char '\\' >> (\c -> T.pack ['\\',c]) <$> anyChar) ) (try $ textStr cl)- notFollowedBy digit -- to prevent capture of $5+ when (cl == "$") $ notFollowedBy digit -- to prevent capture of $5 return $ trimMath $ T.concat words'- where- inBalancedBraces :: (Stream s m Char, UpdateSourcePos s Char)- => Int -> Text -> ParsecT s st m Text- inBalancedBraces n t =- TL.toStrict . TB.toLazyText <$>- go n False (TB.fromText t) (not (T.null t)) - -- go depth lastWasBackslash accumulator started- go :: (Stream s m Char, UpdateSourcePos s Char)- => Int -> Bool -> TB.Builder -> Bool -> ParsecT s st m TB.Builder- go 0 _ acc False = do- c <- anyChar- if c == '{'- then go 1 False (acc <> TB.singleton '{') True- else mzero- go 0 _ acc True = return acc- go depth True acc _ = do- c <- anyChar- go depth False (acc <> TB.singleton c) True- go depth False acc _ = do- c <- anyChar- let acc' = acc <> TB.singleton c- case c of- '\\' -> go depth True acc' True- '}' -> go (depth - 1) False acc' True- '{' -> go (depth + 1) False acc' True- _ -> go depth False acc' True+-- Preserve comments, but do not interpret their braces or math delimiters.+-- Consume the newline too: it terminates the comment, not the math content.+mathComment :: (Stream s m Char, UpdateSourcePos s Char)+ => ParsecT s st m Text+mathComment = do+ char '%'+ content <- manyTill anyChar newline+ notFollowedBy' blankline+ return $ T.pack ('%' : content ++ "\n") -mathDisplayWith :: (Stream s m Char, UpdateSourcePos s Char) => Text -> Text -> ParsecT s st m Text-mathDisplayWith op cl = try $ fmap T.pack $ do- textStr op- many1Till (satisfy (/= '\n') <|> (newline <* notFollowedBy' blankline))- (try $ textStr cl)+-- Consume a whole TeX group so delimiters in command arguments (for example,+-- \text{hi $x$ bye} or \colorbox{aqua}{$x$}) cannot close the outer math.+-- Escaped braces do not change the nesting depth.+mathGroup :: (Stream s m Char, UpdateSourcePos s Char) => ParsecT s st m Text+mathGroup = do+ char '{'+ TL.toStrict . TB.toLazyText <$> go (1 :: Int) False (TB.singleton '{')+ where+ -- go depth lastWasBackslash accumulator+ go 0 _ acc = return acc+ go depth True acc = do+ c <- mathChar+ go depth False (acc <> TB.singleton c)+ go depth False acc =+ (do comment <- mathComment+ go depth False (acc <> TB.fromText comment))+ <|> do+ c <- mathChar+ let acc' = acc <> TB.singleton c+ case c of+ '\\' -> go depth True acc'+ '}' -> go (depth - 1) False acc'+ '{' -> go (depth + 1) False acc'+ _ -> go depth False acc' +mathChar :: (Stream s m Char, UpdateSourcePos s Char) => ParsecT s st m Char+mathChar = satisfy (/= '\n') <|> (newline <* notFollowedBy' blankline)+ mathDisplay :: (HasReaderOptions st, Stream s m Char, UpdateSourcePos s Char) => ParsecT s st m Text mathDisplay =- (guardEnabled Ext_tex_math_dollars >> mathDisplayWith "$$" "$$")- <|> (guardEnabled Ext_tex_math_single_backslash >>- mathDisplayWith "\\[" "\\]")- <|> (guardEnabled Ext_tex_math_double_backslash >>- mathDisplayWith "\\\\[" "\\\\]")+ (guardEnabled Ext_tex_math_dollars >> mathWith "$$" "$$")+ <|> (guardEnabled Ext_tex_math_single_backslash >> mathWith "\\[" "\\]")+ <|> (guardEnabled Ext_tex_math_double_backslash >> mathWith "\\\\[" "\\\\]") mathInline :: (HasReaderOptions st, Stream s m Char, UpdateSourcePos s Char) => ParsecT s st m Text mathInline =- (guardEnabled Ext_tex_math_dollars >> mathInlineWith "$" "$")- <|> (guardEnabled Ext_tex_math_single_backslash >>- mathInlineWith "\\(" "\\)")- <|> (guardEnabled Ext_tex_math_double_backslash >>- mathInlineWith "\\\\(" "\\\\)")+ (guardEnabled Ext_tex_math_dollars >> mathWith "$" "$")+ <|> (guardEnabled Ext_tex_math_single_backslash >> mathWith "\\(" "\\)")+ <|> (guardEnabled Ext_tex_math_double_backslash >> mathWith "\\\\(" "\\\\)")
@@ -30,6 +30,7 @@ , readPptx , readXlsx , readODT+ , readFODT , readMarkdown , readCommonMark , readCreole@@ -167,6 +168,7 @@ ,("pptx" , ByteStringReader readPptx) ,("xlsx" , ByteStringReader readXlsx) ,("odt" , ByteStringReader readODT)+ ,("fodt" , TextReader readFODT) ,("t2t" , TextReader readTxt2Tags) ,("epub" , ByteStringReader readEPUB) ,("muse" , TextReader readMuse)
@@ -25,7 +25,7 @@ import Text.Pandoc.Definition import Text.Pandoc.Walk import Text.Pandoc.Shared (addPandocAttributes, blocksToInlines, safeRead,- tshow)+ tshow, compactifyTable) import qualified Text.Pandoc.UTF8 as UTF8 import qualified AsciiDoc as A import Text.Pandoc.Error@@ -53,9 +53,6 @@ (\(sourcepos, t) -> A.parseDocument getIncludeFile raiseError (sourceName sourcepos) t) sources)- >>= resolveFootnotes- >>= resolveStem- >>= resolveIcons >>= toPandoc where getIncludeFile fp = UTF8.toText <$> readFileStrict fp@@ -63,64 +60,56 @@ $ msg <> " at " <> show fp <> " char " <> show pos -toPandoc :: PandocMonad m => A.Document -> m Pandoc-toPandoc doc =- Pandoc <$> doMeta (A.docMeta doc)- <*> (B.toList <$> doBlocks (A.docBlocks doc))+-- Context used when converting the AsciiDoc AST: resolved footnote+-- contents, plus document attributes governing stem (math) and icon+-- interpretation. These are used to resolve footnote references,+-- math types, and icons during conversion; doing this in the course+-- of the conversion is much cheaper than making separate passes over+-- the AST with mapInlines/mapBlocks.+data ADContext = ADContext+ { adFootnotes :: M.Map T.Text B.Inlines+ , adMathType :: A.MathType+ , adIconFont :: Bool+ , adIconsDir :: T.Text+ , adIconType :: T.Text+ } -resolveFootnotes :: Monad m => A.Document -> m A.Document-resolveFootnotes doc = do- evalStateT (A.mapInlines go doc) (mempty :: M.Map T.Text [A.Inline])+toPandoc :: PandocMonad m => A.Document -> m Pandoc+toPandoc doc = evalStateT+ (Pandoc <$> doMeta (A.docMeta doc)+ <*> (B.toList <$> doBlocks (A.docBlocks doc)))+ ADContext+ { adFootnotes = mempty+ , adMathType = case M.lookup "stem" docattrs of+ Just "asciimath" -> A.AsciiMath+ _ -> A.LaTeXMath+ , adIconFont = case M.lookup "icons" docattrs of+ Just "font" -> True+ _ -> False+ , adIconsDir = fromMaybe "./images/icons" $ M.lookup "iconsdir" docattrs+ , adIconType = fromMaybe "png" $ M.lookup "icontype" docattrs+ } where- go (A.Inline attr (A.Footnote (Just (A.FootnoteId fnid)) ils)) = do- fnmap <- get- case M.lookup fnid fnmap of- Just ils' ->- pure $ A.Inline attr (A.Footnote (Just (A.FootnoteId fnid)) ils')- Nothing -> do- put $ M.insert fnid ils fnmap- pure $ A.Inline attr (A.Footnote (Just (A.FootnoteId fnid)) ils)- go x = pure x--resolveStem :: Monad m => A.Document -> m A.Document-resolveStem doc = do- let defaultType = case M.lookup "stem" (A.docAttributes (A.docMeta doc)) of- Just "asciimath" -> A.AsciiMath- _ -> A.LaTeXMath- let doInlineStem (A.Inline attr (A.Math Nothing t)) =- pure $ A.Inline attr (A.Math (Just defaultType) t)- doInlineStem x = pure x- let doBlockStem (A.Block attr mbtit (A.MathBlock Nothing t)) =- pure $ A.Block attr mbtit (A.MathBlock (Just defaultType) t)- doBlockStem x = A.mapInlines doInlineStem x- A.mapBlocks doBlockStem doc+ docattrs = A.docAttributes (A.docMeta doc) -- resolve icons as either characters in an icon font or images-resolveIcons :: Monad m => A.Document -> m A.Document-resolveIcons doc = A.mapInlines fromIcon doc+resolveIcon :: ADContext -> A.Inline -> A.Inline+resolveIcon ctx (A.Inline attr (A.Icon name)) =+ if adIconFont ctx+ then A.Inline (addClasses ["fa", "fa-" <> name] attr) (A.Span [])+ else -- default is to use an image+ A.Inline (addClasses ["icon"] attr)+ (A.InlineImage+ (A.Target+ (adIconsDir ctx <> "/" <> name <> "." <> adIconType ctx))+ Nothing Nothing Nothing) where- docattrs = A.docAttributes (A.docMeta doc)- iconFont = case M.lookup "icons" docattrs of- Just "font" -> True- _ -> False- iconsdir = fromMaybe "./images/icons" $ M.lookup "iconsdir" docattrs- icontype = fromMaybe "png" $ M.lookup "icontype" docattrs addClasses cls (A.Attr ps kvs) = A.Attr ps $ case M.lookup "role" kvs of Just r -> M.insert "role" (T.unwords (r : cls)) kvs Nothing -> M.insert "role" (T.unwords cls) kvs- fromIcon (A.Inline attr (A.Icon name)) =- if iconFont- then pure $- A.Inline (addClasses ["fa", "fa-" <> name] attr) (A.Span [])- else pure $ -- default is to use an image- A.Inline (addClasses ["icon"] attr)- (A.InlineImage- (A.Target- (iconsdir <> "/" <> name <> "." <> icontype))- Nothing Nothing Nothing)- fromIcon x = pure x+resolveIcon _ x = x addAttribution :: Maybe A.Attribution -> B.Blocks -> B.Blocks addAttribution Nothing bs = bs@@ -132,7 +121,7 @@ where attrBlock = Para (B.toList $ B.text $ "\x2014 " <> t) -doMeta :: PandocMonad m => A.Meta -> m B.Meta+doMeta :: (PandocMonad m, MonadState ADContext m) => A.Meta -> m B.Meta doMeta meta = do tit' <- doInlines (A.docTitle meta) pure $@@ -164,7 +153,7 @@ " (" <> B.link ("mailto:" <> email) "" (B.str email) <> ")") (A.authorEmail au) -doBlocks :: PandocMonad m => [A.Block] -> m B.Blocks+doBlocks :: (PandocMonad m, MonadState ADContext m) => [A.Block] -> m B.Blocks doBlocks = fmap mconcat . mapM doBlock addBlockAttr :: A.Attr -> B.Blocks -> B.Blocks@@ -184,7 +173,8 @@ let tit = B.toList tit' in case B.toList bs of [B.Table attr _ colspecs thead tbody tfoot] ->- B.singleton $ B.Table attr (B.Caption Nothing [B.Plain tit])+ B.singleton $ compactifyTable+ $ B.Table attr (B.Caption Nothing [B.Plain tit]) colspecs thead tbody tfoot [B.Figure attr _ bs'] -> B.singleton $ B.Figure attr (B.Caption Nothing [B.Plain tit]) bs'@@ -195,7 +185,7 @@ B.singleton $ B.Div attr (B.Div ("",["title"],[]) [B.Para tit] : bs') _ -> B.divWith B.nullAttr (B.divWith ("",["title"],[]) (B.para tit') <> bs) -doBlock :: PandocMonad m => A.Block -> m B.Blocks+doBlock :: (PandocMonad m, MonadState ADContext m) => A.Block -> m B.Blocks doBlock (A.Block attr@(A.Attr ps kvs) mbtitle bt) = do mbtitle' <- case mbtitle of Nothing -> pure Nothing@@ -231,11 +221,12 @@ addAttribution mbattrib . B.blockQuote <$> doBlocks bs A.Verse mbattrib bs -> addAttribution mbattrib . B.blockQuote <$> doBlocks bs- -- TODO when texmath's asciimath parser works, convert:- A.MathBlock (Just A.AsciiMath) t -> pure $ B.para $ B.displayMath t- A.MathBlock (Just A.LaTeXMath) t -> pure $ B.para $ B.displayMath t- A.MathBlock Nothing _ ->- throwError $ PandocParseError "Encountered math type Nothing"+ A.MathBlock mbMathType t -> do+ mathType <- maybe (gets adMathType) pure mbMathType+ case mathType of+ -- TODO when texmath's asciimath parser works, convert:+ A.AsciiMath -> pure $ B.para $ B.displayMath t+ A.LaTeXMath -> pure $ B.para $ B.displayMath t A.List (A.BulletList _) items -> B.bulletList <$> mapM doItem items A.List A.CheckList items ->@@ -271,8 +262,10 @@ (B.RowSpan rowspan) (B.ColSpan colspan) . B.toList <$> doBlocks bs let fromRow (A.TableRow cs) = B.Row B.nullAttr <$> mapM fromCell cs- tbody <- B.TableBody B.nullAttr (B.RowHeadColumns 0) [] <$> mapM fromRow rows+ -- note: conversion is stateful (footnotes), so we convert in+ -- document order: header, body, footer thead <- B.TableHead B.nullAttr <$> maybe (pure []) (mapM fromRow) mbHeader+ tbody <- B.TableBody B.nullAttr (B.RowHeadColumns 0) [] <$> mapM fromRow rows tfoot <- B.TableFoot B.nullAttr <$> maybe (pure []) (mapM fromRow) mbFooter let totalWidth = sum $ map (fromMaybe 1 . A.colWidth) specs let toColSpec spec = (maybe B.AlignDefault toAlign (A.colHorizAlign spec),@@ -281,7 +274,8 @@ fromIntegral x / fromIntegral totalWidth)) (A.colWidth spec)) let colspecs = map toColSpec specs- pure $ B.table (B.Caption Nothing mempty) -- added by addBlockTitle+ pure $ compactifyTable+ $ B.table (B.Caption Nothing mempty) -- added by addBlockTitle colspecs thead [tbody] tfoot A.BlockImage target mbalt mbw mbh -> do img' <- doInline (A.Inline mempty (A.InlineImage target mbalt mbw mbh))@@ -315,7 +309,7 @@ Left _ -> pure $ B.rawBlock "html" t Right (Pandoc _ bs) -> pure $ B.fromList bs -doItem :: PandocMonad m => A.ListItem -> m B.Blocks+doItem :: (PandocMonad m, MonadState ADContext m) => A.ListItem -> m B.Blocks doItem (A.ListItem Nothing bs) = doBlocks bs doItem (A.ListItem (Just checkstate) bs) = do bs' <- doBlocks bs@@ -328,18 +322,24 @@ (B.Plain ils : rest) -> B.Plain (check : B.Space : ils) : rest rest -> B.Para [check] : rest -doDefListItem :: PandocMonad m+doDefListItem :: (PandocMonad m, MonadState ADContext m) => ([A.Inline], [A.Block]) -> m (B.Inlines , [B.Blocks]) doDefListItem (lab, bs) = do lab' <- doInlines lab bs' <- doBlocks bs pure (lab', [bs']) -doInlines :: PandocMonad m => [A.Inline] -> m B.Inlines+doInlines :: (PandocMonad m, MonadState ADContext m) => [A.Inline] -> m B.Inlines doInlines = fmap mconcat . mapM doInline -doInline :: PandocMonad m => A.Inline -> m B.Inlines-doInline (A.Inline (A.Attr _ps kvs') it) = do+doInline :: (PandocMonad m, MonadState ADContext m) => A.Inline -> m B.Inlines+doInline il@(A.Inline _ A.Icon{}) = do+ ctx <- get+ doInline' (resolveIcon ctx il)+doInline il = doInline' il++doInline' :: (PandocMonad m, MonadState ADContext m) => A.Inline -> m B.Inlines+doInline' (A.Inline (A.Attr _ps kvs') it) = do let kvs = M.mapKeys (\k -> if k == "role" then "class" else k) kvs' addPandocAttributes (M.toList kvs) <$> case it of@@ -354,12 +354,14 @@ A.Strikethrough ils -> B.strikeout <$> doInlines ils A.DoubleQuoted ils -> B.doubleQuoted <$> doInlines ils A.SingleQuoted ils -> B.singleQuoted <$> doInlines ils- -- TODO when texmath's asciimath parser works, convert:- A.Math (Just A.AsciiMath) t -> pure $ B.math t- A.Math (Just A.LaTeXMath) t -> pure $ B.math t- A.Math Nothing _ ->- throwError $ PandocParseError "Encountered math type Nothing"- A.Icon t -> pure $ B.spanWith ("",["icon"],[("name",t)])+ A.Math mbMathType t -> do+ mathType <- maybe (gets adMathType) pure mbMathType+ case mathType of+ -- TODO when texmath's asciimath parser works, convert:+ A.AsciiMath -> pure $ B.math t+ A.LaTeXMath -> pure $ B.math t+ A.Icon t -> -- can't happen (rewritten by resolveIcon in doInline)+ pure $ B.spanWith ("",["icon"],[("name",t)]) (B.str ("[" <> t <> "]")) A.Button t -> pure $ B.spanWith ("",["button"],[]) (B.strong $ B.str ("[" <> t <> "]"))@@ -380,7 +382,19 @@ Just (A.Height n) -> [("height", T.pack $ show n <> "px")] Nothing -> [] pure $ B.imageWith ("",[], width ++ height) url "" alt- A.Footnote _ ils -> B.note . B.para <$> doInlines ils+ A.Footnote (Just (A.FootnoteId fnid)) ils -> do+ -- repeated references to the same id get the contents of the+ -- first footnote with that id+ contents <- doInlines ils+ fnmap <- gets adFootnotes+ contents' <- case M.lookup fnid fnmap of+ Just stored -> pure stored+ Nothing -> do+ modify $ \ctx ->+ ctx{ adFootnotes = M.insert fnid contents fnmap }+ pure contents+ pure $ B.note $ B.para contents'+ A.Footnote Nothing ils -> B.note . B.para <$> doInlines ils A.InlineAnchor t _ -> pure $ B.spanWith (t, [], []) mempty A.BibliographyAnchor t _ -> pure $ B.spanWith (t, [], []) mempty A.CrossReference t Nothing ->
@@ -79,12 +79,12 @@ makeFigures b = b sourceToToks :: (SourcePos, Text) -> [Tok]-sourceToToks (pos, s) = map adjust $ tokenize (sourceName pos) s+sourceToToks (pos, s) =+ case sourceLine pos of+ 1 -> toks+ n -> map (\tok -> tok{ tokPos = incSourceLine (tokPos tok) (n - 1) }) toks where- adjust = case sourceLine pos of- 1 -> id- n -> \tok -> tok{ tokPos =- incSourceLine (tokPos tok) (n - 1) }+ toks = tokenize (sourceName pos) s metaValueParser :: Monad m@@ -102,7 +102,7 @@ then walk makeFigures else id) . (if isEnabled Ext_tex_math_gfm opts- then walk handleGfmMath+ then walk handleGfmMathBlock . walk handleGfmMathInline else id) . (if readerStripComments opts then walk stripBlockComments . walk stripInlineComments@@ -115,9 +115,9 @@ Left err -> throwError $ fromParsecError s err Right (Cm bls :: Cm () Blocks) -> return $ B.doc bls -handleGfmMath :: Block -> Block-handleGfmMath (CodeBlock ("",["math"],[]) raw) = Para [Math DisplayMath raw]-handleGfmMath x = walk handleGfmMathInline x+handleGfmMathBlock :: Block -> Block+handleGfmMathBlock (CodeBlock ("",["math"],[]) raw) = Para [Math DisplayMath raw]+handleGfmMathBlock x = x handleGfmMathInline :: Inline -> Inline handleGfmMathInline (Math InlineMath math'') =@@ -167,6 +167,8 @@ [ (superscriptSpec <>) | isEnabled Ext_superscript opts ] ++ [ (subscriptSpec <>) | isEnabled Ext_subscript opts ] ++ [ (mathSpec <>) | isEnabled Ext_tex_math_dollars opts ] +++ [ (taskListSpec <>) | isEnabled Ext_task_lists opts ] +++ -- note: task_list before fancy_list, work around jgm/commonmark#180 [ (fancyListSpec <>) | isEnabled Ext_fancy_lists opts ] ++ [ (fencedDivSpec <>) | isEnabled Ext_fenced_divs opts ] ++ [ (bracketedSpanSpec <>) | isEnabled Ext_bracketed_spans opts ] ++@@ -187,7 +189,6 @@ | isEnabled Ext_implicit_header_references opts ] ++ [ (footnoteSpec <>) | isEnabled Ext_footnotes opts ] ++ [ (definitionListSpec <>) | isEnabled Ext_definition_lists opts ] ++- [ (taskListSpec <>) | isEnabled Ext_task_lists opts ] ++ [ (wikilinksSpec TitleAfterPipe <>) | isEnabled Ext_wikilinks_title_after_pipe opts ] ++ [ (wikilinksSpec TitleBeforePipe <>)
@@ -62,6 +62,33 @@ specialChars :: [Char] specialChars = "*/~{}\\|[]()<>\"'" +-- | True for characters that terminate a run of plain text, i.e. the+-- 'specialChars' plus tabs, newlines and spaces. Written as a @case@ so+-- that GHC compiles it to a jump table; this is applied to every single+-- character of the input, so a linear scan over the list here is a+-- significant cost. Must be kept in sync with 'specialChars'.+isStrEnd :: Char -> Bool+isStrEnd c = case c of+ '\t' -> True+ '\n' -> True+ ' ' -> True+ '"' -> True+ '\'' -> True+ '(' -> True+ ')' -> True+ '*' -> True+ '/' -> True+ '<' -> True+ '>' -> True+ '[' -> True+ '\\' -> True+ ']' -> True+ '{' -> True+ '|' -> True+ '}' -> True+ '~' -> True+ _ -> False+ parseCreole :: PandocMonad m => CRLParser m Pandoc parseCreole = do bs <- mconcat <$> many block@@ -91,7 +118,7 @@ where content = brackets <|> line brackets = try $ option "" (T.singleton <$> newline)- <+> (char ' ' >> (manyChar (char ' ') <+> textStr "}}}") <* eol)+ <+> (char ' ' >> (takeWhileP (== ' ') <+> textStr "}}}") <* eol) line = option "" (T.singleton <$> newline) <+> manyTillChar anyChar eol eol = lookAhead $ try $ nowikiEnd <|> newline nowikiStart = optional newline >> string "{{{" >> skipMany spaceChar >> newline@@ -215,8 +242,8 @@ (orig, src) <- wikiImg return $ B.image src "" (B.str orig) where- linkSrc = manyChar $ noneOf "|}\n\r\t"- linkDsc = char '|' >> manyChar (noneOf "}\n\r\t")+ linkSrc = takeWhileP (`notElem` ("|}\n\r\t" :: [Char]))+ linkDsc = char '|' >> takeWhileP (`notElem` ("}\n\r\t" :: [Char])) wikiImg = try $ do string "{{" src <- linkSrc@@ -229,11 +256,11 @@ (orig, src) <- uriLink <|> wikiLink return $ B.link src "" orig where- linkSrc = manyChar $ noneOf "|]\n\r\t"+ linkSrc = takeWhileP (`notElem` ("|]\n\r\t" :: [Char])) linkDsc :: PandocMonad m => Text -> CRLParser m B.Inlines linkDsc otxt = B.str <$> try (option otxt- (char '|' >> manyChar (noneOf "]\n\r\t")))+ (char '|' >> takeWhileP (`notElem` ("]\n\r\t" :: [Char])))) linkImg = try $ char '|' >> image wikiLink = try $ do string "[["@@ -272,8 +299,7 @@ symbol = fmap (B.str . T.singleton) (oneOf specialChars) str :: PandocMonad m => CRLParser m B.Inlines-str = let strChar = noneOf ("\t\n " ++ specialChars) in- fmap B.str (many1Char strChar)+str = fmap B.str (takeWhile1P (not . isStrEnd)) bold :: PandocMonad m => CRLParser m B.Inlines bold = B.strong . mconcat <$>
@@ -25,7 +25,8 @@ import Text.Parsec.Pos (newPos) import Text.Pandoc.Options import Text.Pandoc.Definition-import Text.Pandoc.Shared (addPandocAttributes, tshow)+import Text.Pandoc.Shared (addPandocAttributes, tshow, compactifyTable,+ paraToPlain) import qualified Text.Pandoc.UTF8 as UTF8 import Djot (ParseOptions(..), SourcePosOption(..), parseDoc, Pos(..)) import qualified Djot.AST as D@@ -72,8 +73,11 @@ D.Section bls -> divWith ("",["section"],[]) <$> convertBlocks bls D.Heading lev ils -> header lev <$> convertInlines ils D.BlockQuote bls -> blockQuote <$> convertBlocks bls- D.CodeBlock lang bs -> pure $- codeBlockWith ("", [UTF8.toText lang], []) $ UTF8.toText bs+ D.CodeBlock lang bs ->+ let classes = case UTF8.toText lang of+ "" -> []+ l -> [l]+ in pure $ codeBlockWith ("", classes, []) $ UTF8.toText bs D.Div bls -> divWith nullAttr <$> convertBlocks bls D.OrderedList olattr listSpacing items -> orderedListWith olattr' .@@ -151,7 +155,8 @@ mapM toRow hs <*> mapM toRow rs tbodies <- mapM toTableBody bodies let tfoot = TableFoot mempty []- pure $ singleton $ Table mempty capt colspecs thead tbodies tfoot+ pure $ singleton $ compactifyTable+ $ Table mempty capt colspecs thead tbodies tfoot D.RawBlock (D.Format fmt) bs -> pure $ rawBlock (UTF8.toText fmt) (UTF8.toText bs) @@ -255,9 +260,6 @@ toTight :: Blocks -> Blocks toTight (Many bls) = Many $ paraToPlain <$> bls- where- paraToPlain (Para ils) = Plain ils- paraToPlain x = x toTaskListItem :: PandocMonad m => (D.TaskStatus, D.Blocks) -> ReaderT Env m Blocks
@@ -46,7 +46,7 @@ import Text.Pandoc.Class.PandocMonad (PandocMonad, report) import Text.Pandoc.Options import Text.Pandoc.Logging (LogMessage(..))-import Text.Pandoc.Shared (safeRead, addPandocAttributes)+import Text.Pandoc.Shared (safeRead, addPandocAttributes, paraToPlain) import Text.Pandoc.Sources (ToSources(..), sourcesToText) import Text.Pandoc.Transforms (headerShift) import Text.TeXMath (readMathML, writeTeX)@@ -1481,10 +1481,6 @@ -- | Set the prefix of a name to 'Nothing' removePrefix :: QName -> QName removePrefix elname = elname { qPrefix = Nothing }--paraToPlain :: Block -> Block-paraToPlain (Para ils) = Plain ils-paraToPlain x = x docbookEntityMap :: M.Map Text Text docbookEntityMap = M.fromList
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternGuards #-} {-# LANGUAGE FlexibleContexts #-}@@ -60,6 +59,7 @@ ) where import Codec.Archive.Zip+import Control.Applicative ((<|>)) import Control.Monad ( liftM, unless ) import Control.Monad.Reader ( asks,@@ -80,7 +80,6 @@ import qualified Data.Map as M import qualified Data.Text as T import Data.Maybe (isJust, fromMaybe, mapMaybe)-import Data.Sequence (ViewL (..), viewl) import qualified Data.Sequence as Seq import qualified Data.Set as Set import Citeproc (ItemId(..), Val(TextVal,FancyVal), Reference(..), CitationItem(..))@@ -133,6 +132,11 @@ , docxListState :: M.Map (T.Text, T.Text) Integer , docxPrevPara :: Inlines , docxReferences :: M.Map ItemId (Reference Inlines)+ -- the open list levels, innermost first, as+ -- (text indentation, level, numId); used to+ -- recognize unnumbered paragraphs continuing+ -- a list item+ , docxOpenListLevels :: [(Integer, T.Text, T.Text)] } instance Default DState where@@ -145,6 +149,7 @@ , docxListState = M.empty , docxPrevPara = mempty , docxReferences = mempty+ , docxOpenListLevels = [] } data DEnv = DEnv { docxOptions :: ReaderOptions@@ -267,8 +272,12 @@ parPartToText :: ParPart -> T.Text parPartToText (PlainRun run) = runToText run-parPartToText (InternalHyperLink _ children) = T.concat $ map parPartToText children-parPartToText (ExternalHyperLink _ children) = T.concat $ map parPartToText children+parPartToText (InternalHyperLink _ _ children) =+ T.concat $ map parPartToText children+parPartToText (ExternalHyperLink _ _ children) =+ T.concat $ map parPartToText children+parPartToText (Field _ children) =+ T.concat $ map parPartToText children parPartToText _ = "" blacklistedCharStyles :: [CharStyleName]@@ -311,7 +320,9 @@ | Just True <- isSmallCaps rPr = smallcaps . go rPr{isSmallCaps = Nothing} | Just True <- isStrike rPr =- strikeout . go rPr{isStrike = Nothing}+ strikeout . go rPr{isStrike = Nothing, isDoubleStrike = Nothing}+ | Just True <- isDoubleStrike rPr =+ strikeout . go rPr{isDoubleStrike = Nothing} | Just True <- isRTL rPr = spanWith ("",[],[("dir","rtl")]) . go rPr{isRTL = Nothing} | inBidi, Just False <- isRTL rPr =@@ -320,7 +331,8 @@ superscript . go rPr{rVertAlign = Nothing} | Just SubScrpt <- rVertAlign rPr = subscript . go rPr{rVertAlign = Nothing}- | Just "single" <- rUnderline rPr =+ | Just u <- rUnderline rPr+ , u /= "none" = Pandoc.underline . go rPr{rUnderline = Nothing} | otherwise = id return $ go rPr'@@ -340,8 +352,8 @@ let ils = smushInlines (map runElemToInlines runElems) transform <- runStyleToTransform rPr return $ transform ils-runToInlines (Footnote bps) = note . smushBlocks <$> mapM bodyPartToBlocks bps-runToInlines (Endnote bps) = note . smushBlocks <$> mapM bodyPartToBlocks bps+runToInlines (Footnote bps) = note <$> notePartsToBlocks bps+runToInlines (Endnote bps) = note <$> notePartsToBlocks bps runToInlines (InlineDrawing fp title alt bs ext) = do (lift . lift) $ P.insertMedia fp Nothing bs return $ imageWith (extentToAttr ext) (T.pack fp) title $ text alt@@ -355,6 +367,19 @@ showDim d = tshow (d / 914400) <> "in" extentToAttr _ = nullAttr +-- A note is read in the middle of the paragraph that references it.+-- Text held back from a paragraph whose mark is a tracked change+-- (docxPrevPara) must therefore neither flow into the note from the+-- body, nor out of the note into the referencing paragraph.+notePartsToBlocks :: PandocMonad m => [BodyPart] -> DocxContext m Blocks+notePartsToBlocks bps = do+ outerPrevPara <- gets docxPrevPara+ modify $ \s -> s { docxPrevPara = mempty }+ blks <- smushBlocks <$> mapM bodyPartToBlocks bps+ leftover <- gets docxPrevPara+ modify $ \s -> s { docxPrevPara = outerPrevPara }+ return $ if null leftover then blks else blks <> para leftover+ blocksToInlinesWarn :: PandocMonad m => T.Text -> Blocks -> DocxContext m Inlines blocksToInlinesWarn cmtId blks = do let paraOrPlain :: Block -> Bool@@ -411,14 +436,15 @@ AllChanges -> do blks <- smushBlocks <$> mapM bodyPartToBlocks bodyParts ils <- blocksToInlinesWarn cmtId blks- let attr = ("", ["comment-start"], ("id", cmtId) : addAuthorAndDate author date)+ let attr = ("", ["comment-start"], ("comment-id", cmtId) :+ addAuthorAndDate author date) return $ spanWith attr ils _ -> return mempty parPartToInlines' (CommentEnd cmtId) = do opts <- asks docxOptions case readerTrackChanges opts of AllChanges -> do- let attr = ("", ["comment-end"], [("id", cmtId)])+ let attr = ("", ["comment-end"], [("comment-id", cmtId)]) return $ spanWith attr mempty _ -> return mempty parPartToInlines' (BookMark _ anchor) | anchor `elem` dummyAnchors =@@ -461,19 +487,20 @@ return $ spanWith ("", ["chart"], []) $ text "[CHART]" parPartToInlines' Diagram = return $ spanWith ("", ["diagram"], []) $ text "[DIAGRAM]"-parPartToInlines' (InternalHyperLink anchor children) = do+parPartToInlines' (InternalHyperLink anchor tooltip children) = do ils <- smushInlines <$> mapM parPartToInlines' children- return $ link ("#" <> anchor) "" ils-parPartToInlines' (ExternalHyperLink target children) = do+ return $ link ("#" <> anchor) tooltip ils+parPartToInlines' (ExternalHyperLink target tooltip children) = do ils <- smushInlines <$> mapM parPartToInlines' children- return $ link target "" ils+ return $ link target tooltip ils parPartToInlines' (PlainOMath exps) = return $ math $ writeTeX exps parPartToInlines' (OMathPara exps) = return $ displayMath $ writeTeX exps parPartToInlines' (Field info children) = case info of- HyperlinkField url -> parPartToInlines' $ ExternalHyperLink url children+ HyperlinkField url ->+ parPartToInlines' $ ExternalHyperLink url "" children IndexrefField ie -> pure $ spanWith ("",["indexref"], (("entry", entryTitle ie) :@@ -481,8 +508,10 @@ ++ maybe [] (\x -> [("yomi",x)]) (entryYomi ie) ++ [("bold","") | entryBold ie] ++ [("italic","") | entryItalic ie])) mempty- PagerefField fieldAnchor True -> parPartToInlines' $ InternalHyperLink fieldAnchor children- CrossrefField fieldAnchor True -> parPartToInlines' $ InternalHyperLink fieldAnchor children+ PagerefField fieldAnchor True ->+ parPartToInlines' $ InternalHyperLink fieldAnchor "" children+ CrossrefField fieldAnchor True ->+ parPartToInlines' $ InternalHyperLink fieldAnchor "" children EndNoteCite t -> do formattedCite <- smushInlines <$> mapM parPartToInlines' children opts <- asks docxOptions@@ -605,18 +634,10 @@ return $ Header n (newIdent, classes, kvs) ils makeHeaderAnchor' blk = return blk --- Rewrite a standalone paragraph block as a plain-singleParaToPlain :: Blocks -> Blocks-singleParaToPlain blks- | (Para ils :< seeq) <- viewl $ unMany blks- , Seq.null seeq =- singleton $ Plain ils-singleParaToPlain blks = blks- cellToCell :: PandocMonad m => RowSpan -> Docx.Cell -> DocxContext m Pandoc.Cell cellToCell rowSpan (Docx.Cell align gridSpan _ bps) = do blks <- smushBlocks <$> mapM bodyPartToBlocks bps- let blks' = singleParaToPlain $ fromList $ blocksToDefinitions $ blocksToBullets $ toList blks+ let blks' = fromList $ blocksToDefinitions $ blocksToBullets $ toList blks return (cell (convertAlign align) rowSpan (ColSpan (fromIntegral gridSpan)) blks') @@ -695,7 +716,17 @@ | otherwise = c bodyPartToBlocks :: PandocMonad m => BodyPart -> DocxContext m Blocks-bodyPartToBlocks (Heading n style pPr numId lvl mblvlInfo parparts) = do+bodyPartToBlocks bp =+ case bp of+ Paragraph{} -> bodyPartToBlocks' bp+ ListItem{} -> bodyPartToBlocks' bp+ -- anything else ends the open lists:+ _ -> closeListLevels *> bodyPartToBlocks' bp <* closeListLevels+ where+ closeListLevels = modify $ \s -> s{ docxOpenListLevels = [] }++bodyPartToBlocks' :: PandocMonad m => BodyPart -> DocxContext m Blocks+bodyPartToBlocks' (Heading n style pPr numId lvl mblvlInfo parparts) = do ils <- local (\s-> s{docxInHeaderBlock=True}) (smushInlines <$> mapM parPartToInlines parparts) let classes = map normalizeToClassName . delete style@@ -713,7 +744,7 @@ let start = case M.lookup (numId, lvl) listState of Nothing -> case mblvlInfo of Nothing -> 1- Just (Level _ _ _ z) -> fromMaybe 1 z+ Just (Level _ _ _ z _) -> fromMaybe 1 z Just z -> z + 1 modify $ \st -> st{ docxListState = -- expire all the continuation data for lists of level > this one:@@ -723,77 +754,28 @@ in M.insert (numId, lvl) start (M.filterWithKey notExpired listState) } makeHeaderAnchor $ headerWith ("", addNum classes, []) n ils-bodyPartToBlocks (Paragraph pPr parparts) = do- let getMainPar- | Just True <- pBidi pPr = do- let pPr' = pPr { pBidi = Nothing }- local (\s -> s{ docxInBidi = True })- (bodyPartToBlocks (Paragraph pPr' parparts))- | isCodeDiv pPr = do- transform <- paragraphStyleToTransform pPr- return $- transform $- codeBlock $- T.concat $- map parPartToText parparts- | otherwise = do- ils <- trimSps . smushInlines <$> mapM parPartToInlines parparts- prevParaIls <- gets docxPrevPara- dropIls <- gets docxDropCap- let ils' = dropIls <> ils- let (paraOrPlain, pPr')- | hasStylesInheritedFrom ["Compact"] pPr = (plain, removeStyleNamed "Compact" pPr)- | otherwise = (para, pPr)- if dropCap pPr'- then do modify $ \s -> s { docxDropCap = ils' }- return mempty- else do modify $ \s -> s { docxDropCap = mempty }- let ils'' = (if null prevParaIls then mempty- else prevParaIls <> space) <> ils'- handleInsertion = do- modify $ \s -> s {docxPrevPara = mempty}- transform <- paragraphStyleToTransform pPr'- return $ transform $ paraOrPlain ils''- opts <- asks docxOptions- case (pChange pPr', readerTrackChanges opts) of- _ | null ils'', not (isEnabled Ext_empty_paragraphs opts) ->- return mempty- (Just (TrackedChange Insertion _), AcceptChanges) ->- handleInsertion- (Just (TrackedChange Insertion _), RejectChanges) -> do- modify $ \s -> s {docxPrevPara = ils''}- return mempty- (Just (TrackedChange Insertion (ChangeInfo _ cAuthor cDate))- , AllChanges) -> do- let attr = ("", ["paragraph-insertion"], addAuthorAndDate cAuthor cDate)- insertMark = spanWith attr mempty- transform <- paragraphStyleToTransform pPr'- return $ transform $- paraOrPlain $ ils'' <> insertMark- (Just (TrackedChange Deletion _), AcceptChanges) -> do- modify $ \s -> s {docxPrevPara = ils''}- return mempty- (Just (TrackedChange Deletion _), RejectChanges) ->- handleInsertion- (Just (TrackedChange Deletion (ChangeInfo _ cAuthor cDate))- , AllChanges) -> do- let attr = ("", ["paragraph-deletion"], addAuthorAndDate cAuthor cDate)- insertMark = spanWith attr mempty- transform <- paragraphStyleToTransform pPr'- return $ transform $- paraOrPlain $ ils'' <> insertMark- _ -> handleInsertion- (if pBottomBorder pPr- then (<> horizontalRule)- else id)- <$> getMainPar-bodyPartToBlocks (ListItem pPr numId lvl isRestart (Just levelInfo) parparts) = do+bodyPartToBlocks' (Paragraph pPr parparts) = do+ openLevels <- gets docxOpenListLevels+ let leftIndent = (indentation pPr >>= leftParIndent) <|>+ (pStyleIndentation pPr >>= leftParIndent)+ -- An unnumbered paragraph indented like the text of an open list+ -- item continues that item (this is how pandoc's docx writer and+ -- Word's "List Continue" styles do multi-paragraph list items):+ case break (\(ind, _, _) -> Just ind == leftIndent) openLevels of+ (_, levels@((_, lvl, numId) : _)) | not (numbered pPr) -> do+ modify $ \s -> s{ docxOpenListLevels = levels }+ divWith ("", ["list-continuation"], [("level", lvl), ("num-id", numId)])+ <$> paragraphToBlocks pPr{ indentation = Nothing } parparts+ _ -> do+ modify $ \s -> s{ docxOpenListLevels = [] }+ paragraphToBlocks pPr parparts+bodyPartToBlocks' (ListItem pPr numId lvl isRestart (Just levelInfo) parparts) = do -- We check whether this current abstract numbering id has previously been -- used, since Docx expects us to pick up where we left off -- unless this -- item explicitly restarts numbering (see #8367). listState <- gets docxListState let startFromState = M.lookup (numId, lvl) listState- Level _ fmt txt startFromLevelInfo = levelInfo+ Level _ fmt txt startFromLevelInfo lvlIndent = levelInfo start = case startFromState of Just n | not isRestart -> n + 1 _ -> fromMaybe 1 startFromLevelInfo@@ -808,13 +790,20 @@ -- a new level 1 list item resets continuation for level 2+ let notExpired (_, lvl') _ = lvl' <= lvl in M.insert (numId, lvl) start (M.filterWithKey notExpired listState) }- blks <- bodyPartToBlocks (Paragraph pPr parparts)+ -- record the indentation of the item's text, so that we can recognize+ -- unnumbered paragraphs continuing this item:+ let textIndent = (indentation pPr >>= leftParIndent) <|> lvlIndent <|>+ (pStyleIndentation pPr >>= leftParIndent)+ modify $ \st -> st{ docxOpenListLevels =+ maybe id (\ind -> ((ind, lvl, numId) :)) textIndent+ (filter (\(_, lvl', _) -> lvl' < lvl) (docxOpenListLevels st)) }+ blks <- paragraphToBlocks pPr parparts return $ divWith ("", ["list-item"], kvs) blks-bodyPartToBlocks (ListItem pPr _ _ _ _ parparts) =+bodyPartToBlocks' (ListItem pPr _ _ _ _ parparts) = let pPr' = pPr {pStyle = constructBogusParStyleData "list-paragraph": pStyle pPr} in bodyPartToBlocks $ Paragraph pPr' parparts-bodyPartToBlocks (Captioned parstyle parparts bpart) = do+bodyPartToBlocks' (Captioned parstyle parparts bpart) = do bs <- bodyPartToBlocks bpart captContents <- bodyPartToBlocks (Paragraph parstyle parparts) let capt = Caption Nothing (toList captContents)@@ -831,9 +820,9 @@ -> toCaptioned attr' bls' _ -> captContents pure $ toCaptioned nullAttr (toList bs)-bodyPartToBlocks (Tbl _ _ _ _ []) =+bodyPartToBlocks' (Tbl _ _ _ _ []) = return mempty-bodyPartToBlocks (Tbl mbsty cap grid look parts) = do+bodyPartToBlocks' (Tbl mbsty cap grid look parts) = do let fullCaption = if T.null cap then mempty else plain (text cap) let shortCaption = if T.null cap then Nothing else Just (toList (text cap)) cap' = caption shortCaption fullCaption@@ -848,11 +837,10 @@ -- Horizontal column alignment is taken from the first row's cells. numcols = length widths getAlignment (Docx.Cell al colspan _ _) =- take numcols $ replicate (fromIntegral colspan) (convertAlign al) ++- repeat Pandoc.AlignDefault+ replicate (fromIntegral colspan) (convertAlign al) getAlignments (Docx.Row _ cs) = concatMap getAlignment cs alignments = take numcols $- case hdr ++ rows of+ case rows ++ hdr of [] -> repeat Pandoc.AlignDefault r : _ -> getAlignments r @@ -860,12 +848,81 @@ let attr = case mbsty of Just sty | extStylesEnabled -> ("", [], [("custom-style", sty)]) _ -> nullAttr- return $ tableWith attr cap'+ return $ compactifyTable+ $ tableWith attr cap' (zip alignments widths) (TableHead nullAttr headerCells) [TableBody nullAttr 0 [] bodyCells] (TableFoot nullAttr [])-bodyPartToBlocks HRule = pure Pandoc.horizontalRule+bodyPartToBlocks' HRule = pure Pandoc.horizontalRule++-- | Convert a paragraph (also used for the text of list items).+paragraphToBlocks :: PandocMonad m+ => ParagraphStyle -> [ParPart] -> DocxContext m Blocks+paragraphToBlocks pPr parparts = do+ let getMainPar+ | Just True <- pBidi pPr = do+ let pPr' = pPr { pBidi = Nothing }+ local (\s -> s{ docxInBidi = True })+ (paragraphToBlocks pPr' parparts)+ | isCodeDiv pPr = do+ transform <- paragraphStyleToTransform pPr+ return $+ transform $+ codeBlock $+ T.concat $+ map parPartToText parparts+ | otherwise = do+ ils <- trimSps . smushInlines <$> mapM parPartToInlines parparts+ prevParaIls <- gets docxPrevPara+ dropIls <- gets docxDropCap+ let ils' = dropIls <> ils+ let (paraOrPlain, pPr')+ | hasStylesInheritedFrom ["Compact"] pPr = (plain, removeStyleNamed "Compact" pPr)+ | otherwise = (para, pPr)+ if dropCap pPr'+ then do modify $ \s -> s { docxDropCap = ils' }+ return mempty+ else do modify $ \s -> s { docxDropCap = mempty }+ let ils'' = (if null prevParaIls then mempty+ else prevParaIls <> space) <> ils'+ handleInsertion = do+ modify $ \s -> s {docxPrevPara = mempty}+ transform <- paragraphStyleToTransform pPr'+ return $ transform $ paraOrPlain ils''+ opts <- asks docxOptions+ case (pChange pPr', readerTrackChanges opts) of+ _ | null ils'', not (isEnabled Ext_empty_paragraphs opts) ->+ return mempty+ (Just (TrackedChange Insertion _), AcceptChanges) ->+ handleInsertion+ (Just (TrackedChange Insertion _), RejectChanges) -> do+ modify $ \s -> s {docxPrevPara = ils''}+ return mempty+ (Just (TrackedChange Insertion (ChangeInfo _ cAuthor cDate))+ , AllChanges) -> do+ let attr = ("", ["paragraph-insertion"], addAuthorAndDate cAuthor cDate)+ insertMark = spanWith attr mempty+ transform <- paragraphStyleToTransform pPr'+ return $ transform $+ paraOrPlain $ ils'' <> insertMark+ (Just (TrackedChange Deletion _), AcceptChanges) -> do+ modify $ \s -> s {docxPrevPara = ils''}+ return mempty+ (Just (TrackedChange Deletion _), RejectChanges) ->+ handleInsertion+ (Just (TrackedChange Deletion (ChangeInfo _ cAuthor cDate))+ , AllChanges) -> do+ let attr = ("", ["paragraph-deletion"], addAuthorAndDate cAuthor cDate)+ insertMark = spanWith attr mempty+ transform <- paragraphStyleToTransform pPr'+ return $ transform $+ paraOrPlain $ ils'' <> insertMark+ _ -> handleInsertion+ (if pBottomBorder pPr+ then (<> horizontalRule)+ else id)+ <$> getMainPar -- replace targets with generated anchors. rewriteLink' :: PandocMonad m => Inline -> DocxContext m Inline
@@ -80,8 +80,29 @@ let (fs, ils') = unstackInlines ils (left, (right, contents')) = second (spanr isSpace) $ spanl isSpace $ unMany ils' -- NOTE: spanr counterintuitively returns suffix as the FIRST tuple element- in (Many left, (fs, Many contents'), Many right)+ in case breakAtTrackedChange fs of+ -- A space at the edge of an inserted or deleted run belongs to the+ -- change: moving it out of the span would lose it when the change is+ -- accepted or rejected later (#4427). It is still moved out of the+ -- formatting inside the change.+ Just (outer, inner) ->+ (mempty, (outer, Many left <> stackInlines inner (Many contents')+ <> Many right), mempty)+ Nothing -> (Many left, (fs, Many contents'), Many right) +-- | Split a modifier stack (outermost first) after its innermost+-- tracked-change span, if it has one.+breakAtTrackedChange :: [Modifier a] -> Maybe ([Modifier a], [Modifier a])+breakAtTrackedChange fs =+ case L.findIndex isTrackedChange (reverse fs) of+ Nothing -> Nothing+ Just i -> Just (splitAt (length fs - i) fs)++isTrackedChange :: Modifier a -> Bool+isTrackedChange (AttrModifier _ (_, classes, _)) =+ any (`elem` classes) ["insertion", "deletion"]+isTrackedChange _ = False+ isSpace :: Inline -> Bool isSpace Space = True isSpace SoftBreak = True@@ -170,6 +191,13 @@ , CodeBlock attr' codeStr' :< cs' <- viewl (unMany cs) , attr == attr' = Many $ (bs' |> CodeBlock attr (codeStr <> "\n" <> codeStr')) >< cs'+ -- with +styles, each code paragraph is wrapped in a custom-style Div (#9183)+ | bs' :> Div divAttr [CodeBlock attr codeStr] <- viewr (unMany bs)+ , Div divAttr' [CodeBlock attr' codeStr'] :< cs' <- viewl (unMany cs)+ , divAttr == divAttr'+ , attr == attr' =+ Many $ (bs' |> Div divAttr [CodeBlock attr (codeStr <> "\n" <> codeStr')])+ >< cs' combineBlocks bs cs = bs <> cs instance (Monoid a, Eq a) => Eq (Modifier a) where
@@ -32,6 +32,11 @@ isListItem (Div (_, classes, _) _) | "list-item" `elem` classes = True isListItem _ = False +-- | An unnumbered paragraph continuing a list item.+isListContinuation :: Block -> Bool+isListContinuation (Div (_, classes, _) _) = "list-continuation" `elem` classes+isListContinuation _ = False+ getLevel :: Block -> Maybe Integer getLevel (Div (_, _, kvs) _) = lookup "level" kvs >>= safeRead getLevel _ = Nothing@@ -115,6 +120,8 @@ separateBlocks' b@(OrderedList _ _) acc = init acc ++ [last acc ++ [b]] -- The following is for the invisible bullet lists. This is how -- pandoc-generated ooxml does multiparagraph item lists.+separateBlocks' b acc | isListContinuation b =+ init acc ++ [last acc ++ [b]] separateBlocks' b acc | fmap trim (getText b) == Just "" = init acc ++ [last acc ++ [b]] separateBlocks' b acc = acc ++ [[b]]@@ -219,6 +226,8 @@ case delete "list-item" classes of [] -> blks classes' -> [Div (ident, classes', kvs) blks]+removeListDivs' b@(Div _ blks)+ | isListContinuation b = blks removeListDivs' (Div (ident, classes, kvs) blks) | not $ null $ listParagraphDivs `intersect` classes = case classes \\ listParagraphDivs of
@@ -109,6 +109,7 @@ , envFont :: Maybe Font , envCharStyles :: CharStyleMap , envParStyles :: ParStyleMap+ , envTableStyles :: TableStyleMap , envLocation :: DocumentLocation , envDocXmlPath :: FilePath , envTextWidth :: Int@@ -233,6 +234,8 @@ type ParStyleMap = M.Map ParaStyleId ParStyle +type TableStyleMap = M.Map TableStyleId TableStyle+ data Numbering = Numbering NameSpaces [Numb] [AbstractNumb] deriving Show @@ -246,8 +249,8 @@ data AbstractNumb = AbstractNumb T.Text [Level] deriving Show --- ilvl format string start-data Level = Level T.Text T.Text T.Text (Maybe Integer)+-- ilvl format string start left indent+data Level = Level T.Text T.Text T.Text (Maybe Integer) (Maybe Integer) deriving Show data DocumentLocation = InDocument | InFootnote | InEndnote@@ -382,6 +385,7 @@ , isItalicCTL = isItalicCTL a <|> isItalicCTL b , isSmallCaps = isSmallCaps a <|> isSmallCaps b , isStrike = isStrike a <|> isStrike b+ , isDoubleStrike = isDoubleStrike a <|> isDoubleStrike b , isRTL = isRTL a <|> isRTL b , isForceCTL = isForceCTL a <|> isForceCTL b , rHighlight = rHighlight a <|> rHighlight b@@ -398,8 +402,8 @@ | CommentStart CommentId Author (Maybe CommentDate) [BodyPart] | CommentEnd CommentId | BookMark BookMarkId Anchor- | InternalHyperLink Anchor [ParPart]- | ExternalHyperLink URL [ParPart]+ | InternalHyperLink Anchor T.Text [ParPart] -- tooltip+ | ExternalHyperLink URL T.Text [ParPart] -- tooltip | Drawing FilePath T.Text T.Text B.ByteString Extent -- title, alt | Chart -- placeholder for now | Diagram -- placeholder for now@@ -443,7 +447,7 @@ numbering = archiveToNumbering archive rels = archiveToRelationships archive docXmlPath media = filteredFilesFromArchive archive filePathIsMedia- (styles, parstyles) = archiveToStyles archive+ (styles, parstyles, tabstyles) = archiveToStyles archive textWidth = archiveToTextWidth archive rEnv = ReaderEnv { envNotes = notes , envComments = comments@@ -453,6 +457,7 @@ , envFont = Nothing , envCharStyles = styles , envParStyles = parstyles+ , envTableStyles = tabstyles , envLocation = InDocument , envDocXmlPath = docXmlPath , envTextWidth = fromMaybe 9360 textWidth@@ -512,8 +517,8 @@ | otherwise = ((:[]) <$> elemToBodyPart ns' el) `catchError` (\_ -> return []) -archiveToStyles :: Archive -> (CharStyleMap, ParStyleMap)-archiveToStyles = archiveToStyles' getStyleId getStyleId+archiveToStyles :: Archive -> (CharStyleMap, ParStyleMap, TableStyleMap)+archiveToStyles = archiveToStyles' getStyleId getStyleId getStyleId class HasParentStyle a where getParentStyle :: a -> Maybe a@@ -615,9 +620,9 @@ case lvlOverride of Just (LevelOverride _ _ (Just lvl')) -> Just lvl' Just (LevelOverride _ (Just strt) _) ->- lookup ilvl $ map (\(Level i fmt s _) -> (i, Level i fmt s (Just strt))) lvls+ lookup ilvl $ map (\(Level i fmt s _ ind) -> (i, Level i fmt s (Just strt) ind)) lvls _ ->- lookup ilvl $ map (\l@(Level i _ _ _) -> (i, l)) lvls+ lookup ilvl $ map (\l@(Level i _ _ _ _) -> (i, l)) lvls -- | Given a @numId@ and level, return the abstract numbering id (the stable -- identifier shared by all numbering instances based on the same abstract@@ -685,7 +690,8 @@ let start = findChildByName ns "w" "start" element >>= findAttrByName ns "w" "val" >>= stringToInteger- return (Level ilvl fmt txt start)+ let ind = getIndentation ns element >>= leftParIndent+ return (Level ilvl fmt txt start ind) levelElemToLevel _ _ = Nothing archiveToNumbering' :: Archive -> Maybe Numbering@@ -698,7 +704,23 @@ numElems = findChildrenByName namespaces "w" "num" numberingElem absNumElems = findChildrenByName namespaces "w" "abstractNum" numberingElem nums = mapMaybe (numElemToNum namespaces) numElems- absNums = mapMaybe (absNumElemToAbsNum namespaces) absNumElems+ parsed = mapMaybe (\el -> (,) el <$> absNumElemToAbsNum namespaces el)+ absNumElems+ linkVal name el = findChildByName namespaces "w" name el+ >>= findAttrByName namespaces "w" "val"+ -- An abstractNum with w:numStyleLink has no levels of its own: they+ -- are in the abstractNum whose w:styleLink names the same numbering+ -- style (#9222).+ levelsByStyle = M.fromList+ [ (s, lvls) | (el, AbstractNumb _ lvls) <- parsed+ , Just s <- [linkVal "styleLink" el] ]+ resolve (el, absNum@(AbstractNumb absNumId lvls))+ | null lvls+ , Just lvls' <- linkVal "numStyleLink" el+ >>= (`M.lookup` levelsByStyle)+ = AbstractNumb absNumId lvls'+ | otherwise = absNum+ absNums = map resolve parsed return $ Numbering namespaces nums absNums archiveToNumbering :: Archive -> Numbering@@ -758,9 +780,13 @@ case sum xs of tot | tot > 1.0 -> map (/ tot) xs _ -> xs- normalizeFractions <$>- mapD (\e -> maybeToD (findAttrByName ns "w" "w" e >>=- fmap toFraction . safeRead)) cols+ -- w:w is optional. If a column has no width, we keep it but leave+ -- every column's width unspecified (0), since writers don't handle+ -- a mix of specified and unspecified widths well.+ return $+ case mapM (findAttrByName ns "w" "w" >=> safeRead) cols of+ Just widths -> normalizeFractions (map toFraction widths)+ Nothing -> replicate (length cols) 0 elemToTblGrid _ _ = throwError WrongElem elemToTblLook :: NameSpaces -> Element -> D TblLook@@ -1104,18 +1130,28 @@ info <- eitherToD $ parseFieldInfo t modify $ \st -> st {stateFldCharState = FldCharContent info [] : ancestors} return []+ -- A field with no instruction (e.g. a dead cross-reference, or+ -- one whose w:instrText sits inside a tracked change):+ FldCharOpen : ancestors | fldCharType == "separate" -> do+ modify $ \st -> st {stateFldCharState = FldCharContent UnknownField [] : ancestors}+ return [] -- Some fields have no content, e.g. index XE: FldCharFieldInfo t : ancestors | fldCharType == "end" -> do modify $ \st -> st {stateFldCharState = ancestors} info <- eitherToD $ parseFieldInfo t return [Field info []]- [FldCharContent info children] | fldCharType == "end" -> do- modify $ \st -> st {stateFldCharState = []}- return [Field info $ reverse children]+ FldCharOpen : ancestors | fldCharType == "end" -> do+ modify $ \st -> st {stateFldCharState = ancestors}+ return [Field UnknownField []] FldCharContent info children : FldCharContent parentInfo siblings : ancestors | fldCharType == "end" -> let parent = FldCharContent parentInfo $ (Field info (reverse children)) : siblings in do modify $ \st -> st {stateFldCharState = parent : ancestors} return []+ -- Always pop the field, even if the enclosing field is not+ -- (yet) in its content, so that the state cannot get stuck:+ FldCharContent info children : ancestors | fldCharType == "end" -> do+ modify $ \st -> st {stateFldCharState = ancestors}+ return [Field info $ reverse children] _ -> throwError WrongElem elemToParPart ns element | isElem ns "w" "r" element@@ -1199,7 +1235,7 @@ (matchQName "drawingml" "chart" (Just "c") "chart") drawingElem = return [Chart] elemToParPart' ns element- | isElem ns "w" "r" element = do+ | isElem ns "w" "r" element || isElem ns "w" "customXml" element = do runs <- elemToRun ns element return $ map PlainRun runs elemToParPart' ns element@@ -1207,6 +1243,12 @@ runs <- mconcat <$> mapD (elemToParPart' ns) (elChildren element) return [ChangedRuns change runs] elemToParPart' ns element+ | isElem ns "w" "fldSimple" element+ , Just instr <- findAttrByName ns "w" "instr" element = do+ info <- eitherToD $ parseFieldInfo instr+ children <- mconcat <$> mapD (elemToParPart ns) (elChildren element)+ return [Field info children]+elemToParPart' ns element | isElem ns "w" "bookmarkStart" element , Just bmId <- findAttrByName ns "w" "id" element , Just bmName <- findAttrByName ns "w" "name" element =@@ -1217,24 +1259,26 @@ location <- asks envLocation children <- mconcat <$> mapD (elemToParPart ns) (elChildren element) rels <- asks envRelationships+ let tooltip = fromMaybe "" $ findAttrByName ns "w" "tooltip" element case lookupRelationship location relId rels of Just target -> case findAttrByName ns "w" "anchor" element of Just anchor -> return- [ExternalHyperLink (target <> "#" <> anchor) children]- Nothing -> return [ExternalHyperLink target children]- Nothing -> return [ExternalHyperLink "" children]+ [ExternalHyperLink (target <> "#" <> anchor) tooltip children]+ Nothing -> return [ExternalHyperLink target tooltip children]+ Nothing -> return [ExternalHyperLink "" tooltip children] elemToParPart' ns element | isElem ns "w" "hyperlink" element , Just anchor <- findAttrByName ns "w" "anchor" element = do children <- mconcat <$> mapD (elemToParPart ns) (elChildren element)- return [InternalHyperLink anchor children]+ let tooltip = fromMaybe "" $ findAttrByName ns "w" "tooltip" element+ return [InternalHyperLink anchor tooltip children] elemToParPart' ns element | isElem ns "w" "commentRangeStart" element , Just cmtId <- findAttrByName ns "w" "id" element = do- (Comments _ commentMap) <- asks envComments+ (Comments cmtNs commentMap) <- asks envComments case M.lookup cmtId commentMap of- Just cmtElem -> elemToCommentStart ns cmtElem+ Just cmtElem -> elemToCommentStart cmtNs cmtElem Nothing -> throwError WrongElem elemToParPart' ns element | isElem ns "w" "commentRangeEnd" element@@ -1303,17 +1347,17 @@ childElemToRun ns element | isElem ns "w" "footnoteReference" element , Just fnId <- findAttrByName ns "w" "id" element = do- notes <- asks envNotes+ notes@(Notes notesNs _ _) <- asks envNotes case lookupFootnote fnId notes of- Just e -> do bps <- local (\r -> r {envLocation=InFootnote}) $ mapD (elemToBodyPart ns) (elChildren e)+ Just e -> do bps <- local (\r -> r {envLocation=InFootnote}) $ mapD (elemToBodyPart notesNs) (elChildren e) return [Footnote bps] Nothing -> return [Footnote []] childElemToRun ns element | isElem ns "w" "endnoteReference" element , Just enId <- findAttrByName ns "w" "id" element = do- notes <- asks envNotes+ notes@(Notes notesNs _ _) <- asks envNotes case lookupEndnote enId notes of- Just e -> do bps <- local (\r -> r {envLocation=InEndnote}) $ mapD (elemToBodyPart ns) (elChildren e)+ Just e -> do bps <- local (\r -> r {envLocation=InEndnote}) $ mapD (elemToBodyPart notesNs) (elChildren e) return [Endnote bps] Nothing -> return [Endnote []] childElemToRun _ _ = throwError WrongElem@@ -1336,6 +1380,9 @@ runElems <- elemToRunElems ns element runStyle <- elemToRunStyleD ns element return [Run runStyle runElems]+elemToRun ns element+ | isElem ns "w" "customXml" element =+ mconcat <$> mapM (elemToRun ns) (elChildren element) elemToRun _ _ = throwError WrongElem getParentStyleValue :: (ParStyle -> Maybe a) -> ParStyle -> Maybe a@@ -1504,10 +1551,16 @@ stripCaptionLabel :: [Element] -> [Element] stripCaptionLabel els =- if any isNumberElt els- then dropWhile (not . isNumberElt) els- else els+ if null afters+ then els+ else filter isBookmark befores <> dropNumber afters where+ (befores, afters) = break isNumberElt els+ isBookmark (Element name _ _ _) =+ qName name == "bookmarkStart" || qName name == "bookmarkEnd"+ -- A w:fldSimple carries the number as its result: drop it with the label.+ dropNumber (el:rest) | qName (elName el) == "fldSimple" = rest+ dropNumber rest = rest isNumberElt el@(Element name attribs _ _) = (qName name == "fldSimple" && case lookupAttrBy ((== "instr") . qName) attribs of
@@ -21,12 +21,15 @@ , CharStyle , ParaStyleId(..) , ParStyle(..)+ , TableStyleId(..)+ , TableStyle , ParIndentation(..) , RunStyle(..) , HasStyleName , StyleName , ParaStyleName , CharStyleName+ , TableStyleName , FromStyleName , VertAlign(..) , StyleId@@ -62,11 +65,15 @@ deriving (Show, Eq, Ord, IsString, FromStyleId) newtype ParaStyleId = ParaStyleId T.Text deriving (Show, Eq, Ord, IsString, FromStyleId)+newtype TableStyleId = TableStyleId T.Text+ deriving (Show, Eq, Ord, IsString, FromStyleId) newtype CharStyleName = CharStyleName CIString deriving (Show, Eq, Ord, IsString, FromStyleName) newtype ParaStyleName = ParaStyleName CIString deriving (Show, Eq, Ord, IsString, FromStyleName)+newtype TableStyleName = TableStyleName CIString+ deriving (Show, Eq, Ord, IsString, FromStyleName) -- Case-insensitive comparisons newtype CIString = CIString T.Text deriving (Show, IsString, FromStyleName)@@ -103,18 +110,19 @@ , cStyleData :: RunStyle } deriving (Show) -data RunStyle = RunStyle { isBold :: Maybe Bool- , isBoldCTL :: Maybe Bool- , isItalic :: Maybe Bool- , isItalicCTL :: Maybe Bool- , isSmallCaps :: Maybe Bool- , isStrike :: Maybe Bool- , isRTL :: Maybe Bool- , isForceCTL :: Maybe Bool- , rHighlight :: Maybe Text- , rVertAlign :: Maybe VertAlign- , rUnderline :: Maybe Text- , rParentStyle :: Maybe CharStyle+data RunStyle = RunStyle { isBold :: Maybe Bool+ , isBoldCTL :: Maybe Bool+ , isItalic :: Maybe Bool+ , isItalicCTL :: Maybe Bool+ , isSmallCaps :: Maybe Bool+ , isStrike :: Maybe Bool+ , isDoubleStrike :: Maybe Bool+ , isRTL :: Maybe Bool+ , isForceCTL :: Maybe Bool+ , rHighlight :: Maybe Text+ , rVertAlign :: Maybe VertAlign+ , rUnderline :: Maybe Text+ , rParentStyle :: Maybe CharStyle } deriving Show @@ -132,6 +140,10 @@ } deriving Show +data TableStyle = TableStyle { tStyleName :: TableStyleName+ , tStyleId :: TableStyleId }+ deriving Show+ defaultRunStyle :: RunStyle defaultRunStyle = RunStyle { isBold = Nothing , isBoldCTL = Nothing@@ -139,6 +151,7 @@ , isItalicCTL = Nothing , isSmallCaps = Nothing , isStrike = Nothing+ , isDoubleStrike = Nothing , isRTL = Nothing , isForceCTL = Nothing , rHighlight = Nothing@@ -148,21 +161,24 @@ } archiveToStyles'- :: (Ord k1, Ord k2, ElemToStyle a1, ElemToStyle a2)- => (a1 -> k1) -> (a2 -> k2) -> Archive -> (M.Map k1 a1, M.Map k2 a2)-archiveToStyles' conv1 conv2 zf =+ :: (Ord k1, Ord k2, Ord k3, ElemToStyle a1, ElemToStyle a2, ElemToStyle a3)+ => (a1 -> k1) -> (a2 -> k2) -> (a3 -> k3)+ -> Archive -> (M.Map k1 a1, M.Map k2 a2, M.Map k3 a3)+archiveToStyles' conv1 conv2 conv3 zf = case findEntryByPath "word/styles.xml" zf of- Nothing -> (M.empty, M.empty)+ Nothing -> (M.empty, M.empty, M.empty) Just entry -> case parseXMLElement . UTF8.toTextLazy . fromEntry $ entry of- Left _ -> (M.empty, M.empty)+ Left _ -> (M.empty, M.empty, M.empty) Right styElem -> let namespaces = elemToNameSpaces styElem in ( M.fromList $ map (\r -> (conv1 r, r)) $ buildBasedOnList namespaces styElem Nothing, M.fromList $ map (\p -> (conv2 p, p)) $- buildBasedOnList namespaces styElem Nothing)+ buildBasedOnList namespaces styElem Nothing,+ M.fromList $ map (\p -> (conv3 p, p)) $+ buildBasedOnList namespaces styElem Nothing ) isBasedOnStyle :: (ElemToStyle a, FromStyleId (StyleId a)) => NameSpaces -> Element -> Maybe a -> Bool isBasedOnStyle ns element parentStyle@@ -223,6 +239,22 @@ type StyleName ParStyle = ParaStyleName getStyleName = pStyleName +instance ElemToStyle TableStyle where+ cStyleType _ = "table"+ elemToStyle ns element parentStyle+ | isElem ns "w" "style" element+ , Just "table" <- findAttrByName ns "w" "type" element+ = elemToTableStyleData ns element parentStyle+ | otherwise = Nothing++instance HasStyleId TableStyle where+ type StyleId TableStyle = TableStyleId+ getStyleId = tStyleId++instance HasStyleName TableStyle where+ type StyleName TableStyle = TableStyleName+ getStyleName = tStyleName+ getStyleChildren :: (ElemToStyle a) => NameSpaces -> Element -> Maybe a -> [a] getStyleChildren ns element parentStyle | isElem ns "w" "styles" element =@@ -275,6 +307,7 @@ , isItalicCTL = checkOnOff ns rPr (elemName ns "w" "iCs") , isSmallCaps = checkOnOff ns rPr (elemName ns "w" "smallCaps") , isStrike = checkOnOff ns rPr (elemName ns "w" "strike")+ , isDoubleStrike = checkOnOff ns rPr (elemName ns "w" "dstrike") , isRTL = checkOnOff ns rPr (elemName ns "w" "rtl") , isForceCTL = checkOnOff ns rPr (elemName ns "w" "cs") , rHighlight =@@ -350,3 +383,14 @@ , pStyleId = ParaStyleId styleId } elemToParStyleData _ _ _ = Nothing++elemToTableStyleData :: NameSpaces+ -> Element -> Maybe TableStyle -> Maybe TableStyle+elemToTableStyleData ns element _parentStyle+ | Just styleId <- findAttrByName ns "w" "styleId" element+ , Just styleName <- getElementStyleName ns element+ = Just $ TableStyle+ { tStyleName = styleName+ , tStyleId = TableStyleId styleId+ }+elemToTableStyleData _ _ _ = Nothing
@@ -28,7 +28,7 @@ import Text.Pandoc.Definition import Text.Pandoc.Options import Text.Pandoc.Parsing hiding (enclosed)-import Text.Pandoc.Shared (trim, stringify, tshow)+import Text.Pandoc.Shared (trim, stringifyInlines, tshow, compactifyTable) import Data.List (isPrefixOf, isSuffixOf, groupBy) import qualified Safe @@ -174,10 +174,11 @@ => DWParser m a -> DWParser m Text nestedText end = innerSpace <|> countChar 1 nonspaceChar where- innerSpace = try $ many1Char spaceChar <* notFollowedBy end+ innerSpace = try $ takeWhile1P (\c -> c == ' ' || c == '\t')+ <* notFollowedBy end monospaced :: PandocMonad m => DWParser m B.Inlines-monospaced = try $ B.code . (T.concat . map stringify . B.toList) <$> enclosed (string "''") nestedInlines+monospaced = try $ B.code . stringifyInlines <$> enclosed (string "''") nestedInlines subscript :: PandocMonad m => DWParser m B.Inlines subscript = try $ B.subscript <$> between (string "<sub>") (try $ string "</sub>") nestedInlines@@ -238,7 +239,7 @@ nocache = try $ mempty <$ string "~~NOCACHE~~" str :: PandocMonad m => DWParser m B.Inlines-str = B.str <$> (many1Char alphaNum <|> characterReference)+str = B.str <$> (takeWhile1P isAlphaNum <|> characterReference) symbol :: PandocMonad m => DWParser m B.Inlines symbol = B.str <$> (notFollowedBy' blockCode *> countChar 1 nonspaceChar)@@ -517,7 +518,8 @@ let attrs = map (\(a, _) -> (a, ColWidthDefault)) firstRow let toRow = Row nullAttr . map B.simpleCell toHeaderRow l = [toRow l | not (null l)]- pure $ B.table B.emptyCaption+ pure $ compactifyTable+ $ B.table B.emptyCaption attrs (TableHead nullAttr $ toHeaderRow (map snd headerRow)) [TableBody nullAttr 0 [] $ map (toRow . (map snd)) body]
@@ -58,13 +58,15 @@ import Text.Pandoc.Logging import Text.Pandoc.Options ( Extension (Ext_epub_html_exts, Ext_empty_paragraphs, Ext_native_divs,- Ext_native_spans, Ext_raw_html, Ext_line_blocks, Ext_raw_tex),+ Ext_native_spans, Ext_raw_html, Ext_line_blocks, Ext_raw_tex,+ Ext_smart, Ext_tex_math_dollars,+ Ext_tex_math_single_backslash, Ext_tex_math_double_backslash), ReaderOptions (readerExtensions, readerStripComments), extensionEnabled) import Text.Pandoc.Parsing hiding ((<|>)) import Text.Pandoc.Shared ( addMetaField, extractSpaces, htmlSpanLikeElements, renderTags',- safeRead, tshow, formatCode)+ safeRead, tshow, formatCode, plainToPara) import Text.Pandoc.URI (escapeURI) import Text.Pandoc.Walk import Text.TeXMath (readMathML, writeTeX)@@ -76,7 +78,17 @@ => ReaderOptions -- ^ Reader options -> a -- ^ Input to parse -> m Pandoc-readHtml opts inp = do+readHtml = readHtmlWithDepth 0++-- Like 'readHtml', but starting at the given iframe nesting depth.+-- Used to limit recursion when the contents of iframes are fetched+-- and parsed (see pIframe).+readHtmlWithDepth :: (PandocMonad m, ToSources a)+ => Int+ -> ReaderOptions+ -> a+ -> m Pandoc+readHtmlWithDepth depth opts inp = do let tags = stripPrefixes $ canonicalizeTags $ parseTagsOptions parseOptions{ optTagPosition = True } (sourcesToText $ toSources inp)@@ -92,7 +104,7 @@ result <- flip runReaderT def $ runParserT parseDoc (HTMLState def{ stateOptions = opts }- [] Nothing Set.empty [] M.empty opts False False)+ M.empty M.empty Nothing Set.empty [] M.empty opts False False depth) "source" tags case result of Right doc -> return doc@@ -127,12 +139,14 @@ walkM (replaceNotes' notes) bs replaceNotes' :: PandocMonad m- => [(Text, Blocks)] -> Inline -> TagParser m Inline+ => M.Map Text Blocks -> Inline -> TagParser m Inline replaceNotes' noteTbl (RawInline (Format "noteref") ref) =- maybe warnNotFound (pure . Note . B.toList) $ lookup ref noteTbl+ maybe warnNotFound (pure . Note . B.toList) $ M.lookup ref noteTbl where warnNotFound = do- pos <- getPosition+ -- use the position of the noteref, if we recorded one; the+ -- current position is at the end of the document by now:+ pos <- M.lookup ref . noteRefPos <$> getState >>= maybe getPosition pure logMessage $ ReferenceNotFound ref pos pure (Note []) replaceNotes' _ x = pure x@@ -296,9 +310,9 @@ _ -> False) let attr = toStringAttr attr' let ident = fromMaybe "" (lookup "id" attr)- content <- pInTags tag block+ content <- local (\s -> s{ inNoteBody = True }) $ pInTags tag block updateState $ \s ->- s {noteTable = (ident, content) : noteTable s}+ s {noteTable = M.insert ident content (noteTable s)} eFootnotes :: PandocMonad m => TagParser m Blocks eFootnotes = try $ do@@ -331,6 +345,9 @@ ident <- case lookup "href" attr >>= T.uncons of Just ('#', rest) -> return rest _ -> mzero+ pos <- getPosition+ updateState $ \s ->+ s{ noteRefPos = M.insertWith (\_new old -> old) ident pos (noteRefPos s) } _ <- manyTill pAny (pSatisfy (\case TagClose t -> t == tag _ -> False))@@ -377,8 +394,9 @@ pCheckbox :: PandocMonad m => TagParser m Inlines pCheckbox = do- TagOpen _ attr' <- pSatisfy $ matchTagOpen "input" [("type","checkbox")]- TagClose _ <- pSatisfy (matchTagClose "input")+ -- <input> is a void element, so the closing tag is optional+ TagOpen _ attr' <- pSelfClosing (=="input")+ (\as -> lookup "type" as == Just "checkbox") let attr = toStringAttr attr' let isChecked = isJust $ lookup "checked" attr let escapeSequence = B.str $ if isChecked then "\9746" else "\9744"@@ -404,15 +422,23 @@ pOrderedList :: PandocMonad m => TagParser m Blocks pOrderedList = try $ do TagOpen _ attribs' <- pSatisfy (matchTagOpen "ol" [])- isNoteList <- inFootnotes <$> getState+ -- an <ol> inside the body of a note is an ordered list, not the+ -- list of notes (#11852):+ inNotes <- inFootnotes <$> getState+ inBody <- asks inNoteBody+ let isNoteList = inNotes && not inBody let attribs = toStringAttr attribs' let start = fromMaybe 1 $ lookup "start" attribs >>= safeRead let style = fromMaybe DefaultStyle $ (parseTypeAttr <$> lookup "type" attribs)- <|> (parseListStyleType <$> lookup "class" attribs)+ <|> (lookup "class" attribs >>= pickClassStyle) <|> (parseListStyleType <$> (lookup "style" attribs >>= pickListStyle)) where pickListStyle = pickStyleAttrProps ["list-style-type", "list-style"]+ -- the list style may be one of several words in the class+ -- attribute:+ pickClassStyle = L.find (/= DefaultStyle)+ . map parseListStyleType . T.words -- note: if they have an <ol> or <ul> not in scope of a <li>, -- treat it as a list item, though it's not valid xhtml...@@ -463,8 +489,6 @@ isParaish OrderedList{} = not inList isParaish DefinitionList{} = not inList isParaish _ = False- plainToPara (Plain xs) = Para xs- plainToPara x = x bs' = B.toList bs pRawTag :: PandocMonad m => TagParser m Text@@ -520,7 +544,10 @@ skipMany pBlank pCloses "iframe" <|> eof url <- canonicalizeUrl $ fromAttrib "src" tag- if T.null url+ depth <- iframeDepth <$> getState+ -- limit nesting depth, since iframes that (indirectly) embed+ -- themselves would otherwise cause infinite recursion:+ if T.null url || depth >= maxIframeDepth then ignore $ renderTags' [tag, TagClose "iframe"] else catchError (do (bs, mbMime) <- openURL url@@ -529,7 +556,7 @@ | "text/html" `T.isPrefixOf` mt -> do let inp = UTF8.toText bs opts <- readerOpts <$> getState- Pandoc _ contents <- readHtml opts inp+ Pandoc _ contents <- readHtmlWithDepth (depth + 1) opts inp return $ B.divWith ("",["iframe"],[]) $ B.fromList contents | "image/" `T.isPrefixOf` mt -> do return $ B.divWith ("",["iframe"],[]) $@@ -539,6 +566,9 @@ logMessage $ CouldNotFetchResource url (renderError e) ignore $ renderTags' [tag, TagClose "iframe"]) +maxIframeDepth :: Int+maxIframeDepth = 5+ pRawHtmlBlock :: PandocMonad m => TagParser m Blocks pRawHtmlBlock = do raw <- pHtmlBlock "script" <|> pHtmlBlock "style" <|> pHtmlBlock "textarea"@@ -611,7 +641,8 @@ pHrule = do pSelfClosing (=="hr") (const True) inNotes <- inFootnotes <$> getState- return $ if inNotes+ inBody <- asks inNoteBody+ return $ if inNotes && not inBody then mempty else B.horizontalRule @@ -664,7 +695,9 @@ let modifyClasses f ("class",v) = ("class", T.unwords . map f . T.words $ v) modifyClasses _ (k,v) = (k,v)- let attr = toAttr $ map (modifyClasses stripLanguagePrefix) $ codeAttr <> attr'+ -- pre's attributes take precedence (toAttr keeps the first of+ -- duplicate attributes):+ let attr = toAttr $ map (modifyClasses stripLanguagePrefix) $ attr' <> codeAttr contents <- manyTill pAny (pCloses "pre" <|> eof) let rawText = T.concat $ map tagToText contents -- drop trailing newline if any@@ -720,11 +753,14 @@ "input" | lookup "type" attr == Just "checkbox" -> asks inListItem >>= guard >> pCheckbox- "style" -> B.rawInline "html" <$> pHtmlBlock "style"+ "style"+ | extensionEnabled Ext_raw_html exts+ -> B.rawInline "html" <$> pHtmlBlock "style"+ | otherwise -> pHtmlBlock "style" >>= ignore "script" | Just x <- lookup "type" attr , "math/tex" `T.isPrefixOf` x -> pScriptMath- _ | name `elem` htmlSpanLikeElements -> pSpanLike+ _ | name `Set.member` htmlSpanLikeElements -> pSpanLike name _ -> pRawHtmlInline TagText _ -> pTagText _ -> pRawHtmlInline@@ -770,18 +806,12 @@ pSubscript :: PandocMonad m => TagParser m Inlines pSubscript = pInlinesInTags "sub" B.subscript -pSpanLike :: PandocMonad m => TagParser m Inlines-pSpanLike =- Set.foldr- (\tagName acc -> acc <|> parseTag tagName)- mzero- htmlSpanLikeElements- where- parseTag tagName = do- TagOpen _ attrs <- pSatisfy $ tagOpenLit tagName (const True)- let (ids, cs, kvs) = toAttr attrs- content <- mconcat <$> manyTill inline (pCloses tagName <|> eof)- return $ B.spanWith (ids, tagName : cs, kvs) content+pSpanLike :: PandocMonad m => Text -> TagParser m Inlines+pSpanLike tagName = do+ TagOpen _ attrs <- pSatisfy $ tagOpenLit tagName (const True)+ let (ids, cs, kvs) = toAttr attrs+ content <- mconcat <$> manyTill inline (pCloses tagName <|> eof)+ return $ B.spanWith (ids, tagName : cs, kvs) content pSmall :: PandocMonad m => TagParser m Inlines pSmall = pInlinesInTags "small" (B.spanWith ("",["small"],[]))@@ -973,23 +1003,43 @@ pos <- getPosition (TagText str) <- pSatisfy isTagText st <- getState- qu <- ask- parsed <- lift $ lift $- flip runReaderT qu $ runParserT (many pTagContents) st "text"- (Sources [(pos, str)])- case parsed of- Left _ -> throwError $ PandocParseError $- "Could not parse `" <> str <> "'"- Right result -> return $ mconcat result+ exts <- getOption readerExtensions+ let smart = extensionEnabled Ext_smart exts+ let mathDollars = extensionEnabled Ext_tex_math_dollars exts+ let backslashSpecial = extensionEnabled Ext_raw_tex exts ||+ extensionEnabled Ext_tex_math_single_backslash exts ||+ extensionEnabled Ext_tex_math_double_backslash exts+ -- Chars that could make pTagContents yield something different from+ -- a plain B.text decomposition into Str, Space, and SoftBreak:+ let needsParsing c =+ isBad c || -- gets remapped (see pBad)+ (c == '$' && mathDollars) || -- may start tex math+ (c == '\\' && backslashSpecial) || -- raw tex or tex math+ (smart && (c == '\'' || c == '"' || c == '.' || c == '-'))+ if not (inPre st) && not (T.any needsParsing str)+ -- fast path: the result is a plain decomposition of the text into+ -- Str, Space, and SoftBreak, which is exactly what B.text gives us+ -- (adjacent Strs produced by pTagContents are merged by the+ -- Inlines Semigroup instance, so the results coincide):+ then return $ B.text str+ else do+ qu <- ask+ parsed <- lift $ lift $+ flip runReaderT qu $ runParserT (many pTagContents) st "text"+ (Sources [(pos, str)])+ case parsed of+ Left _ -> throwError $ PandocParseError $+ "Could not parse `" <> str <> "'"+ Right result -> return $ mconcat result type InlinesParser m = HTMLParser m Sources pTagContents :: PandocMonad m => InlinesParser m Inlines pTagContents =- B.displayMath <$> mathDisplay+ pStr -- can't consume the special chars that start the other+ <|> pSpace -- parsers, so it is safe to try these two first+ <|> B.displayMath <$> mathDisplay <|> B.math <$> mathInline- <|> pStr- <|> pSpace <|> smartPunctuation pTagContents <|> pRawTeX <|> pSymbol@@ -1184,12 +1234,19 @@ let ts = canonicalizeTags $ parseTagsOptions parseOptions{ optTagWarning = False , optTagPosition = True }- (inp <> " ")- -- add space to ensure that- -- we get a TagPosition after the tag+ inp+ -- if the tag is the last token, there is no TagPosition after it;+ -- in that case its end is the end of the input (positions are 1-based).+ -- (Note: only computed when needed, to avoid a scan of the input.)+ let endOfInput = (T.count "\n" inp + 1,+ T.length (T.takeWhileEnd (/= '\n') inp) + 1) (next, ln, col) <- case ts of- (TagPosition{} : next : TagPosition ln col : _)- | f next -> return (next, ln, col)+ (TagPosition{} : next : rest)+ | f next ->+ case dropWhile (not . isTagPosition) rest of+ TagPosition ln col : _ -> return (next, ln, col)+ _ -> let (ln, col) = endOfInput+ in return (next, ln, col) _ -> mzero -- <www.boe.es/buscar/act.php?id=BOE-A-1996-8930#a66>
@@ -1,3 +1,4 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {- |@@ -34,12 +35,15 @@ import Data.Maybe (fromMaybe) import Data.Text (Text) import Text.HTML.TagSoup- ( Attribute, Tag (..), isTagPosition, isTagOpen, isTagClose, (~==) )+ ( Attribute, Tag (..), isTagOpen, isTagClose, (~==) ) import Text.Pandoc.Class.PandocMonad (PandocMonad (..)) import Text.Pandoc.Definition (Attr) import Text.Pandoc.Parsing- ( (<|>), eof, getPosition, lookAhead, manyTill, newPos, option, optional- , skipMany, setPosition, token, try)+ ( (<|>), eof, lookAhead, manyTill, newPos, option, optional+ , skipMany, try)+import Text.Parsec.Prim (mkPT, Consumed (..), Reply (..), State (..))+import Text.Parsec.Error (Message (SysUnExpect), newErrorMessage,+ newErrorUnknown) import Text.Pandoc.Readers.HTML.TagCategories import Text.Pandoc.Readers.HTML.Types import Text.Pandoc.Shared (tshow)@@ -126,18 +130,24 @@ isBlank (TagComment _) = True isBlank _ = False -pLocation :: PandocMonad m => TagParser m ()-pLocation = do- (TagPosition r c) <- pSat isTagPosition- setPosition $ newPos "input" r c--pSat :: PandocMonad m => (Tag Text -> Bool) -> TagParser m (Tag Text)-pSat f = do- pos <- getPosition- token tshow (const pos) (\x -> if f x then Just x else Nothing)-+-- | Skip any 'TagPosition' tokens (using them to update the source+-- position), then consume the next tag if it satisfies the predicate.+-- Fails without consuming input otherwise. Implemented as a single+-- parsec primitive, since this is the hottest spot of the HTML reader. pSatisfy :: PandocMonad m => (Tag Text -> Bool) -> TagParser m (Tag Text)-pSatisfy f = try $ optional pLocation >> pSat f+pSatisfy f = mkPT $ \(State inp pos u) ->+ let go !pos' toks =+ case toks of+ TagPosition r c : rest -> go (newPos "input" r c) rest+ t : rest+ | f t -> Consumed (return+ (Ok t (State rest pos' u) (newErrorUnknown pos')))+ _ -> Empty (return (Error+ (newErrorMessage (SysUnExpect (descr toks)) pos')))+ in return (go pos inp)+ where+ descr [] = ""+ descr (t:_) = T.unpack (tshow t) matchTagClose :: Text -> (Tag Text -> Bool) matchTagClose t = (~== TagClose t)@@ -199,20 +209,23 @@ _ `closes` _ = False toStringAttr :: [(Text, Text)] -> [(Text, Text)]-toStringAttr = foldr go []+toStringAttr = go mempty where- go :: (Text, Text) -> [(Text, Text)] -> [(Text, Text)]+ go :: Set.Set Text -> [(Text, Text)] -> [(Text, Text)]+ go _ [] = []+ go seen ((x,y):rest)+ -- prevent duplicate attributes; the first one wins:+ | x' `Set.member` seen = go seen rest+ | otherwise = (x', y) : go (Set.insert x' seen) rest+ where x' = normalizeName x -- treat xml:lang as lang- go ("xml:lang",y) ats = go ("lang",y) ats- -- prevent duplicate attributes- go (x,y) ats- | any (\(x',_) -> x == x') ats = ats- | otherwise =- case T.stripPrefix "data-" x of- Just x' | x' `Set.notMember` (html5Attributes <>- html4Attributes <> rdfaAttributes)- -> go (x',y) ats- _ -> (x,y):ats+ normalizeName "xml:lang" = "lang"+ -- strip data- prefix unless the bare name is a standard attribute+ normalizeName x =+ case T.stripPrefix "data-" x of+ Just x' | x' `Set.notMember` standardAttrs -> x'+ _ -> x+ standardAttrs = html5Attributes <> html4Attributes <> rdfaAttributes -- Unlike fromAttrib from tagsoup, this distinguishes -- between a missing attribute and an attribute with empty content.
@@ -28,7 +28,7 @@ import Text.Pandoc.Definition import Text.Pandoc.Class.PandocMonad (PandocMonad (..)) import Text.Pandoc.Parsing- ( eof, lookAhead, many, many1, manyTill, option, optional+ ( eof, lookAhead, many, many1, manyTill, notFollowedBy, option, optional , optionMaybe, skipMany, try ) import Text.Pandoc.Readers.HTML.Parsing import Text.Pandoc.Readers.HTML.Types (TagParser)@@ -134,7 +134,8 @@ skipMany pBlank TagOpen _ attribs <- pSatisfy (matchTagOpen "tr" []) <* skipMany pBlank cells <- many (pCell block BodyCell <|> pCell block HeaderCell)- TagClose _ <- pSatisfy (matchTagClose "tr")+ -- the closing tag may be omitted (it is optional in HTML):+ optional $ pSatisfy (matchTagClose "tr") let numheadcells = length $ takeWhile (\(ct,_) -> ct == HeaderCell) cells return (numheadcells, Row (toAttr attribs) $ map snd cells) @@ -145,9 +146,15 @@ -> TagParser m B.Row pHeaderRow block = try $ do skipMany pBlank- let pThs = many (snd <$> pCell block HeaderCell)- let mkRow (attribs, cells) = Row (toAttr attribs) cells- mkRow <$> pInTagWithAttribs TagsRequired "tr" pThs+ TagOpen _ attribs <- pSatisfy (matchTagOpen "tr" [])+ cells <- many (snd <$> pCell block HeaderCell)+ skipMany pBlank+ -- a header row may contain only <th> cells; a following <td>+ -- means this is a body row, so we backtrack and let pRow parse it:+ notFollowedBy $ pSatisfy (matchTagOpen "td" [])+ -- the closing tag may be omitted (it is optional in HTML):+ optional $ pSatisfy (matchTagClose "tr")+ return $ Row (toAttr attribs) cells -- | Parses a table head. If there is no @thead@ element, this looks for -- a row of @<th>@-only elements as the first line of the table.@@ -278,7 +285,9 @@ [] -> case tblType of SimpleTable -> replicate ncols ColWidthDefault NormalTable -> replicate ncols (ColWidth $ 1 / fromIntegral ncols)- widths -> widths+ -- pad if fewer <col> elements than columns, so that cells in the+ -- extra columns aren't lost (the colspecs are zipped with alignments):+ widths -> widths ++ replicate (ncols - length widths) ColWidthDefault calculateAlignments :: Int -> [TableBody] -> [Alignment] calculateAlignments cols tbodies =
@@ -33,7 +33,7 @@ import Text.Pandoc.Parsing ( HasIdentifierList (..), HasLastStrPosition (..), HasLogMessages (..) , HasMacros (..), HasQuoteContext (..), HasReaderOptions (..)- , ParsecT, ParserState, QuoteContext (NoQuote)+ , ParsecT, ParserState, QuoteContext (NoQuote), SourcePos ) import Text.Pandoc.TeX (Macro) @@ -46,7 +46,8 @@ -- | Global HTML parser state data HTMLState = HTMLState { parserState :: ParserState- , noteTable :: [(Text, Blocks)]+ , noteTable :: Map Text Blocks+ , noteRefPos :: Map Text SourcePos -- ^ position of first ref to each note , baseHref :: Maybe URI , identifiers :: Set Text , logMessages :: [LogMessage]@@ -54,6 +55,7 @@ , readerOpts :: ReaderOptions , inFootnotes :: Bool , inPre :: Bool+ , iframeDepth :: Int -- ^ how many iframes deep we are (recursion limit) } -- | Local HTML parser state@@ -62,6 +64,7 @@ , inChapter :: Bool -- ^ Set if in chapter section , inPlain :: Bool -- ^ Set if in pPlain , inListItem :: Bool -- ^ Set if in <li> tag+ , inNoteBody :: Bool -- ^ Set if parsing the content of a footnote } @@ -93,7 +96,7 @@ deleteMeta s st = st {parserState = deleteMeta s $ parserState st} instance Default HTMLLocal where- def = HTMLLocal NoQuote False False False+ def = HTMLLocal NoQuote False False False False instance HasLastStrPosition HTMLState where setLastStrPos s st = st {parserState = setLastStrPos s (parserState st)}
@@ -20,7 +20,7 @@ import Text.Pandoc.Builder hiding (cell) import Text.Pandoc.Error (PandocError (PandocParseError)) import Text.Pandoc.Options (ReaderOptions)-import Text.Pandoc.Shared (stringify)+import Text.Pandoc.Shared (stringifyInlines) import Text.Pandoc.Sources (ToSources(..), sourcesToText) import qualified Text.Jira.Markup as Jira @@ -140,7 +140,7 @@ in imageWith attr (Jira.fromURL url) title mempty Jira.Link lt alias url -> jiraLinkToPandoc lt alias url Jira.Linebreak -> linebreak- Jira.Monospaced inlns -> code . stringify . toList . fromInlines $ inlns+ Jira.Monospaced inlns -> code . stringifyInlines . toList . fromInlines $ inlns Jira.Space -> space Jira.SpecialChar c -> str (Data.Text.singleton c) Jira.Str t -> str t
@@ -24,6 +24,7 @@ import Control.Applicative (many, optional, (<|>)) import Control.Monad import Control.Monad.Except (throwError)+import Control.Monad.Reader (runReaderT) import Data.Containers.ListUtils (nubOrd) import Data.Char (isDigit, isLetter, isAlphaNum, toUpper, chr) import Data.Default@@ -87,12 +88,22 @@ -> m Pandoc readLaTeX opts ltx = do let sources = toSources ltx- parsed <- runParserT parseLaTeX def{ sOptions = opts } "source"+ parsed <- flip runReaderT latexEnv $+ runParserT parseLaTeX def{ sOptions = opts } "source" (TokStream False (tokenizeSources sources)) case parsed of Right result -> return result Left e -> throwError $ fromParsecError sources e +-- | The command dispatch tables, built once per parse and shared+-- through the reader environment (see 'LaTeXEnv').+latexEnv :: PandocMonad m => LaTeXEnv m+latexEnv = LaTeXEnv+ { envInlineCommands = inlineCommands+ , envBlockCommands = blockCommands+ , envEnvironments = environments+ }+ parseLaTeX :: PandocMonad m => LP m Pandoc parseLaTeX = do bs <- blocks@@ -127,13 +138,15 @@ -- | Resolve footnote marks (\footnotemark) to actual notes using -- the footnote texts collected from \footnotetext commands.-resolveFootnoteMarks :: M.Map Int Blocks -> Inline -> Inline+-- Marks inside the resolved note texts are resolved too (each text+-- is used at most once, so this terminates).+resolveFootnoteMarks :: M.Map Text Blocks -> Inline -> Inline resolveFootnoteMarks fnTexts (Span (_, classes, kvs) _) | "footnote-mark" `elem` classes- , Just numText <- lookup "note-num" kvs- , [(n, "")] <- reads (T.unpack numText)- = case M.lookup n fnTexts of- Just contents -> Note (toList contents)+ , Just key <- lookup "note-key" kvs+ = case M.lookup key fnTexts of+ Just contents -> Note $ walk (resolveFootnoteMarks (M.delete key fnTexts))+ $ toList contents Nothing -> Str "" -- No matching footnotetext found resolveFootnoteMarks _ x = x @@ -156,7 +169,7 @@ lookAhead (try (char '\\' >> letter)) toks <- getInputTokens snd <$> (- rawLaTeXParser toks+ rawLaTeXParser latexEnv toks (makeAtLetterSection <|> macroDef (const mempty) <|> do choice (map controlSeq@@ -164,7 +177,7 @@ skipMany opt braced return mempty) blocks- <|> rawLaTeXParser toks+ <|> rawLaTeXParser latexEnv toks (void (environment <|> blockCommand)) (mconcat <$> many (block <|> beginOrEndCommand))) @@ -199,10 +212,10 @@ lookAhead (try (char '\\' >> letter)) toks <- getInputTokens raw <- snd <$>- ( rawLaTeXParser toks+ ( rawLaTeXParser latexEnv toks (mempty <$ (controlSeq "input" >> skipMany rawopt >> braced)) inlines- <|> rawLaTeXParser toks (void inline) inlines+ <|> rawLaTeXParser latexEnv toks (void inline) inlines ) finalbraces <- mconcat <$> many (try (string "{}")) -- see #5439 return $ raw <> T.pack finalbraces@@ -211,7 +224,8 @@ inlineCommand = do lookAhead (try (char '\\' >> letter)) toks <- getInputTokens- fst <$> rawLaTeXParser toks (void (inlineEnvironment <|> inlineCommand'))+ fst <$> rawLaTeXParser latexEnv toks+ (void (inlineEnvironment <|> inlineCommand')) inlines -- inline elements:@@ -337,21 +351,23 @@ rawcommand <- getRawCommand name (cmd <> star) (guardEnabled Ext_raw_tex >> return (rawInline "latex" rawcommand)) <|> ignore rawcommand- lookupListDefault raw names inlineCommands+ commandMap <- envInlineCommands <$> askEnv+ lookupListDefault raw names commandMap tok :: PandocMonad m => LP m Inlines tok = tokWith inline unescapeURL :: Text -> Text-unescapeURL = T.concat . go . T.splitOn "\\"- where- isEscapable c = T.any (== c) "#$%&~_^\\{}"- go (x:xs) = x : map unescapeInterior xs- go [] = []- unescapeInterior t- | Just (c, _) <- T.uncons t- , isEscapable c = t- | otherwise = "\\" <> t+unescapeURL t =+ let (xs, ys) = T.break (== '\\') t+ in case T.uncons ys of+ Nothing -> xs+ Just (_, rest) ->+ case T.uncons rest of+ Just (c, rest')+ | isEscapable c -> xs <> T.cons c (unescapeURL rest')+ _ -> xs <> "\\" <> unescapeURL rest+ where isEscapable c = T.any (== c) "#$%&~_^\\{}" inlineCommands :: PandocMonad m => M.Map Text (LP m Inlines) inlineCommands = M.unions@@ -407,22 +423,32 @@ , ("sl", extractSpaces emph <$> inlines) , ("bf", extractSpaces strong <$> inlines) , ("tt", formatCode nullAttr <$> inlines)+ , ("ttfamily", extractSpaces (formatCode nullAttr) <$> inlines) , ("rm", inlines) , ("itshape", extractSpaces emph <$> inlines) , ("slshape", extractSpaces emph <$> inlines) , ("scshape", extractSpaces smallcaps <$> inlines) , ("bfseries", extractSpaces strong <$> inlines)- , ("MakeUppercase", makeUppercase <$> tok)- , ("MakeTextUppercase", makeUppercase <$> tok) -- textcase- , ("uppercase", makeUppercase <$> tok)- , ("MakeLowercase", makeLowercase <$> tok)- , ("MakeTextLowercase", makeLowercase <$> tok)- , ("lowercase", makeLowercase <$> tok)+ , ("MakeUppercase", caseTransform "upper" T.toUpper)+ , ("MakeTextUppercase", caseTransform "upper" T.toUpper) -- textcase+ , ("uppercase", caseTransform "upper" T.toUpper)+ , ("MakeLowercase", caseTransform "lower" T.toLower)+ , ("MakeTextLowercase", caseTransform "lower" T.toLower)+ , ("lowercase", caseTransform "lower" T.toLower)+ , ("MakeTitlecase", makeTitlecaseCommand)+ , ("NoCaseChange", spanWith ("",["nocasechange"],[]) <$> tok)+ , ("CaseSwitch", tok <* tok <* tok <* tok) , ("thanks", skipopts >> note <$> grouped block) , ("footnote", skipopts >> footnote) , ("footnotemark", footnotemark) , ("footnotetext", footnotetext)+ , ("pandocfootnotemark", pandocfootnotemark)+ , ("pandocfootnotetext", pandocfootnotetext) , ("newline", pure B.linebreak)+ -- xparse argument markers, in case they leak into the document:+ , ("NoValue", pure (B.str "-NoValue-"))+ , ("BooleanTrue", pure mempty)+ , ("BooleanFalse", pure mempty) , ("passthrough", fixPassthroughEscapes <$> tok) -- \passthrough macro used by latex writer -- for listings@@ -457,6 +483,7 @@ , ("iftoggle", try $ ifToggle >> inline) -- include , ("input", rawInlineOr "input" $ include "input")+ , ("expandableinput", rawInlineOr "expandableinput" $ include "input") -- soul package , ("st", extractSpaces strikeout <$> tok) , ("ul", underline <$> tok)@@ -471,6 +498,19 @@ -- this is used internally by pandoc but the definition is too complicated -- for pandoc to handle (see #11140): , ("pandocbounded", tok)+ -- LaTeX3 constants+ , ("c_ampersand_str", pure (str "&"))+ , ("c_atsign_str", pure (str "@"))+ , ("c_backslash_str", pure (str "\\"))+ , ("c_left_brace_str", pure (str "{"))+ , ("c_right_brace_str", pure (str "}"))+ , ("c_circumflex_str", pure (str "^"))+ , ("c_colon_str", pure (str ":"))+ , ("c_dollar_str", pure (str "$"))+ , ("c_hash_str", pure (str "#"))+ , ("c_percent_str", pure (str "%"))+ , ("c_tilde_str", pure (str "~"))+ , ("c_underscore_str", pure (str "_")) ] bracedFilename :: PandocMonad m => LP m Text@@ -502,7 +542,7 @@ Nothing -> do updateState $ \st -> st{ sLastNoteNum = sLastNoteNum st + 1 } sLastNoteNum <$> getState- return $ B.spanWith ("", ["footnote-mark"], [("note-num", tshow noteNum)]) mempty+ return $ footnoteMarkSpan (tshow noteNum) -- | Parse \footnotetext[n]{text}. If n is not given, use current counter. -- Stores the text in state to be resolved later.@@ -512,11 +552,51 @@ noteNum <- case mbNum of Just n -> return n Nothing -> sLastNoteNum <$> getState+ storeFootnoteText (tshow noteNum)++-- | Parse \pandocfootnotemark{id}, which is produced by the LaTeX writer+-- for notes in tables, captions, etc. It is paired with+-- \pandocfootnotetext{id}{text}.+pandocfootnotemark :: PandocMonad m => LP m Inlines+pandocfootnotemark = do+ key <- untokenize <$> braced+ updateState $ \st -> st{ sLastNoteNum = sLastNoteNum st + 1 }+ return $ footnoteMarkSpan ("pandoc:" <> key)++-- | Parse \pandocfootnotetext{id}{text}.+pandocfootnotetext :: PandocMonad m => LP m Inlines+pandocfootnotetext = do+ key <- untokenize <$> braced+ storeFootnoteText ("pandoc:" <> key)++-- | A placeholder for a footnote mark, to be resolved later+-- (see 'resolveFootnoteMarks').+footnoteMarkSpan :: Text -> Inlines+footnoteMarkSpan key = B.spanWith ("", ["footnote-mark"], [("note-key", key)]) mempty++storeFootnoteText :: PandocMonad m => Text -> LP m Inlines+storeFootnoteText key = do contents <- grouped block >>= walkM resolveNoteLabel updateState $ \st -> st{- sFootnoteTexts = M.insert noteNum contents (sFootnoteTexts st) }+ sFootnoteTexts = M.insert key contents (sFootnoteTexts st) } return mempty +-- | Parse \addtocounter{ctr}{n}. Adjustments to the footnote counter+-- are tracked, so that \footnotetext gets the right number;+-- otherwise the command is treated as raw LaTeX.+addtocounter :: PandocMonad m => LP m Blocks+addtocounter = do+ (ctrAndNum, toks) <- withRaw $ (,) <$> (untokenize <$> braced)+ <*> (untokenize <$> braced)+ case ctrAndNum of+ ("footnote", num) | Just n <- safeRead (T.strip num) -> do+ updateState $ \st -> st{ sLastNoteNum = sLastNoteNum st + n }+ return mempty+ _ -> do+ let raw = "\\addtocounter" <> untokenize toks+ (guardEnabled Ext_raw_tex >> return (rawBlock "latex" raw))+ <|> ignore raw+ -- | Parse optional footnote number argument [n] optionalFootnoteNum :: PandocMonad m => LP m (Maybe Int) optionalFootnoteNum = option Nothing $ do@@ -547,16 +627,98 @@ contents <- manyTill anyTok (controlSeq "fi") return $ rawInline "latex" $ "\\ifdim" <> untokenize contents <> "\\fi" -makeUppercase :: Inlines -> Inlines-makeUppercase = fromList . walk (alterStr T.toUpper) . toList+-- | Parse the argument of a case-changing command (\MakeUppercase,+-- \MakeLowercase) and apply the case transformation, honoring+-- exclusions declared with \Declare*caseExclusions.+caseTransform :: PandocMonad m => Text -> (Text -> Text) -> LP m Inlines+caseTransform kind f = do+ void $ option [] keyvals -- locale options, ignored+ excl <- M.findWithDefault Set.empty kind . sCaseExclusions <$> getState+ caseTransformWith excl f <$> tok -makeLowercase :: Inlines -> Inlines-makeLowercase = fromList . walk (alterStr T.toLower) . toList+-- | Apply a case transformation to text, leaving math, code and+-- citations untouched, unwrapping (and skipping) \NoCaseChange+-- content, and skipping excluded words.+caseTransformWith :: Set.Set Text -> (Text -> Text) -> Inlines -> Inlines+caseTransformWith excl f = fromList . go . toList+ where+ go = concatMap goInline+ goInline (Span ("",["nocasechange"],[]) ils) = ils+ goInline (Str t)+ | t `Set.member` excl = [Str t]+ | otherwise = [Str (f t)]+ goInline (Emph ils) = [Emph (go ils)]+ goInline (Strong ils) = [Strong (go ils)]+ goInline (Underline ils) = [Underline (go ils)]+ goInline (Strikeout ils) = [Strikeout (go ils)]+ goInline (Superscript ils) = [Superscript (go ils)]+ goInline (Subscript ils) = [Subscript (go ils)]+ goInline (SmallCaps ils) = [SmallCaps (go ils)]+ goInline (Quoted qt ils) = [Quoted qt (go ils)]+ goInline (Span attr ils) = [Span attr (go ils)]+ goInline (Link attr ils target) = [Link attr (go ils) target]+ goInline x = [x] -- Math, Code, Cite, Space, etc. -alterStr :: (Text -> Text) -> Inline -> Inline-alterStr f (Str xs) = Str (f xs)-alterStr _ x = x+-- | Parse the arguments of \MakeTitlecase, supporting the+-- @words=all@ option and title-case exclusions.+makeTitlecaseCommand :: PandocMonad m => LP m Inlines+makeTitlecaseCommand = do+ options <- option [] keyvals+ excl <- M.findWithDefault Set.empty "title" . sCaseExclusions <$> getState+ (if lookup "words" options == Just "all"+ then makeTitlecaseAll excl+ else makeTitlecase) <$> tok +-- | Titlecase the first letter of each word (\MakeTitlecase with+-- @words=all@), skipping excluded words.+makeTitlecaseAll :: Set.Set Text -> Inlines -> Inlines+makeTitlecaseAll excl = fromList . go . toList+ where+ go [] = []+ go xs =+ let (w, rest) = break isSep xs+ (seps, rest') = span isSep rest+ in tcWord w ++ seps ++ go rest'+ isSep Space = True+ isSep SoftBreak = True+ isSep LineBreak = True+ isSep _ = False+ tcWord w+ | stringify w `Set.member` excl = w+ | otherwise = toList (makeTitlecase (fromList w))++-- | Handle \DeclareUppercaseExclusions and friends: store a+-- comma-separated list of words excluded from case changing.+declareCaseExclusions :: PandocMonad m => Text -> LP m Blocks+declareCaseExclusions kind = do+ ws <- map T.strip . T.splitOn "," . untokenize <$> braced+ updateState $ \st -> st{ sCaseExclusions =+ M.insertWith Set.union kind (Set.fromList ws) (sCaseExclusions st) }+ return mempty++-- | Uppercase the first character of the first string (LaTeX3+-- \MakeTitlecase, which title-cases only the first word by default).+makeTitlecase :: Inlines -> Inlines+makeTitlecase = fromList . snd . go . toList+ where+ go :: [Inline] -> (Bool, [Inline])+ go (x : xs) =+ case goInline x of+ (True, x') -> (True, x' : xs)+ (False, x') -> (x' :) <$> go xs+ go [] = (False, [])+ goInline (Str t) | not (T.null t) =+ (True, Str (T.toTitle (T.take 1 t) <> T.drop 1 t))+ goInline (Emph ils) = Emph <$> go ils+ goInline (Strong ils) = Strong <$> go ils+ goInline (Underline ils) = Underline <$> go ils+ goInline (SmallCaps ils) = SmallCaps <$> go ils+ goInline (Span attr ils) = Span attr <$> go ils+ goInline (Link attr ils target) =+ (\ils' -> Link attr ils' target) <$> go ils+ goInline (Quoted qt ils) = Quoted qt <$> go ils+ goInline x = (False, x)+ fixPassthroughEscapes :: Inlines -> Inlines fixPassthroughEscapes = walk go where@@ -647,7 +809,7 @@ [ "special", "pdfannot", "pdfstringdef" , "bibliographystyle" , "maketitle", "makeindex", "makeglossary"- , "addcontentsline", "addtocontents", "addtocounter"+ , "addcontentsline", "addtocontents" -- \ignore{} is used conventionally in literate haskell for definitions -- that are to be processed by the compiler but not printed. , "ignore"@@ -704,13 +866,16 @@ -> eatOneToken *> option (str "-") (symbol '-' *> option (str "–") (str "—" <$ symbol '-'))- "'" -> eatOneToken *>- option (str "’") (str "”" <$ (guard ligatures *> symbol '\''))+ "'" | ligatures+ -> eatOneToken *>+ option (str "’") (str "”" <$ symbol '\'')+ | otherwise+ -> symbolAsString "~" -> str "\160" <$ eatOneToken "`" | ligatures -> doubleQuote <|> singleQuote <|> (str "‘" <$ symbol '`') | otherwise- -> str "‘" <$ symbol '`'+ -> symbolAsString "\"" | ligatures -> doubleQuote <|> singleQuote <|> symbolAsString "“" -> doubleQuote <|> symbolAsString@@ -740,13 +905,10 @@ opt :: PandocMonad m => LP m Inlines opt = do toks <- try (sp *> bracketedToks <* sp)- -- now parse the toks as inlines- st <- getState- parsed <- runParserT (mconcat <$> many inline) st "bracketed option"- (TokStream False toks)- case parsed of- Right result -> return result- Left e -> throwError $ fromParsecError (toSources toks) e+ -- now parse the toks as inlines; parseFromToks preserves any+ -- state changes (e.g. macro definitions), since an optional+ -- argument is not a TeX group+ parseFromToks (mconcat <$> many inline) toks -- block elements: @@ -764,7 +926,7 @@ rule :: PandocMonad m => LP m Blocks rule = do skipopts- width <- T.takeWhile (\c -> isDigit c || c == '.') . stringify <$> tok+ width <- T.takeWhile (\c -> isDigit c || c == '.') . stringifyInlines <$> tok _thickness <- tok -- 0-width rules are used to fix spacing issues: case safeRead width of@@ -945,7 +1107,8 @@ lookAhead $ blankline <|> startCommand return $ curr <> mconcat rest let raw = rawDefiniteBlock <|> rawMaybeBlock- lookupListDefault raw names blockCommands+ commandMap <- envBlockCommands <$> askEnv+ lookupListDefault raw names commandMap closing :: PandocMonad m => LP m Blocks closing = do@@ -975,6 +1138,7 @@ blockCommands :: PandocMonad m => M.Map Text (LP m Blocks) blockCommands = M.fromList [ ("par", mempty <$ skipopts)+ , ("addtocounter", addtocounter) , ("parbox", parbox) , ("title", mempty <$ (skipopts *> (grouped inline >>= addMeta "title")@@ -1062,10 +1226,30 @@ -- include , ("include", rawBlockOr "include" $ include "include") , ("input", rawBlockOr "input" $ include "input")+ , ("expandableinput", rawBlockOr "expandableinput" $ include "input") , ("subfile", rawBlockOr "subfile" doSubfile) , ("usepackage", rawBlockOr "usepackage" usepackage) -- preamble , ("PackageError", mempty <$ (braced >> braced >> braced))+ -- LaTeX3 conveniences, parsed and ignored:+ , ("ExplSyntaxOn", pure mempty)+ , ("ExplSyntaxOff", pure mempty)+ , ("ShowCommand", mempty <$ withVerbatimMode (spaces *> anyControlSeq))+ , ("ShowEnvironment", mempty <$ braced)+ , ("DeclareKeys", mempty <$ (skipopts *> braced))+ , ("DeclareUnknownKeyHandler", mempty <$ (skipopts *> braced))+ , ("ProcessKeyOptions", mempty <$ skipopts)+ , ("SetKeys", mempty <$ (skipopts *> braced))+ -- LaTeX3 case changing+ , ("DeclareUppercaseExclusions", declareCaseExclusions "upper")+ , ("DeclareLowercaseExclusions", declareCaseExclusions "lower")+ , ("DeclareTitlecaseExclusions", declareCaseExclusions "title")+ , ("AddToNoCaseChangeList", mempty <$ braced)+ , ("DeclareCaseChangeEquivalent", mempty <$+ (withVerbatimMode (spaces *> anyControlSeq) *> braced))+ , ("DeclareUppercaseMapping", mempty <$ (skipopts *> braced *> braced))+ , ("DeclareLowercaseMapping", mempty <$ (skipopts *> braced *> braced))+ , ("DeclareTitlecaseMapping", mempty <$ (skipopts *> braced *> braced)) -- epigraph package , ("epigraph", epigraph) -- alignment@@ -1154,7 +1338,8 @@ environment = try $ do controlSeq "begin" name <- untokenize <$> braced- M.findWithDefault mzero name environments <|>+ envMap <- envEnvironments <$> askEnv+ M.findWithDefault mzero name envMap <|> langEnvironment name <|> theoremEnvironment blocks inlines opt name <|> if M.member name (inlineEnvironments
@@ -13,8 +13,6 @@ import Data.Text (Text) import Control.Applicative ((<|>), optional, many) import Control.Monad (mzero)-import Control.Monad.Trans (lift)-import Control.Monad.Except (throwError) import Text.Pandoc.Parsing hiding (blankline, many, mathDisplay, mathInline, optional, space, spaces, withRaw, (<|>)) @@ -113,14 +111,10 @@ opt :: PandocMonad m => LP m Inlines opt = do toks <- try (sp *> bracketedToks <* sp)- -- now parse the toks as inlines- st <- getState- parsed <- lift $- runParserT (mconcat <$> many inline) st "bracketed option"- (TokStream False toks)- case parsed of- Right result -> return result- Left e -> throwError $ fromParsecError (toSources toks) e+ -- now parse the toks as inlines; parseFromToks preserves any+ -- state changes (e.g. macro definitions), since an optional+ -- argument is not a TeX group+ parseFromToks (mconcat <$> many inline) toks
@@ -36,8 +36,7 @@ import Text.Pandoc.Readers.LaTeX.Parsing import Text.Pandoc.Extensions (extensionEnabled, Extension(..)) import Text.Pandoc.Parsing (getOption, updateState, getState, notFollowedBy,- manyTill, getInput, setInput, incSourceColumn,- option, many1)+ manyTill, option, many1) import Data.Char (isDigit) import Text.Pandoc.Highlighting (fromListingsLanguage,) import Data.Maybe (maybeToList, fromMaybe)@@ -89,19 +88,6 @@ code . untokenize <$> manyTill (notFollowedBy newlineTok >> verbTok marker) (symbol marker) -verbTok :: PandocMonad m => Char -> LP m Tok-verbTok stopchar = do- t@(Tok pos toktype txt) <- anyTok- case T.findIndex (== stopchar) txt of- Nothing -> return t- Just i -> do- let (t1, t2) = T.splitAt i txt- TokStream macrosExpanded inp <- getInput- setInput $ TokStream macrosExpanded- $ Tok (incSourceColumn pos i) Symbol (T.singleton stopchar)- : tokenize (incSourceColumn pos (i + 1)) (T.drop 1 t2) ++ inp- return $ Tok pos toktype t1- listingsLanguage :: [(Text, Text)] -> Maybe Text listingsLanguage opts = case lookup "language" opts of@@ -282,7 +268,7 @@ , ("{", lit "{") , ("}", lit "}") , ("-", lit "\x00ad") -- soft hyphen- , ("qed", lit "\a0\x25FB")+ , ("qed", lit "\xa0\x25FB") , ("lq", return (str "‘")) , ("rq", return (str "’")) , ("textquoteleft", return (str "‘"))
@@ -12,7 +12,10 @@ import Text.Pandoc.Parsing hiding (blankline, mathDisplay, mathInline, optional, space, spaces, withRaw, (<|>)) import Control.Applicative ((<|>), optional)+import Control.Monad (guard)+import Data.Char (chr, isLetter, ord) import qualified Data.Map as M+import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as T import qualified Data.List.NonEmpty as NonEmpty@@ -20,15 +23,22 @@ macroDef :: (PandocMonad m, Monoid a) => (Text -> a) -> LP m a macroDef constructor = do+ Tok _ (CtrlSeq name) _ <- peekTok+ -- fail quickly, before trying each alternative in turn, unless+ -- the next token can begin a macro definition:+ guard $ name `Set.member` macroDefCommands (_, s) <- withRaw (commandDef <|> environmentDef) (constructor (untokenize s) <$ guardDisabled Ext_latex_macros) <|> return mempty where commandDef = do- nameMacroPairs <- newcommand <|>+ nameMacroPairs <- newcommand <|> newDocumentCommand <|>+ newDocumentEnvironment <|> commandCopy <|> environmentCopy <|> checkGlobal (letmacro <|> edefmacro <|> defmacro <|> newif) guardDisabled Ext_latex_macros <|>- mapM_ insertMacro nameMacroPairs+ mapM_ insertMacro+ (filter ((`Set.notMember` pandocInternalMacros) . fst)+ nameMacroPairs) environmentDef = do mbenv <- newenvironment case mbenv of@@ -42,6 +52,33 @@ -- @\newcommand{\envname}[n-args][default]{begin}@ -- @\newcommand{\endenvname}@ +-- | Macros defined in pandoc's LaTeX template and handled directly+-- by the reader. Their definitions are too complex for the reader's+-- macro expansion, so we ignore them.+pandocInternalMacros :: Set.Set Text+pandocInternalMacros = Set.fromList+ [ "pandocbounded", "pandocfootnotemark", "pandocfootnotetext" ]++-- | Control sequences that can begin a macro definition. This+-- must include every control sequence that one of the parsers+-- used in 'macroDef' can start with.+macroDefCommands :: Set.Set Text+macroDefCommands = Set.fromList+ [ "global" -- see checkGlobal+ , "let", "edef", "xdef", "def", "gdef", "newif"+ , "newcommand", "renewcommand", "providecommand"+ , "DeclareMathOperator", "DeclareRobustCommand"+ , "NewDocumentCommand", "RenewDocumentCommand"+ , "ProvideDocumentCommand", "DeclareDocumentCommand"+ , "NewExpandableDocumentCommand", "RenewExpandableDocumentCommand"+ , "ProvideExpandableDocumentCommand", "DeclareExpandableDocumentCommand"+ , "NewDocumentEnvironment", "RenewDocumentEnvironment"+ , "ProvideDocumentEnvironment", "DeclareDocumentEnvironment"+ , "NewCommandCopy", "RenewCommandCopy", "DeclareCommandCopy"+ , "NewEnvironmentCopy", "RenewEnvironmentCopy", "DeclareEnvironmentCopy"+ , "newenvironment", "renewenvironment", "provideenvironment"+ ]+ insertMacro :: PandocMonad m => (Text, Macro) -> LP m () insertMacro (name, macro'@(Macro GlobalScope _ _ _ _)) = updateState $ \s ->@@ -116,10 +153,11 @@ -- \footrue to be a command that defines \iffoo to be \iftrue -- \foofalse to be a command that defines \iffoo to be \iffalse newif :: PandocMonad m => LP m [(Text, Macro)]-newif = do+newif = try $ do controlSeq "newif" withVerbatimMode $ do Tok pos (CtrlSeq name) _ <- anyControlSeq+ guard $ "if" `T.isPrefixOf` name -- \def\iffoo\iffalse -- \def\footrue{\def\iffoo\iftrue} -- \def\foofalse{\def\iffoo\iffalse}@@ -191,6 +229,241 @@ "renewcommand" -> return [(name, macro)] _ -> [] <$ report (MacroAlreadyDefined txt pos)) <|> pure [(name, macro)]++-- | Parses a definition of the form+-- @\NewDocumentCommand\cmd{argspec}{body}@ (and the Renew, Provide,+-- Declare, and Expandable variants), with xparse (LaTeX3) argument+-- specifiers.+newDocumentCommand :: PandocMonad m => LP m [(Text, Macro)]+newDocumentCommand = try $ do+ Tok pos (CtrlSeq mtype) _ <-+ controlSeq "NewDocumentCommand"+ <|> controlSeq "RenewDocumentCommand"+ <|> controlSeq "ProvideDocumentCommand"+ <|> controlSeq "DeclareDocumentCommand"+ <|> controlSeq "NewExpandableDocumentCommand"+ <|> controlSeq "RenewExpandableDocumentCommand"+ <|> controlSeq "ProvideExpandableDocumentCommand"+ <|> controlSeq "DeclareExpandableDocumentCommand"+ withVerbatimMode $ do+ Tok _ (CtrlSeq name) txt <- do+ spaces+ anyControlSeq <|>+ (symbol '{' *> spaces *> anyControlSeq <* spaces <* symbol '}')+ spaces+ (argspecs, _) <- xparseArgSpecs False+ spaces+ contents <- bracedOrToken+ let macro = Macro GroupScope ExpandWhenUsed argspecs Nothing contents+ (do lookupMacro name+ if "Provide" `T.isPrefixOf` mtype+ then return []+ else if "New" `T.isPrefixOf` mtype+ then [] <$ report (MacroAlreadyDefined txt pos)+ else return [(name, macro)]) -- Renew or Declare+ <|> pure [(name, macro)]++-- | Parses a definition of the form+-- @\NewDocumentEnvironment{name}{argspec}{begin-code}{end-code}@+-- (and the Renew, Provide, and Declare variants), with xparse+-- (LaTeX3) argument specifiers. Unlike with @\newenvironment@,+-- the arguments are also available in the end-code; to support+-- this we bind a group-scoped helper macro, at the point where+-- @\begin{name}@ is expanded, whose body is the end-code with the+-- arguments already substituted; @\end{name}@ just expands the+-- helper. (Group scoping makes this work for nested environments.)+newDocumentEnvironment :: PandocMonad m => LP m [(Text, Macro)]+newDocumentEnvironment = try $ do+ Tok pos (CtrlSeq mtype) _ <-+ controlSeq "NewDocumentEnvironment"+ <|> controlSeq "RenewDocumentEnvironment"+ <|> controlSeq "ProvideDocumentEnvironment"+ <|> controlSeq "DeclareDocumentEnvironment"+ withVerbatimMode $ do+ spaces+ name <- T.strip . untokenize <$> braced+ spaces+ (argspecs, usesBody) <- xparseArgSpecs True+ startcontents <- spaces >> bracedOrToken+ endcontents <- spaces >> bracedOrToken+ -- we need the environment to be in a group so macros defined+ -- inside behave correctly:+ let bg = Tok pos (CtrlSeq "bgroup") "\\bgroup "+ let eg = Tok pos (CtrlSeq "egroup") "\\egroup "+ let helperName = "pandocxparseenvend" <> letterize name+ let helper = Tok pos (CtrlSeq helperName) ("\\" <> helperName <> " ")+ let defHelper = Tok pos (CtrlSeq "def") "\\def"+ : helper+ : Tok pos Symbol "{"+ : endcontents ++ [Tok pos Symbol "}"]+ let result+ | usesBody =+ -- a 'b' argspec grabs everything up to \end{name} as the+ -- last argument (the argspecs end with its ArgNum, and we+ -- add a Pattern that consumes the \end{name}), so begin-+ -- and end-code run together:+ [ (name,+ Macro GroupScope ExpandWhenUsed+ (argspecs ++ [Pattern (tokenize pos ("\\end{" <> name <> "}"))])+ Nothing+ (bg : defHelper ++ startcontents ++ [helper, eg])) ]+ | otherwise =+ [ (name,+ Macro GroupScope ExpandWhenUsed argspecs Nothing+ (bg : defHelper ++ startcontents))+ , ("end" <> name,+ Macro GroupScope ExpandWhenUsed [] Nothing [helper, eg]) ]+ (do lookupMacro name+ if "Provide" `T.isPrefixOf` mtype+ then return []+ else if "New" `T.isPrefixOf` mtype+ then [] <$ report (MacroAlreadyDefined name pos)+ else return result) -- Renew or Declare+ <|> pure result++-- | Parses @\NewCommandCopy\new\old@ (and the Renew and Declare+-- variants): like @\let@, restricted to control sequences.+commandCopy :: PandocMonad m => LP m [(Text, Macro)]+commandCopy = try $ do+ Tok pos (CtrlSeq mtype) _ <-+ controlSeq "NewCommandCopy"+ <|> controlSeq "RenewCommandCopy"+ <|> controlSeq "DeclareCommandCopy"+ withVerbatimMode $ do+ Tok _ (CtrlSeq name) txt <- do+ spaces+ anyControlSeq <|>+ (symbol '{' *> spaces *> anyControlSeq <* spaces <* symbol '}')+ spaces+ target@(Tok _ (CtrlSeq targetName) _) <- anyControlSeq+ result <- (do m <- lookupMacro targetName+ pure [(name, m)])+ <|> pure [(name, Macro GroupScope ExpandWhenDefined [] Nothing+ [target])]+ (do lookupMacro name+ if "New" `T.isPrefixOf` mtype+ then [] <$ report (MacroAlreadyDefined txt pos)+ else return result) -- Renew or Declare+ <|> pure result++-- | Parses @\NewEnvironmentCopy{new}{old}@ (and the Renew and+-- Declare variants), copying both the begin and the end macros.+-- If the target environment is not macro-defined, the copy expands+-- to @\begin{old}@ / @\end{old}@.+environmentCopy :: PandocMonad m => LP m [(Text, Macro)]+environmentCopy = try $ do+ Tok pos (CtrlSeq mtype) _ <-+ controlSeq "NewEnvironmentCopy"+ <|> controlSeq "RenewEnvironmentCopy"+ <|> controlSeq "DeclareEnvironmentCopy"+ withVerbatimMode $ do+ spaces+ name <- untokenize <$> braced+ spaces+ target <- untokenize <$> braced+ let copyOne to from fallback =+ (do m <- lookupMacro from+ pure (to, m))+ <|> pure (to, Macro GroupScope ExpandWhenUsed [] Nothing+ (tokenize pos fallback))+ beginmacro <- copyOne name target ("\\begin{" <> target <> "}")+ endmacro <- copyOne ("end" <> name) ("end" <> target)+ ("\\end{" <> target <> "}")+ let result = [beginmacro, endmacro]+ (do lookupMacro name+ if "New" `T.isPrefixOf` mtype+ then [] <$ report (MacroAlreadyDefined name pos)+ else return result) -- Renew or Declare+ <|> pure result++-- | Encode a text using letters only (so that the result can be+-- part of a control sequence name that survives retokenization).+letterize :: Text -> Text+letterize = T.concatMap go+ where go c | isLetter c = T.singleton c+ | otherwise = "x" <> T.map toLetter (T.pack (show (ord c)))+ toLetter d = chr (ord d + 49) -- '0'..'9' -> 'a'..'j'++-- | Parses a braced xparse argument specification, e.g.+-- @{s O{default} m}@. The Bool parameter determines whether a+-- @b@ (environment body) specifier is allowed; the Bool in the+-- result is True if one was used (it can only come last).+xparseArgSpecs :: PandocMonad m => Bool -> LP m ([ArgSpec], Bool)+xparseArgSpecs allowBody = symbol '{' *> go 1+ where+ go n = do+ spaces+ (([], False) <$ symbol '}') <|>+ (do (spec, isBody) <- xparseArgSpec allowBody n+ if isBody+ then ([spec], True) <$ (spaces <* symbol '}')+ else do (rest, usesBody) <- go (n + numslots spec)+ return (spec : rest, usesBody))+ -- most specifiers bind one argument; embellishments bind one+ -- argument per embellishment token+ numslots (EmbellishArg embs) = length embs+ numslots (ProcessedArg _ spec) = numslots spec+ numslots _ = 1++xparseArgSpec :: PandocMonad m => Bool -> Int -> LP m (ArgSpec, Bool)+xparseArgSpec allowBody n = go True+ where+ -- the parameter is False if the ! modifier has been seen; it+ -- disables space-skipping before optional arguments, and (for the+ -- b and c body specifiers) trimming of the ends of the body+ go noBang = do+ Tok pos _ c <- singleChar+ let plain spec = pure (spec, False)+ case c of+ "+" -> spaces *> go noBang -- "long": no distinction+ "!" -> spaces *> go False -- no space-skipping+ "=" -> spaces *> braced *> spaces *> go noBang+ -- key-value interface for the argument: not modeled+ ">" -> do proc <- spaces *> braced+ (spec, isBody) <- spaces *> go noBang+ if isBody+ then pure (spec, isBody)+ -- processors are not supported on 'b' arguments+ else case spec of+ ProcessedArg procs inner ->+ pure (ProcessedArg (proc : procs) inner, False)+ _ -> pure (ProcessedArg [proc] spec, False)+ "m" -> plain $ ArgNum n+ "o" -> plain $ DelimArg noBang (lbTok pos) (rbTok pos) Nothing+ "O" -> do dflt <- spaces *> braced+ plain $ DelimArg noBang (lbTok pos) (rbTok pos) (Just dflt)+ "s" -> plain $ BoolArg noBang (Tok pos Symbol "*")+ "t" -> specTok >>= plain . BoolArg noBang+ "d" -> do o <- specTok+ c' <- specTok+ plain $ DelimArg noBang o c' Nothing+ "D" -> do o <- specTok+ c' <- specTok+ dflt <- spaces *> braced+ plain $ DelimArg noBang o c' (Just dflt)+ "r" -> do o <- specTok+ c' <- specTok+ plain $ DelimArg noBang o c' Nothing+ "R" -> do o <- specTok+ c' <- specTok+ dflt <- spaces *> braced+ plain $ DelimArg noBang o c' (Just dflt)+ "v" -> plain VerbArg+ "e" -> do embtoks <- spaces *> braced+ plain $ EmbellishArg [(t, Nothing) | t <- embTokens embtoks]+ "E" -> do embtoks <- spaces *> braced+ spaces+ defaults <- symbol '{' *> many (try (spaces *> braced))+ <* spaces <* symbol '}'+ plain $ EmbellishArg $+ zip (embTokens embtoks) (map Just defaults ++ repeat Nothing)+ "b" | allowBody -> pure (BodyArg noBang n, True)+ "c" | allowBody -> pure (VerbBodyArg noBang n, True) -- verbatim body+ _ -> fail "unsupported xparse argument specifier"+ lbTok pos = Tok pos Symbol "["+ rbTok pos = Tok pos Symbol "]"+ specTok = spaces *> singleChar+ embTokens = filter (not . tokTypeIn [Spaces, Newline, Comment]) newenvironment :: PandocMonad m => LP m (Maybe (Text, Macro, Macro)) newenvironment = do
@@ -25,6 +25,9 @@ , LaTeXState(..) , defaultLaTeXState , LP+ , LaTeXEnv(..)+ , emptyLaTeXEnv+ , askEnv , TokStream(..) , withVerbatimMode , rawLaTeXParser@@ -73,6 +76,7 @@ , bracedOrToken , bracketed , bracketedToks+ , verbTok , parenWrapped , dimenarg , ignore@@ -97,11 +101,12 @@ import Control.Applicative (many, (<|>)) import Control.Monad import Control.Monad.Except (throwError)+import Control.Monad.Reader (ReaderT, ask, runReaderT) import Control.Monad.Trans (lift) import Data.Char (chr, isAlphaNum, isDigit, isLetter, ord) import Data.Default-import Data.List (intercalate)-import qualified Data.IntMap as IntMap+import Data.List (dropWhileEnd, intercalate, isSuffixOf, unfoldr)+import Numeric (showEFloat, showFFloat) import qualified Data.Map as M import qualified Data.Set as Set import Data.Text (Text)@@ -112,7 +117,7 @@ import Text.Pandoc.Builder import Text.Pandoc.Class.PandocMonad (PandocMonad, report) import Text.Pandoc.Error- (PandocError (PandocMacroLoop,PandocShouldNeverHappenError))+ (PandocError (PandocMacroLoop)) import Text.Pandoc.Logging import Text.Pandoc.Options import Text.Pandoc.Parsing hiding (blankline, many, mathDisplay, mathInline,@@ -165,7 +170,10 @@ , sLastFigureNum :: DottedNum , sLastTableNum :: DottedNum , sLastNoteNum :: Int- , sFootnoteTexts :: M.Map Int Blocks+ , sFootnoteTexts :: M.Map Text Blocks+ -- ^ keys are footnote numbers for+ -- \footnotetext and "pandoc:" ids for+ -- \pandocfootnotetext , sTheoremMap :: M.Map Text TheoremSpec , sLastTheoremStyle :: TheoremStyle , sLastLabel :: Maybe Text@@ -174,8 +182,23 @@ , sToggles :: M.Map Text Bool , sFileContents :: M.Map Text Text , sEnableWithRaw :: Bool- , sRawTokens :: IntMap.IntMap [Tok]+ , sRawTokens :: [Tok]+ -- ^ reversed list of tokens consumed+ -- while at least one withRaw scope is+ -- active+ , sRawTokenCount :: !Int+ -- ^ length of sRawTokens+ , sRawScopes :: !Int+ -- ^ number of active withRaw scopes , sLigatures :: Bool+ , sCaseExclusions :: M.Map Text (Set.Set Text)+ -- ^ words excluded from case changing+ -- (keys: @upper@, @lower@, @title@)+ , sLastExpansion :: Maybe (Text, SourcePos, Int)+ -- ^ name and position of the last macro+ -- expanded, and the number of consecutive+ -- times it has been expanded there+ -- (used to detect expansion loops) } deriving Show @@ -205,8 +228,12 @@ , sToggles = M.empty , sFileContents = M.empty , sEnableWithRaw = True- , sRawTokens = IntMap.empty+ , sRawTokens = []+ , sRawTokenCount = 0+ , sRawScopes = 0 , sLigatures = True+ , sCaseExclusions = M.empty+ , sLastExpansion = Nothing } instance PandocMonad m => HasQuoteContext LaTeXState m where@@ -267,8 +294,27 @@ uncons (TokStream _ []) = return Nothing uncons (TokStream _ (t:ts)) = return $ Just (t, TokStream False ts) -type LP m = ParsecT TokStream LaTeXState m+type LP m = ParsecT TokStream LaTeXState (ReaderT (LaTeXEnv m) m) +-- | Environment holding the command dispatch tables. Because these+-- tables have types that mention the parser monad, they cannot be+-- top-level constants; passing them in a reader environment ensures+-- they are constructed once per parse rather than once per use.+data LaTeXEnv m = LaTeXEnv+ { envInlineCommands :: M.Map Text (LP m Inlines)+ , envBlockCommands :: M.Map Text (LP m Blocks)+ , envEnvironments :: M.Map Text (LP m Blocks)+ }++-- | An environment with empty dispatch tables, for running parsers+-- that do not consult them.+emptyLaTeXEnv :: LaTeXEnv m+emptyLaTeXEnv = LaTeXEnv mempty mempty mempty++-- | Retrieve the command dispatch tables.+askEnv :: Monad m => LP m (LaTeXEnv m)+askEnv = lift ask+ withVerbatimMode :: PandocMonad m => LP m a -> LP m a withVerbatimMode parser = do alreadyVerbatimMode <- sVerbatimMode <$> getState@@ -281,9 +327,9 @@ return result rawLaTeXParser :: (PandocMonad m, HasMacros s, HasReaderOptions s, Show a)- => [Tok] -> LP m () -> LP m a+ => LaTeXEnv m -> [Tok] -> LP m () -> LP m a -> ParsecT Sources s m (a, Text)-rawLaTeXParser toks parser valParser = do+rawLaTeXParser lenv toks parser valParser = do pstate <- getState let lstate = def{ sOptions = extractReaderOptions pstate } let lstate' = lstate { sMacros = extractMacros pstate :| [] }@@ -292,12 +338,14 @@ _ -> return () let preparser = setStartPos >> parser let rawparser = (,) <$> withRaw valParser <*> getState- res' <- lift $ runParserT (withRaw (preparser >> getPosition))+ res' <- lift $ flip runReaderT lenv $+ runParserT (withRaw (preparser >> getPosition)) lstate "chunk" $ TokStream False toks case res' of Left _ -> mzero Right (endpos, toks') -> do- res <- lift $ runParserT rawparser lstate' "chunk"+ res <- lift $ flip runReaderT lenv $+ runParserT rawparser lstate' "chunk" $ TokStream False toks' case res of Left _ -> mzero@@ -305,7 +353,7 @@ updateState (updateMacros ((NonEmpty.head (sMacros st)) <>)) let rawChar = do pos <- getPosition- if pos >= endpos+ if sourceName pos == sourceName endpos && pos >= endpos then mzero else anyChar result <- (guardEnabled Ext_latex_macros@@ -328,7 +376,8 @@ pstate <- getState let lstate = def{ sOptions = extractReaderOptions pstate , sMacros = extractMacros pstate :| [] }- res <- runParserT retokenize lstate "math" $+ res <- flip runReaderT emptyLaTeXEnv $+ runParserT retokenize lstate "math" $ TokStream False (tokenize (initialPos "math") s) case res of Left e -> Prelude.fail (show e)@@ -360,7 +409,7 @@ Sources ((_,t):rest) -> tokenizeSources $ Sources ((pos,t):rest) tokenize :: SourcePos -> Text -> [Tok]-tokenize = totoks False+tokenize = totoks (TokenizerState False False) where totoks atIsLetter pos t = case T.uncons t of@@ -389,10 +438,17 @@ | isLetter' atIsLetter d -> let (ws, rest'') = T.span (isLetter' atIsLetter) rest (ss, rest''') = T.span isSpaceOrTab rest''- atIsLetter' = case ws of- "makeatletter" -> True- "makeatother" -> False- _ -> atIsLetter+ atIsLetter' =+ case ws of+ "makeatletter" ->+ atIsLetter{ tsAtIsLetter = True }+ "makeatother" ->+ atIsLetter{ tsAtIsLetter = False }+ "ExplSyntaxOn" ->+ atIsLetter{ tsExplSyntax = True }+ "ExplSyntaxOff" ->+ atIsLetter{ tsExplSyntax = False }+ _ -> atIsLetter in Tok pos (CtrlSeq ws) ("\\" <> ws <> ss) : totoks atIsLetter' (incSourceColumn pos (1 + T.length ws + T.length ss)) rest'''@@ -408,11 +464,10 @@ Just ('\n', _) -> Tok pos (CtrlSeq " ") ("\\" <> w1) : totoks atIsLetter- (incSourceColumn pos (T.length ws)) r1+ (incSourceColumn pos (1 + T.length w1)) r1 _ -> Tok pos (CtrlSeq " ") ws- : totoks atIsLetter- (incSourceColumn pos (T.length ws)) r3+ : totoks atIsLetter (advancePos pos ws) r3 | otherwise -> Tok pos (CtrlSeq (T.singleton d)) (T.pack [c,d]) : totoks atIsLetter (incSourceColumn pos 2) rest'@@ -427,7 +482,7 @@ (incSourceColumn pos (2 + T.length t1)) t2 Nothing -> Tok pos Symbol "#" : Tok (incSourceColumn pos 1) Symbol "#"- : totoks atIsLetter (incSourceColumn pos 1) t3+ : totoks atIsLetter (incSourceColumn pos 2) t3 _ -> let (t1, t2) = T.span (\d -> d >= '0' && d <= '9') rest in case safeRead t1 of@@ -465,14 +520,29 @@ Tok pos Symbol (T.singleton c) : totoks atIsLetter (incSourceColumn pos 1) rest + -- Keep tabs at one column, as in the rest of the tokenizer.+ advancePos = T.foldl' $ \pos c ->+ if c == '\n'+ then setSourceColumn (incSourceLine pos 1) 1+ else incSourceColumn pos 1+ isSpaceOrTab :: Char -> Bool isSpaceOrTab ' ' = True isSpaceOrTab '\t' = True isSpaceOrTab _ = False --- First parameter is True if @ is letter-isLetter' :: Bool -> Char -> Bool-isLetter' True '@' = True+-- | State threaded through the tokenizer: whether @\@@ is a letter+-- (between @\makeatletter@ and @\makeatother@), and whether @:@ and+-- @_@ are letters (between @\ExplSyntaxOn@ and @\ExplSyntaxOff@).+data TokenizerState = TokenizerState+ { tsAtIsLetter :: Bool+ , tsExplSyntax :: Bool+ }++isLetter' :: TokenizerState -> Char -> Bool+isLetter' st '@' = tsAtIsLetter st+isLetter' st ':' = tsExplSyntax st+isLetter' st '_' = tsExplSyntax st isLetter' _ c = isLetter c isLetterOrAt :: Char -> Bool@@ -483,21 +553,23 @@ isLowerHex x = x >= '0' && x <= '9' || x >= 'a' && x <= 'f' untokenize :: [Tok] -> Text-untokenize = foldr untokenAccum mempty--untokenAccum :: Tok -> Text -> Text-untokenAccum (Tok _ (CtrlSeq _) t) accum =- -- insert space to prevent breaking a control sequence; see #5836- case (T.unsnoc t, T.uncons accum) of- (Just (_,c), Just (d,_))- | isLetter c- , isLetter d- -> t <> " " <> accum- _ -> t <> accum-untokenAccum (Tok _ _ t) accum = t <> accum+untokenize = T.concat . go+ where+ go [] = []+ go (Tok _ (CtrlSeq _) t : ts)+ -- insert space to prevent breaking a control sequence; see #5836+ | Just (_, c) <- T.unsnoc t+ , isLetter c+ , nextStartsWithLetter ts = t : " " : go ts+ go (Tok _ _ t : ts) = t : go ts+ nextStartsWithLetter (Tok _ _ t : ts) =+ case T.uncons t of+ Just (d, _) -> isLetter d+ Nothing -> nextStartsWithLetter ts+ nextStartsWithLetter [] = False untoken :: Tok -> Text-untoken t = untokenAccum t mempty+untoken (Tok _ _ t) = t parseFromToks :: PandocMonad m => LP m a -> [Tok] -> LP m a parseFromToks parser toks = do@@ -507,11 +579,15 @@ case toks of Tok pos _ _ : _ -> setPosition pos _ -> return ()- -- we ignore existing raw tokens maps (see #9517)- oldRawTokens <- sRawTokens <$> getState- updateState $ \st -> st{ sRawTokens = mempty }+ -- we ignore existing raw token accumulation (see #9517)+ oldst <- getState+ updateState $ \st -> st{ sRawTokens = []+ , sRawTokenCount = 0+ , sRawScopes = 0 } result <- parser- updateState $ \st -> st{ sRawTokens = oldRawTokens }+ updateState $ \st -> st{ sRawTokens = sRawTokens oldst+ , sRawTokenCount = sRawTokenCount oldst+ , sRawScopes = sRawScopes oldst } setInput oldInput setPosition oldpos return result@@ -529,10 +605,9 @@ doMacros -- apply macros on remaining input stream res <- tokenPrim (T.unpack . untoken) updatePos matcher updateState $ \st ->- if sEnableWithRaw st- then- let !newraws = IntMap.map (res:) $! sRawTokens st- in st{ sRawTokens = newraws }+ if sRawScopes st > 0 && sEnableWithRaw st+ then st{ sRawTokens = res : sRawTokens st+ , sRawTokenCount = sRawTokenCount st + 1 } else st return $! res where matcher t | f t = Just t@@ -544,15 +619,24 @@ peekTok :: PandocMonad m => LP m Tok peekTok = do doMacros- lookAhead (satisfyTok (const True))+ TokStream _ toks <- getInput+ case toks of+ t : _ -> return t+ [] -> mzero doMacros :: PandocMonad m => LP m () doMacros = do TokStream macrosExpanded toks <- getInput- unless macrosExpanded $ do- st <- getState- unless (sVerbatimMode st) $- doMacros' 1 toks >>= setInput . TokStream True+ unless macrosExpanded $+ case toks of+ -- only a control sequence at the head of the stream can+ -- trigger macro expansion; in other cases we skip the+ -- state update:+ Tok _ (CtrlSeq _) _ : _ -> do+ st <- getState+ unless (sVerbatimMode st) $+ doMacros' 1 toks >>= setInput . TokStream True+ _ -> return () doMacros' :: PandocMonad m => Int -> [Tok] -> LP m [Tok] doMacros' n inp =@@ -583,18 +667,81 @@ matchPattern toks = try $ mapM_ matchTok toks - getargs argmap [] = return argmap- getargs argmap (Pattern toks : rest) = try $ do+ -- the first parameter is the number of the next argument+ -- to be bound (needed for the argspecs that don't carry an+ -- argument number themselves)+ getargs _ argmap [] = return argmap+ getargs num argmap (Pattern toks : rest) = try $ do matchPattern toks- getargs argmap rest- getargs argmap (ArgNum i : Pattern toks : rest) =+ getargs num argmap rest+ getargs _ argmap (ArgNum i : Pattern toks : rest) = try $ do x <- mconcat <$> manyTill (braced <|> ((:[]) <$> anyTok)) (matchPattern toks)- getargs (M.insert i x argmap) rest- getargs argmap (ArgNum i : rest) = do+ getargs (i + 1) (M.insert i x argmap) rest+ getargs _ argmap (ArgNum i : rest) = do x <- try $ spaces >> bracedOrToken- getargs (M.insert i x argmap) rest+ getargs (i + 1) (M.insert i x argmap) rest+ getargs num argmap (BoolArg skipSp t@(Tok pos _ _) : rest) = do+ -- the ! modifier (skipSp False) disables space-skipping:+ let sp' = when skipSp sp+ x <- option [Tok pos (CtrlSeq "BooleanFalse") "\\BooleanFalse "]+ ([Tok pos (CtrlSeq "BooleanTrue") "\\BooleanTrue "]+ <$ try (sp' *> matchTok t))+ getargs (num + 1) (M.insert num x argmap) rest+ getargs num argmap (DelimArg skipSp open@(Tok pos _ _) close mbdef+ : rest) = do+ let sp' = when skipSp sp+ let missing = fromMaybe [Tok pos (CtrlSeq "NoValue") "\\NoValue "] mbdef+ x <- option missing (try (sp' *> delimitedToks open close))+ getargs (num + 1) (M.insert num x argmap) rest+ getargs num argmap (VerbArg : rest) = do+ x <- verbatimArg+ getargs (num + 1) (M.insert num x argmap) rest+ -- xparse 'b' specifier: environment body, grabbed up to the+ -- \end{name} pattern; spaces at the ends are trimmed unless+ -- the ! modifier was used:+ getargs _ argmap (BodyArg doTrim i : Pattern toks : rest) = try $ do+ x <- mconcat <$> manyTill ((snd <$> withRaw (try braced))+ <|> ((:[]) <$> anyTok))+ (matchPattern toks)+ let x' = if doTrim then trimSpaceToks x else x+ getargs (i + 1) (M.insert i x' argmap) rest+ getargs num argmap (BodyArg _ i : rest) =+ getargs num argmap (ArgNum i : rest)+ -- xparse 'c' specifier: environment body, grabbed verbatim up+ -- to the \end{name} pattern; blank lines at the ends are+ -- trimmed unless the ! modifier was used:+ getargs _ argmap (VerbBodyArg doTrim i : Pattern toks : rest) = try $ do+ x <- mconcat <$> manyTill ((snd <$> withRaw (try braced))+ <|> ((:[]) <$> anyTok))+ (matchPattern toks)+ getargs (i + 1) (M.insert i (verbatimBodyToks doTrim x) argmap) rest+ getargs num argmap (VerbBodyArg _ i : rest) =+ getargs num argmap (ArgNum i : rest)+ getargs num argmap (EmbellishArg embs : rest) = do+ let grab seen+ | length seen == length embs = pure seen+ | otherwise = option seen $ try $ do+ sp+ i <- choice [ i <$ matchTok t+ | (i, (t, _)) <- zip [(0 :: Int)..] embs+ , i `notElem` map fst seen ]+ x <- braced <|> count 1 anyTok+ grab ((i, x) : seen)+ seen <- grab []+ let getval i (Tok pos _ _, mbdef) =+ case lookup i seen of+ Just x -> x+ Nothing ->+ fromMaybe [Tok pos (CtrlSeq "NoValue") "\\NoValue "] mbdef+ let argmap' = foldr (\(i, e) m -> M.insert (num + i) (getval i e) m)+ argmap (zip [0..] embs)+ getargs (num + length embs) argmap' rest+ getargs num argmap (ProcessedArg procs spec : rest) = do+ newargs <- getargs num M.empty [spec]+ newargs' <- traverse (applyArgProcessors procs) newargs+ getargs (num + M.size newargs) (M.union newargs' argmap) rest addTok False _args spos (Tok _ (DeferredArg i) txt) acc = Tok spos (Arg i) txt : acc@@ -615,18 +762,32 @@ $ throwError $ PandocMacroLoop name (macros :| _ ) <- sMacros <$> getState case M.lookup name macros of- Nothing -> trySpecialMacro name ts+ -- the result of a special macro may itself begin with a+ -- macro call, so we continue expanding:+ Nothing -> trySpecialMacro name ts >>= doMacros' (n' + 1) Just (Macro _scope expansionPoint argspecs optarg newtoks) -> do+ -- A macro whose expansion contains itself after an+ -- unknown command (e.g. \def\small{\@setfontsize\small..})+ -- would be expanded again and again at the same position,+ -- each time in a fresh call to doMacros (#11088):+ lastExp <- sLastExpansion <$> getState+ let expCount = case lastExp of+ Just (name', spos', i)+ | name' == name, spos' == spos -> i + 1+ _ -> 1 :: Int+ when (expCount > 1000) $ throwError $ PandocMacroLoop name+ updateState $ \st -> st{ sLastExpansion =+ Just (name, spos, expCount) } let getargs' = do args <- (case expansionPoint of ExpandWhenUsed -> withVerbatimMode ExpandWhenDefined -> id) $ case optarg of- Nothing -> getargs M.empty argspecs+ Nothing -> getargs 1 M.empty argspecs Just o -> do x <- option o bracketedToks- getargs (M.singleton 1 x) $ drop 1 argspecs+ getargs 2 (M.singleton 1 x) $ drop 1 argspecs TokStream _ rest <- getInput return (args, rest) lstate <- getState@@ -634,11 +795,34 @@ case res of Left _ -> Prelude.fail $ "Could not parse arguments for " ++ T.unpack name- Right (args, rest) -> do+ Right (args', rest) -> do+ -- An argument default may refer to other arguments+ -- (e.g. O{#2}); resolve such references (the visited+ -- list guards against reference cycles):+ let resolveTok visited t@(Tok _ (Arg j) _)+ | j `notElem` visited+ , Just ys <- M.lookup j args'+ = concatMap (resolveTok (j : visited)) ys+ | otherwise = [t]+ resolveTok _ t = [t]+ let args = M.mapWithKey+ (\i -> concatMap (resolveTok [i])) args'+ -- In TeX, a newline in a macro body behaves like a+ -- space; a single newline at the end of the body,+ -- followed by a newline in the source, must not be+ -- mistaken for a blank line (paragraph break):+ let newtoks' =+ case reverse newtoks of+ Tok p Newline _ : rts+ | not (any isNewlineTok (take 1 rts))+ , (Tok _ Newline _ : _) <-+ dropWhile (tokTypeIn [Spaces, Comment]) rest+ -> reverse (Tok p Spaces " " : rts)+ _ -> newtoks -- first boolean param is true if we're tokenizing -- an argument (in which case we don't want to -- expand #1 etc.)- let result = foldr (addTok False args spos) rest newtoks+ let result = foldr (addTok False args spos) rest newtoks' case expansionPoint of ExpandWhenUsed -> doMacros' (n' + 1) result ExpandWhenDefined -> return result@@ -652,15 +836,133 @@ Tok pos Word t : _ | startsWithAlphaNum t -> return $ Tok pos Spaces " " : ts' _ -> return ts'-trySpecialMacro "iftrue" ts = handleIf (ifParser True) ts-trySpecialMacro "iffalse" ts = handleIf (ifParser False) ts+trySpecialMacro "iftrue" ts = doIf True ts+trySpecialMacro "iffalse" ts = doIf False ts trySpecialMacro "ifmmode" ts = do mathMode <- sMathMode <$> getState- handleIf (ifParser mathMode) ts+ doIf mathMode ts trySpecialMacro "ifstrequal" ts = do handleIf ifStrequalParser ts+-- xparse (LaTeX3) argument conditionals:+trySpecialMacro "IfNoValueTF" ts = handleIf (xparseIf isNoValueArg True True) ts+trySpecialMacro "IfNoValueT" ts = handleIf (xparseIf isNoValueArg True False) ts+trySpecialMacro "IfNoValueF" ts = handleIf (xparseIf isNoValueArg False True) ts+trySpecialMacro "IfValueTF" ts =+ handleIf (xparseIf (not . isNoValueArg) True True) ts+trySpecialMacro "IfValueT" ts =+ handleIf (xparseIf (not . isNoValueArg) True False) ts+trySpecialMacro "IfValueF" ts =+ handleIf (xparseIf (not . isNoValueArg) False True) ts+trySpecialMacro "IfBooleanTF" ts =+ handleIf (xparseIf isBooleanTrueArg True True) ts+trySpecialMacro "IfBooleanT" ts =+ handleIf (xparseIf isBooleanTrueArg True False) ts+trySpecialMacro "IfBooleanF" ts =+ handleIf (xparseIf isBooleanTrueArg False True) ts+trySpecialMacro "IfBlankTF" ts = handleIf (xparseIf isBlankArg True True) ts+trySpecialMacro "IfBlankT" ts = handleIf (xparseIf isBlankArg True False) ts+trySpecialMacro "IfBlankF" ts = handleIf (xparseIf isBlankArg False True) ts+-- \ProcessList{list}{tokens}: apply tokens to every item of list:+trySpecialMacro "ProcessList" ts = handleIf processListParser ts+-- \UseName{string}: turn string into a csname and execute it:+trySpecialMacro "UseName" ts = handleIf useNameParser ts+-- \ExpandArgs{spec}\cmd{arg1}...: pre-expand the command's+-- arguments as described by the spec:+trySpecialMacro "ExpandArgs" ts = handleIf expandArgsParser ts+-- LaTeX3 expandable evaluators:+trySpecialMacro "inteval" ts = handleEval evalInteval ts+trySpecialMacro "fpeval" ts = handleEval evalFpeval ts+-- dimension expressions are not evaluated; we substitute the+-- expression itself:+trySpecialMacro "dimeval" ts = handleEval (Just . T.strip) ts+trySpecialMacro "skipeval" ts = handleEval (Just . T.strip) ts trySpecialMacro _ _ = mzero +-- | Parse a conditional of the kind used with xparse commands+-- (@\\IfNoValueTF@ etc.): test the first argument and select the+-- true or false branch. The Bool parameters indicate whether a+-- true and a false branch, respectively, are to be parsed.+xparseIf :: PandocMonad m => ([Tok] -> Bool) -> Bool -> Bool -> LP m [Tok]+xparseIf test hasTrueBranch hasFalseBranch = do+ -- spaces and comments may intervene between the arguments:+ let grabArg = withVerbatimMode (spaces *> (braced <|> count 1 anyTok))+ let getBranch cond = if cond+ then grabArg+ else pure []+ arg <- grabArg+ trueToks <- getBranch hasTrueBranch+ falseToks <- getBranch hasFalseBranch+ TokStream _ rest <- getInput+ return $ (if test arg then trueToks else falseToks) ++ rest++isNoValueArg :: [Tok] -> Bool+isNoValueArg toks =+ case filter (not . tokTypeIn [Spaces, Newline, Comment]) toks of+ [Tok _ (CtrlSeq "NoValue") _] -> True+ [Tok _ Symbol "-", Tok _ Word "NoValue", Tok _ Symbol "-"] -> True+ _ -> False++isBooleanTrueArg :: [Tok] -> Bool+isBooleanTrueArg toks =+ case filter (not . tokTypeIn [Spaces, Newline, Comment]) toks of+ [Tok _ (CtrlSeq "BooleanTrue") _] -> True+ _ -> False++-- | An argument is \"blank\" (in the sense of @\\IfBlankTF@) if it+-- is empty or consists only of blanks.+isBlankArg :: [Tok] -> Bool+isBlankArg = all (tokTypeIn [Spaces, Newline, Comment])++-- | Parser for the arguments of @\\ProcessList{list}{tokens}@:+-- apply the tokens to every item (braced group or single token) of+-- the list.+processListParser :: PandocMonad m => LP m [Tok]+processListParser = withVerbatimMode $ do+ pos <- getPosition+ spaces+ list <- braced <|> count 1 anyTok+ spaces+ fn <- braced <|> count 1 anyTok+ TokStream _ rest <- getInput+ let items = unfoldr tokGroup list+ let braceIt item = Tok pos Symbol "{" : item ++ [Tok pos Symbol "}"]+ return $ concatMap (\item -> fn ++ braceIt item) items ++ rest++-- | Parser for the argument of @\\UseName{string}@: turn the+-- string into a control sequence.+useNameParser :: PandocMonad m => LP m [Tok]+useNameParser = do+ pos <- getPosition+ name <- untokenize <$> withVerbatimMode (spaces *> braced)+ TokStream _ rest <- getInput+ return $ Tok pos (CtrlSeq name) ("\\" <> name <> " ") : rest++-- | Parser for the arguments of @\\ExpandArgs{spec}\\cmd{arg1}...@:+-- transform each argument as described by the corresponding letter+-- of the spec (@c@ = turn a string into a control sequence, @n@ =+-- leave a braced argument unchanged, @N@ = leave a single token+-- unchanged), then put the command before the transformed+-- arguments.+expandArgsParser :: PandocMonad m => LP m [Tok]+expandArgsParser = withVerbatimMode $ do+ spec <- T.unpack . untokenize <$> (spaces *> braced)+ spaces+ cmd <- anyTok+ args <- concat <$> mapM transformArg spec+ TokStream _ rest <- getInput+ return $ cmd : args ++ rest+ where+ transformArg 'c' = do+ pos <- getPosition+ name <- untokenize <$> (spaces *> braced)+ return [Tok pos (CtrlSeq name) ("\\" <> name <> " ")]+ transformArg 'n' = do+ pos <- getPosition+ toks <- spaces *> braced+ return $ Tok pos Symbol "{" : toks ++ [Tok pos Symbol "}"]+ transformArg 'N' = spaces *> count 1 anyTok+ transformArg _ = mzero+ ifStrequalParser :: PandocMonad m => LP m [Tok] ifStrequalParser = do str1 <- braced <|> count 1 anyTok@@ -681,15 +983,292 @@ Left _ -> Prelude.fail "Could not parse conditional" Right ts' -> return ts' -ifParser :: PandocMonad m => Bool -> LP m [Tok]-ifParser b = do- ifToks <- many (notFollowedBy (controlSeq "else" <|> controlSeq "fi")- *> anyTok)- elseToks <- (controlSeq "else" >> manyTill anyTok (controlSeq "fi"))- <|> ([] <$ controlSeq "fi")- TokStream _ rest <- getInput- return $ (if b then ifToks else elseToks) ++ rest+-- | Names of TeX's primitive conditionals. While scanning for the+-- @\\else@ or @\\fi@ that ends a conditional branch, we need to+-- know which control sequences begin a conditional, so that the+-- @\\else@ and @\\fi@ belonging to nested conditionals are not+-- mistaken for the end of the current one.+primitiveConditionalNames :: Set.Set Text+primitiveConditionalNames = Set.fromList+ [ "if", "ifcase", "ifcat", "ifcsname", "ifdefined", "ifdim"+ , "ifeof", "iffalse", "iffontchar", "ifhbox", "ifhmode"+ , "ifincsname", "ifinner", "ifmmode", "ifnum", "ifodd", "iftrue"+ , "ifvbox", "ifvmode", "ifvoid", "ifx" ] +-- | Handle a conditional like @\\iftrue@: select the branch before+-- @\\else@ (or @\\fi@) if the Bool is True, the else branch+-- otherwise. The unselected branch is skipped without expansion+-- (as TeX does), keeping nested conditionals balanced.+doIf :: PandocMonad m => Bool -> [Tok] -> LP m [Tok]+doIf b ts = do+ macros <- sMacros <$> getState+ -- Conditionals defined with \newif (or \let from a primitive+ -- conditional) expand to a single conditional token; TeX+ -- recognizes these too when skipping conditional text.+ let isConditionalMacro (Macro _ _ [] Nothing [Tok _ (CtrlSeq n) _]) =+ n `Set.member` primitiveConditionalNames+ isConditionalMacro _ = False+ let conditionals = foldr+ (\m s -> M.foldrWithKey+ (\k v s' -> if isConditionalMacro v+ then Set.insert k s'+ else s')+ s m)+ primitiveConditionalNames macros+ case splitConditional conditionals ts of+ Just (ifToks, elseToks, rest) ->+ return $ (if b then ifToks else elseToks) ++ rest+ Nothing -> Prelude.fail "Could not parse conditional"++-- | Split the tokens following a conditional into the tokens+-- before @\\else@ (or @\\fi@), the tokens of the else branch (if+-- any), and the tokens after the matching @\\fi@. Returns Nothing+-- if there is no matching @\\fi@.+splitConditional :: Set.Set Text -> [Tok] -> Maybe ([Tok], [Tok], [Tok])+splitConditional conditionals = goIf (0 :: Int) id+ where+ goIf _ _ [] = Nothing+ goIf depth acc (t@(Tok _ (CtrlSeq name) _) : rest)+ | name == "fi"+ , depth == 0 = Just (acc [], [], rest)+ | name == "fi" = goIf (depth - 1) (acc . (t:)) rest+ | name == "else"+ , depth == 0 = goElse (acc []) (0 :: Int) id rest+ | name `Set.member` conditionals = goIf (depth + 1) (acc . (t:)) rest+ goIf depth acc (t : rest) = goIf depth (acc . (t:)) rest+ goElse _ _ _ [] = Nothing+ goElse ifToks depth acc (t@(Tok _ (CtrlSeq name) _) : rest)+ | name == "fi"+ , depth == 0 = Just (ifToks, acc [], rest)+ | name == "fi" = goElse ifToks (depth - 1) (acc . (t:)) rest+ | name `Set.member` conditionals =+ goElse ifToks (depth + 1) (acc . (t:)) rest+ goElse ifToks depth acc (t : rest) = goElse ifToks depth (acc . (t:)) rest++-- | Handle a LaTeX3 expandable evaluator (@\inteval@, @\fpeval@,+-- ...): grab the braced argument (macros in it are expanded as it+-- is consumed), evaluate it, and substitute the result. If the+-- expression cannot be evaluated, substitute the expression text+-- itself and report it.+handleEval :: PandocMonad m => (Text -> Maybe Text) -> [Tok] -> LP m [Tok]+handleEval evaluator ts = do+ lstate <- getState+ res <- lift $ runParserT evalParser lstate "eval" $ TokStream False ts+ case res of+ Left _ -> Prelude.fail "Could not parse evaluator argument"+ Right ts' -> return ts'+ where+ evalParser = do+ pos <- getPosition+ arg <- untokenize <$> braced+ TokStream _ rest <- getInput+ case evaluator arg of+ Just result -> return $ tokenize pos result ++ rest+ Nothing -> do+ report $ SkippedContent ("evaluation of " <> arg) pos+ return $ tokenize pos arg ++ rest++-- | Evaluate an integer expression (@\inteval@): @+ - * / ( )@,+-- with division rounding to the nearest integer (ties away from+-- zero, as in eTeX's @\numexpr@).+evalInteval :: Text -> Maybe Text+evalInteval t = do+ (n, rest) <- pIntExpr t+ guard $ T.null (skipWs rest)+ pure $ T.pack (show n)+ where+ pIntExpr s0 = pIntTerm s0 >>= addLoop+ addLoop (acc, s) =+ case T.uncons (skipWs s) of+ Just ('+', s') -> do (y, s'') <- pIntTerm s'+ addLoop (acc + y, s'')+ Just ('-', s') -> do (y, s'') <- pIntTerm s'+ addLoop (acc - y, s'')+ _ -> Just (acc, s)+ pIntTerm s0 = pIntFactor s0 >>= mulLoop+ mulLoop (acc, s) =+ case T.uncons (skipWs s) of+ Just ('*', s') -> do (y, s'') <- pIntFactor s'+ mulLoop (acc * y, s'')+ Just ('/', s') -> do (y, s'') <- pIntFactor s'+ guard $ y /= 0+ mulLoop (divRound acc y, s'')+ _ -> Just (acc, s)+ pIntFactor s0 =+ case T.uncons (skipWs s0) of+ Just ('-', s) -> do (x, s') <- pIntFactor s+ pure (negate x, s')+ Just ('+', s) -> pIntFactor s+ Just ('(', s) -> do+ (x, s') <- pIntExpr s+ case T.uncons (skipWs s') of+ Just (')', s'') -> pure (x, s'')+ _ -> Nothing+ Just (c, _) | isDigit c ->+ let (ds, s) = T.span isDigit (skipWs s0)+ in do n <- safeRead ds+ pure (n :: Integer, s)+ _ -> Nothing+ divRound a b =+ let (q, r) = a `quotRem` b+ in if 2 * abs r >= abs b+ then q + signum a * signum b+ else q++-- | Evaluate a floating point expression (a practical subset of+-- @\fpeval@): @+ - * / ^ ( )@ (also @**@ for @^@) and decimal+-- literals.+evalFpeval :: Text -> Maybe Text+evalFpeval t0 = do+ let t = T.replace "**" "^" t0+ (x, rest) <- pFpExpr t+ guard $ T.null (skipWs rest)+ guard $ not (isNaN x || isInfinite x)+ pure $ formatFp x+ where+ pFpExpr s0 = pFpTerm s0 >>= addLoop+ addLoop (acc, s) =+ case T.uncons (skipWs s) of+ Just ('+', s') -> do (y, s'') <- pFpTerm s'+ addLoop (acc + y, s'')+ Just ('-', s') -> do (y, s'') <- pFpTerm s'+ addLoop (acc - y, s'')+ _ -> Just (acc, s)+ pFpTerm s0 = pFpPow s0 >>= mulLoop+ mulLoop (acc, s) =+ case T.uncons (skipWs s) of+ Just ('*', s') -> do (y, s'') <- pFpPow s'+ mulLoop (acc * y, s'')+ Just ('/', s') -> do (y, s'') <- pFpPow s'+ mulLoop (acc / y, s'')+ _ -> Just (acc, s)+ pFpPow s0 = do+ (x, s) <- pFpFactor s0+ case T.uncons (skipWs s) of+ Just ('^', s') -> do (y, s'') <- pFpPow s' -- right-associative+ pure (x ** y, s'')+ _ -> pure (x, s)+ pFpFactor s0 =+ case T.uncons (skipWs s0) of+ Just ('-', s) -> do (x, s') <- pFpFactor s+ pure (negate x, s')+ Just ('+', s) -> pFpFactor s+ Just ('(', s) -> do+ (x, s') <- pFpExpr s+ case T.uncons (skipWs s') of+ Just (')', s'') -> pure (x, s'')+ _ -> Nothing+ Just (c, _) | isLetter c ->+ let (name, s1) = T.span isLetter (skipWs s0)+ in case name of+ "pi" -> pure (pi, s1)+ "deg" -> pure (pi / 180, s1) -- one degree in radians+ _ ->+ case T.uncons (skipWs s1) of+ Just ('(', s2) -> do+ (args, s3) <- pFpArgs s2+ x <- applyFn name args+ pure (x, s3)+ _ -> do+ -- prefix application without parentheses,+ -- e.g. "sqrt 2":+ (y, s2) <- pFpFactor s1+ x <- applyFn name [y]+ pure (x, s2)+ Just (c, _) | isDigit c || c == '.' ->+ let s = skipWs s0+ (ds, s') = T.span (\d -> isDigit d || d == '.') s+ (expt, s'') = case T.uncons s' of+ Just (e, r) | e == 'e' || e == 'E' ->+ let (sign, r') =+ case T.uncons r of+ Just (sg, rr) | sg == '+' || sg == '-'+ -> (T.singleton sg, rr)+ _ -> ("", r)+ (eds, r'') = T.span isDigit r'+ in if T.null eds+ then ("", s')+ else ("e" <> sign <> eds, r'')+ _ -> ("", s')+ in do x <- safeRead (fixup ds <> expt)+ pure (x :: Double, s'')+ _ -> Nothing+ fixup ds -- make the literal readable for Haskell's 'read'+ | "." `T.isPrefixOf` ds = "0" <> fixup' ds+ | otherwise = fixup' ds+ fixup' ds+ | "." `T.isSuffixOf` ds = ds <> "0"+ | otherwise = ds+ -- comma-separated function arguments, ending with ')':+ pFpArgs s0 = do+ (x, s) <- pFpExpr s0+ case T.uncons (skipWs s) of+ Just (',', s') -> do (xs, s'') <- pFpArgs s'+ pure (x : xs, s'')+ Just (')', s') -> pure ([x], s')+ _ -> Nothing+ applyFn name args =+ case (lookup name unaryFns, args) of+ (Just f, [x]) -> Just (f x)+ _ ->+ case (name, args) of+ ("max", _:_) -> Just (maximum args)+ ("min", _:_) -> Just (minimum args)+ ("atan", [x, y]) -> Just (atan2 x y)+ ("atand", [x, y]) -> Just (unrad (atan2 x y))+ ("round", _) -> rounder (fromInteger . round) args+ ("floor", _) -> rounder (fromInteger . floor) args+ ("ceil", _) -> rounder (fromInteger . ceiling) args+ ("trunc", _) -> rounder (fromInteger . truncate) args+ _ -> Nothing+ -- rounding functions take an optional number of decimal places:+ rounder f [x] = Just (f x)+ rounder f [x, n] | n == fromInteger (round n) =+ let m = 10 ^^ (round n :: Integer)+ -- round to 16 significant digits first (like l3fp), so+ -- that e.g. round(2.345,2) gives 2.34, not 2.35:+ y = read (showEFloat (Just 15) (x * m) "") :: Double+ in Just (f y / m)+ rounder _ _ = Nothing+ rad x = x * pi / 180+ unrad x = x * 180 / pi+ unaryFns :: [(Text, Double -> Double)]+ unaryFns =+ [ ("abs", abs), ("sign", signum), ("sqrt", sqrt)+ , ("exp", exp), ("ln", log)+ , ("fact", \x -> if x >= 0 && x == fromInteger (round x) && x < 171+ then fromInteger (product [1 .. round x])+ else 0 / 0)+ , ("sin", sin), ("cos", cos), ("tan", tan)+ , ("cot", recip . tan), ("sec", recip . cos), ("csc", recip . sin)+ , ("asin", asin), ("acos", acos), ("atan", atan)+ , ("acot", atan . recip), ("asec", acos . recip), ("acsc", asin . recip)+ , ("sind", sin . rad), ("cosd", cos . rad), ("tand", tan . rad)+ , ("cotd", recip . tan . rad), ("secd", recip . cos . rad)+ , ("cscd", recip . sin . rad)+ , ("asind", unrad . asin), ("acosd", unrad . acos)+ , ("atand", unrad . atan), ("acotd", unrad . atan . recip)+ , ("asecd", unrad . acos . recip), ("acscd", unrad . asin . recip)+ ]++formatFp :: Double -> Text+formatFp x0+ | x == fromInteger r && abs x < 1e16 = T.pack (show r)+ | otherwise = T.pack (trimZeros (showFFloat Nothing x ""))+ where+ -- l3fp computes with 16 significant decimal digits; round to+ -- that precision so that e.g. 0.1 + 0.2 yields 0.3.+ x = read (showEFloat (Just 15) x0 "") :: Double+ r = round x+ trimZeros s+ | '.' `elem` s = case dropWhileEnd (== '0') s of+ s' | "." `isSuffixOf` s' -> take (length s' - 1) s'+ | otherwise -> s'+ | otherwise = s++skipWs :: Text -> Text+skipWs = T.dropWhile (\c -> c == ' ' || c == '\t' || c == '\n' || c == '\r')+ startsWithAlphaNum :: Text -> Bool startsWithAlphaNum t = case T.uncons t of@@ -876,10 +1455,7 @@ retokenizeComment :: PandocMonad m => LP m () retokenizeComment = (do Tok pos Comment txt <- satisfyTok isCommentTok- let updPos (Tok pos' toktype' txt') =- Tok (incSourceColumn (incSourceLine pos' (sourceLine pos - 1))- (sourceColumn pos)) toktype' txt'- let newtoks = map updPos $ tokenize pos $ T.tail txt+ let newtoks = tokenize (incSourceColumn pos 1) $ T.tail txt TokStream macrosExpanded ts <- getInput setInput $ TokStream macrosExpanded ((Tok pos Symbol "%" : newtoks) ++ ts)) <|> return ()@@ -898,6 +1474,168 @@ concat <$> manyTill ((snd <$> withRaw (try braced)) <|> count 1 anyTok) (symbol ']') +-- | Tokens between opening and closing delimiter tokens (which are+-- compared by token type and text, ignoring position). Nested+-- delimiter pairs are balanced (when the delimiters differ), and+-- braced groups are skipped, so delimiters inside braces don't count.+delimitedToks :: PandocMonad m => Tok -> Tok -> LP m [Tok]+delimitedToks open close = matchesTok open *> go (1 :: Int)+ where+ matchesTok (Tok _ toktype txt) =+ satisfyTok (\(Tok _ toktype' txt') -> toktype == toktype' && txt == txt')+ go n = (do ts <- snd <$> withRaw (try braced)+ (ts ++) <$> go n)+ <|> (do t <- matchesTok close+ if n == 1+ then return []+ else (t:) <$> go (n - 1))+ <|> (do t <- matchesTok open+ (t:) <$> go (n + 1))+ <|> (do t <- anyTok+ (t:) <$> go n)++-- | A verbatim argument (xparse @v@ specifier): either a braced+-- group or tokens between two identical delimiter characters.+-- The result is a single Word token containing the raw text, so+-- that its contents are not reinterpreted when substituted.+verbatimArg :: PandocMonad m => LP m [Tok]+verbatimArg = try $ do+ optional sp+ toks <- braced <|> delimited+ case toks of+ [] -> pure []+ Tok pos _ _ : _ -> pure [Tok pos Word (untokenize toks)]+ where+ delimited = do+ Tok _ Symbol t <- anySymbol+ marker <- case T.uncons t of+ Just (c, ts) | T.null ts -> return c+ _ -> mzero+ manyTill (notFollowedBy newlineTok >> verbTok marker) (symbol marker)++-- | Convert a verbatim environment body (xparse @c@ specifier) to+-- tokens for substitution. The body is typeset verbatim by LaTeX,+-- with each space rendered as the character in slot 32 of the+-- current font (the visible space U+2423 in typewriter fonts) and+-- each source line on its own line; we emulate this by making each+-- line a single Word token (so contents are not reinterpreted),+-- with spaces replaced by U+2423 and lines separated by @\\\\@.+-- Tabs are kept as-is (LaTeX typesets the raw tab character, not a+-- visible space). Note that the typewriter font is not automatic:+-- it comes from a @\\ttfamily@ or similar in the environment+-- definition. If the first parameter is True, leading and trailing+-- blank lines are trimmed.+verbatimBodyToks :: Bool -> [Tok] -> [Tok]+verbatimBodyToks _ [] = []+verbatimBodyToks doTrim toks@(Tok pos _ _ : _) =+ intercalate [Tok pos (CtrlSeq "\\") "\\\\"] (map lineToks ls)+ where+ lineToks l+ | T.null l = []+ | otherwise = [Tok pos Word (T.map toVisibleSpace l)]+ toVisibleSpace c = if c == ' ' then '\x2423' else c+ ls = (if doTrim+ then dropWhileEnd isBlankLine . dropWhile isBlankLine+ else id) $ T.lines (untokenize toks)+ isBlankLine = T.all (\c -> c == ' ' || c == '\t')++-- | Apply xparse argument processors (from the @>{...}@ modifier)+-- to a grabbed argument. Processors are applied from right to left+-- (i.e., the one nearest the argument specifier first).+applyArgProcessors :: PandocMonad m => [[Tok]] -> [Tok] -> LP m [Tok]+applyArgProcessors [] x = pure x+applyArgProcessors (p:ps) x = applyArgProcessors ps x >>= applyArgProcessor p++applyArgProcessor :: PandocMonad m => [Tok] -> [Tok] -> LP m [Tok]+applyArgProcessor proc x =+ case dropWhile spaceLike proc of+ Tok _ (CtrlSeq "TrimSpaces") _ : _ -> pure $ trimSpaceToks x+ Tok pos (CtrlSeq "ReverseBoolean") _ : _ ->+ pure $ case filter (not . spaceLike) x of+ [Tok _ (CtrlSeq "BooleanTrue") _] ->+ [Tok pos (CtrlSeq "BooleanFalse") "\\BooleanFalse "]+ [Tok _ (CtrlSeq "BooleanFalse") _] ->+ [Tok pos (CtrlSeq "BooleanTrue") "\\BooleanTrue "]+ _ -> x+ Tok pos (CtrlSeq "SplitArgument") _ : ts+ | Just (numtoks, ts') <- tokGroup ts+ , Just numparts <- safeRead (untokenize numtoks)+ , Just (delimtoks, _) <- tokGroup ts'+ , (d : _) <- dropWhile spaceLike delimtoks ->+ let missing = [Tok pos (CtrlSeq "NoValue") "\\NoValue "]+ pieces = take (numparts + 1) $+ map trimSpaceToks (splitToksOn d x) ++ repeat missing+ in pure $ concatMap (braceGroup pos) pieces+ Tok pos (CtrlSeq "SplitList") _ : ts+ | Just (delimtoks, _) <- tokGroup ts+ , (d : _) <- dropWhile spaceLike delimtoks ->+ pure $ concatMap (braceGroup pos) (map trimSpaceToks (splitToksOn d x))+ Tok pos _ _ : _ -> do+ -- unknown (or malformed) processor: keep the argument unprocessed+ report $ SkippedContent ("processor " <> untokenize proc) pos+ pure x+ [] -> pure x+ where+ spaceLike = tokTypeIn [Spaces, Newline, Comment]+ braceGroup pos ts = Tok pos Symbol "{" : ts ++ [Tok pos Symbol "}"]++-- | Remove space tokens at both ends of a token list.+trimSpaceToks :: [Tok] -> [Tok]+trimSpaceToks = dropWhile isSp . dropWhileEnd isSp+ where isSp = tokTypeIn [Spaces, Newline]++-- | Parse a braced group (or a single token) from the beginning of+-- a token list, skipping leading spaces; return the group's+-- contents and the remaining tokens.+tokGroup :: [Tok] -> Maybe ([Tok], [Tok])+tokGroup ts =+ case dropWhile (tokTypeIn [Spaces, Newline, Comment]) ts of+ Tok _ Symbol "{" : rest -> go (1 :: Int) [] rest+ t : rest -> Just ([t], rest)+ [] -> Nothing+ where+ go _ _ [] = Nothing+ go depth acc (t : rest)+ | isSym "{" t = go (depth + 1) (t : acc) rest+ | isSym "}" t = if depth == 1+ then Just (reverse acc, rest)+ else go (depth - 1) (t : acc) rest+ | otherwise = go depth (t : acc) rest+ isSym s (Tok _ Symbol s') = s == s'+ isSym _ _ = False++-- | Split a token list at each depth-0 occurrence of the delimiter+-- token (compared by token type and text).+splitToksOn :: Tok -> [Tok] -> [[Tok]]+splitToksOn (Tok _ dtype dtxt) = go (0 :: Int) []+ where+ go _ acc [] = [reverse acc]+ go depth acc (t@(Tok _ toktype txt) : rest)+ | depth == 0, toktype == dtype, txt == dtxt = reverse acc : go 0 [] rest+ | otherwise =+ let depth' = case t of+ Tok _ Symbol "{" -> depth + 1+ Tok _ Symbol "}" -> max 0 (depth - 1)+ _ -> depth+ in go depth' (t : acc) rest++-- | Any token, but if the token contains @stopchar@, it is split+-- so that the part before @stopchar@ is returned and @stopchar@+-- itself (plus what follows) is left in the input. Used for+-- parsing verbatim text delimited by @stopchar@.+verbTok :: PandocMonad m => Char -> LP m Tok+verbTok stopchar = do+ t@(Tok pos toktype txt) <- anyTok+ case T.findIndex (== stopchar) txt of+ Nothing -> return t+ Just i -> do+ let (t1, t2) = T.splitAt i txt+ TokStream macrosExpanded inp <- getInput+ setInput $ TokStream macrosExpanded+ $ Tok (incSourceColumn pos i) Symbol (T.singleton stopchar)+ : tokenize (incSourceColumn pos (i + 1)) (T.drop 1 t2) ++ inp+ return $ Tok pos toktype t1+ parenWrapped :: PandocMonad m => Monoid a => LP m a -> LP m a parenWrapped parser = try $ do symbol '('@@ -936,23 +1674,16 @@ withRaw :: PandocMonad m => LP m a -> LP m (a, [Tok]) withRaw parser = do- rawTokensMap <- sRawTokens <$> getState- let key = case IntMap.lookupMax rawTokensMap of- Nothing -> 0- Just (n,_) -> n + 1- -- insert empty list at key- updateState $ \st -> st{ sRawTokens =- IntMap.insert key [] $ sRawTokens st }+ startCount <- sRawTokenCount <$> getState+ updateState $ \st -> st{ sRawScopes = sRawScopes st + 1 } result <- parser- mbRevToks <- IntMap.lookup key . sRawTokens <$> getState- raw <- case mbRevToks of- Just revtoks -> do- updateState $ \st -> st{ sRawTokens =- IntMap.delete key $ sRawTokens st}- return $ reverse revtoks- Nothing ->- throwError $ PandocShouldNeverHappenError $- "sRawTokens has nothing at key " <> T.pack (show key)+ st <- getState+ let raw = reverse $ take (sRawTokenCount st - startCount) (sRawTokens st)+ setState $ if sRawScopes st <= 1+ then st{ sRawScopes = 0+ , sRawTokens = []+ , sRawTokenCount = 0 }+ else st{ sRawScopes = sRawScopes st - 1 } return (result, raw) keyval :: PandocMonad m => LP m (Text, Text)
@@ -15,7 +15,7 @@ import qualified Data.Text as T import Control.Applicative ((<|>), optional, many) import Control.Monad (when, void)-import Text.Pandoc.Shared (safeRead, trim)+import Text.Pandoc.Shared (safeRead, trim, compactifyTable) import Text.Pandoc.Logging (LogMessage(SkippedContent)) import Text.Pandoc.Walk (walkM) import Text.Pandoc.Parsing hiding (blankline, many, mathDisplay, mathInline,@@ -190,7 +190,7 @@ -- The parsing of empty cells is important in LaTeX, especially when dealing -- with multirow/multicolumn. See #6603. parseEmptyCell = spaces $> emptyCell- parseSimpleCell = simpleCell <$> (plainify . mconcat <$> many block)+ parseSimpleCell = simpleCell . mconcat <$> many block cellAlignment :: PandocMonad m => LP m Alignment@@ -205,11 +205,6 @@ "*" -> AlignDefault _ -> AlignDefault -plainify :: Blocks -> Blocks-plainify bs = case toList bs of- [Para ils] -> plain (fromList ils)- _ -> bs- multirowCell :: PandocMonad m => LP m Blocks -> LP m Cell multirowCell block = controlSeq "multirow" >> do -- Full prototype for \multirow macro is:@@ -221,7 +216,7 @@ _ <- optional $ symbol '[' *> manyTill anyTok (symbol ']') -- bigstrut-related _ <- symbol '{' *> manyTill anyTok (symbol '}') -- Cell width _ <- optional $ symbol '[' *> manyTill anyTok (symbol ']') -- Length used for fine-tuning- content <- symbol '{' *> (plainify . mconcat <$> many block) <* symbol '}'+ content <- symbol '{' *> (mconcat <$> many block) <* symbol '}' return $ cell AlignDefault (RowSpan nrows) (ColSpan 1) content multicolumnCell :: PandocMonad m => LP m Blocks -> LP m Cell@@ -230,7 +225,7 @@ alignment <- symbol '{' *> cellAlignment <* symbol '}' let singleCell = do- content <- plainify . mconcat <$> many block+ content <- mconcat <$> many block return $ cell alignment (RowSpan 1) (ColSpan span') content -- Two possible contents: either a \multirow cell, or content.@@ -357,7 +352,8 @@ let th = fixTableHead $ TableHead nullAttr header' let tbs = [fixTableBody $ TableBody nullAttr 0 [] rows] let tf = TableFoot nullAttr []- return $ table emptyCaption (zip aligns widths) th tbs tf+ return $ compactifyTable+ $ table emptyCaption (zip aligns widths) th tbs tf addTableCaption :: PandocMonad m => Blocks -> LP m Blocks addTableCaption = walkM go
@@ -33,7 +33,7 @@ import qualified Text.Pandoc.Parsing as P import qualified Data.Foldable as Foldable import qualified Data.Set as Set-import Text.Pandoc.Shared (extractSpaces)+import Text.Pandoc.Shared (extractSpaces, compactifyTable) data ManState = ManState { readerOptions :: ReaderOptions , manLogMessages :: []LogMessage@@ -129,7 +129,8 @@ let widths = if isPlainTable then repeat ColWidthDefault else repeat $ ColWidth (1.0 / fromIntegral (length alignments))- return $ B.table B.emptyCaption (zip alignments widths)+ return $ compactifyTable+ $ B.table B.emptyCaption (zip alignments widths) (TableHead nullAttr $ toHeaderRow headerRow) [TableBody nullAttr 0 [] $ map toRow bodyRows] (TableFoot nullAttr [])) <|> fallback pos
@@ -23,7 +23,7 @@ import Control.Monad import Control.Monad.Except (throwError) import qualified Data.Bifunctor as Bifunctor-import Data.Char (isAlphaNum, isPunctuation, isSpace)+import Data.Char (isAlphaNum, isDigit, isLetter, isPunctuation, isSpace) import Data.List (transpose, elemIndex, sortOn) import qualified Data.List as L import qualified Data.Map as M@@ -53,7 +53,8 @@ import Text.Pandoc.Readers.HTML.TagCategories (voidTags) import Text.Pandoc.Readers.LaTeX (applyMacros, rawLaTeXBlock, rawLaTeXInline) import Text.Pandoc.Shared-import Text.Pandoc.URI (escapeURI, isURI, pBase64DataURI)+import Text.Pandoc.URI (escapeURI, pBase64DataURI)+import Network.URI (isURI) import Text.Pandoc.XML (fromEntities) import Text.Pandoc.Readers.Metadata (yamlBsToMeta, yamlBsToRefs, yamlMetaBlock) -- import Debug.Trace (traceShowId)@@ -328,7 +329,8 @@ kvPair :: PandocMonad m => Bool -> MarkdownParser m (Text, MetaValue) kvPair allowEmpty = try $ do- key <- many1TillChar (alphaNum <|> oneOf "_- ") (char ':')+ key <- takeWhile1P (\c -> isAlphaNum c || c == '_' || c == '-' || c == ' ')+ _ <- char ':' val <- trim <$> manyTillChar anyChar (try $ newline >> lookAhead (blankline <|> nonspaceChar)) guard $ allowEmpty || not (T.null val)@@ -412,7 +414,7 @@ char c notFollowedBy spaces let pEnder = try $ char c >> notFollowedBy (satisfy isAlphaNum)- let regChunk = many1Char (noneOf ['\\','\n','&',c]) <|> litChar+ let regChunk = takeWhile1P (`notElem` ['\\','\n','&',c]) <|> litChar let nestedChunk = (\x -> (c `T.cons` x) `T.snoc` c) <$> quotedTitle c T.unwords . T.words . T.concat <$> manyTill (nestedChunk <|> regChunk) pEnder @@ -464,7 +466,7 @@ rest <- many $ try $ blanklines >> indentSpaces >> rawLines let raw = T.unlines (first:rest) <> "\n" optional blanklines- parsed <- parseFromString' parseBlocks raw+ parsed <- parseFromString' (setPosition pos >> parseBlocks) raw oldnotes <- stateNotes' <$> getState case M.lookup ref oldnotes of Just _ -> logMessage $ DuplicateNoteReference ref pos@@ -575,11 +577,18 @@ skipSpaces return attr +setextUnderline :: PandocMonad m => MarkdownParser m Char+setextUnderline = do+ underlineChar <- oneOf setextHChars+ skipMany (char underlineChar)+ blanklines+ pure underlineChar+ setextHeader :: PandocMonad m => MarkdownParser m (F Blocks) setextHeader = try $ do -- This lookahead prevents us from wasting time parsing Inlines -- unless necessary -- it gives a significant performance boost.- lookAhead $ anyLine >> many1 (oneOf setextHChars) >> blankline+ lookAhead $ anyLine >> setextUnderline skipSpaces (text, raw) <- withRaw $ do oldAllowLineBreaks <- stateAllowLineBreaks <$> getState@@ -589,9 +598,7 @@ updateState $ \st -> st{ stateAllowLineBreaks = oldAllowLineBreaks } return res attr <- setextHeaderEnd- underlineChar <- oneOf setextHChars- many (char underlineChar)- blanklines+ underlineChar <- setextUnderline let level = fromMaybe 0 (elemIndex underlineChar setextHChars) + 1 attr' <- registerHeader attr (runF text defaultParserState) guardDisabled Ext_implicit_header_references@@ -660,7 +667,7 @@ identifierAttr :: PandocMonad m => MarkdownParser m (Attr -> Attr) identifierAttr = try $ do char '#'- result <- T.pack <$> many1 (alphaNum <|> oneOf "-_:.") -- see #7920+ result <- takeWhile1P (\x -> isAlphaNum x || x `elem` ("-_:." :: [Char])) -- see #7920 return $ \(_,cs,kvs) -> (result,cs,kvs) classAttr :: PandocMonad m => MarkdownParser m (Attr -> Attr)@@ -694,7 +701,7 @@ char '{' skipMany spaceChar char '='- format <- many1Char $ satisfy (\c -> isAlphaNum c || c `elem` ['-', '_'])+ format <- takeWhile1P (\c -> isAlphaNum c || c `elem` ['-', '_']) skipMany spaceChar char '}' return format@@ -711,8 +718,8 @@ (Left <$> (guardEnabled Ext_raw_attribute >> try rawAttribute)) <|> (Right <$> do- let pLangId = many1Char . satisfy $ \x ->- x `notElem` ['`', '{', '}'] && not (isSpace x)+ let pLangId = takeWhile1P (\x ->+ x `notElem` ['`', '{', '}'] && not (isSpace x)) mbLanguageId <- optionMaybe (toLanguageId <$> pLangId) skipMany spaceChar mbAttr <- optionMaybe@@ -814,12 +821,13 @@ blockQuote :: PandocMonad m => MarkdownParser m (F Blocks) blockQuote = do+ pos <- getPosition raw <- emailBlockQuote (mbAlert, raw') <- (do guardEnabled Ext_alerts case raw of (t:ts) | "[!" `T.isPrefixOf` t ->- case T.strip t of+ case T.toUpper (T.strip t) of "[!TIP]" -> pure (Just "tip", ts) "[!WARNING]" -> pure (Just "warning", ts) "[!IMPORTANT]" -> pure (Just "important", ts)@@ -829,12 +837,13 @@ _ -> pure (Nothing, raw)) <|> pure (Nothing, raw) -- parse the extracted block, which may contain various block elements:- contents <- parseFromString' parseBlocks $ T.intercalate "\n" raw' <> "\n\n"+ contents <- parseFromString' (setPosition pos >> parseBlocks)+ $ T.intercalate "\n" raw' <> "\n\n" return $ case mbAlert of Nothing -> B.blockQuote <$> contents Just alert ->- (B.divWith ("", [alert], [])+ (B.divWith ("", [alert, "alert"], []) . (B.divWith ("", ["title"], []) (B.para (B.str (T.toTitle alert))) <>)) <$> contents @@ -859,7 +868,7 @@ skipNonindentSpaces notFollowedBy $ string "p." >> spaceChar >> digit -- page number (do guardDisabled Ext_fancy_lists- start <- many1Char digit >>= safeRead+ start <- takeWhile1P isDigit >>= safeRead char '.' gobbleSpaces 1 <|> () <$ lookAhead newline optional $ try (gobbleAtMostSpaces 3 >> notFollowedBy spaceChar)@@ -971,11 +980,12 @@ state <- getState let oldContext = stateParserContext state setState $ state {stateParserContext = ListItemState}+ pos <- getPosition (first, continuationIndent) <- rawListItem fourSpaceRule start continuations <- many (listContinuation continuationIndent) -- parse the extracted block, which may contain various block elements: let raw = T.concat (first:continuations)- contents <- parseFromString' parseBlocks raw+ contents <- parseFromString' (setPosition pos >> parseBlocks) raw updateState (\st -> st {stateParserContext = oldContext}) exts <- getOption readerExtensions return $ B.fromList . taskListItemFromAscii exts . B.toList <$> contents@@ -1016,8 +1026,9 @@ definitionListItem :: PandocMonad m => MarkdownParser m (F (Inlines, [Blocks])) definitionListItem = try $ do+ pos <- getPosition rawLine' <- anyLine- term <- parseFromString' (trimInlinesF <$> inlines) rawLine'+ term <- parseFromString' (setPosition pos >> (trimInlinesF <$> inlines)) rawLine' isTight <- (False <$ blanklines) <|> pure True fourSpaceRule <- (True <$ guardEnabled Ext_four_space_rule) <|> pure False contents <- many1 $ listItem fourSpaceRule defListStart@@ -1028,10 +1039,6 @@ then fmap (fmap (fmap paraToPlain)) else id) (sequence contents)) -paraToPlain :: Block -> Block-paraToPlain (Para ils) = Plain ils-paraToPlain x = x- definitionList :: PandocMonad m => MarkdownParser m (F Blocks) definitionList = try $ do guardEnabled Ext_definition_lists@@ -1223,8 +1230,9 @@ lineBlock = do guardEnabled Ext_line_blocks try $ do+ pos <- getPosition lines' <- lineBlockLines >>=- mapM (parseFromString' (trimInlinesF <$> inlines))+ mapM (parseFromString' (setPosition pos >> (trimInlinesF <$> inlines))) return $ B.lineBlock <$> sequence lines' --@@ -1311,8 +1319,9 @@ tableLine :: PandocMonad m => [Int] -> MarkdownParser m (F [Blocks])-tableLine indices = rawTableLine indices >>=- fmap sequence . mapM (parseFromString' (mconcat <$> many plain))+tableLine indices = do+ raw <- rawTableLine indices+ sequence <$> mapM (parseFromString' (mconcat <$> many plain)) raw -- Parse a multiline table row and return a list of blocks (columns). multilineRow :: PandocMonad m@@ -1397,7 +1406,7 @@ then [] else map (T.unlines . map trim) rawHeadsList heads <- fmap sequence $- mapM (parseFromString' (mconcat <$> many plain).trim) rawHeads+ mapM (parseFromString' (mconcat <$> many plain) . trim) rawHeads return (fmap (:[]) heads, aligns, indices') -- Parse a grid table: starts with row of '-' on top, then header@@ -1533,7 +1542,7 @@ return $ do caption' <- caption (TableComponents _attr _capt colspecs th tb tf) <- tableComponents- return $ B.tableWith attr+ return $ compactifyTable $ B.tableWith attr (B.simpleCaption $ B.plain caption') colspecs th tb tf --@@ -1647,8 +1656,8 @@ skipSpaces result <- trim . T.concat <$> manyTill- ( many1Char (noneOf "`\n")- <|> many1Char (char '`')+ ( takeWhile1P (\c -> c /= '`' && c /= '\n')+ <|> takeWhile1P (== '`') <|> (char '\n' >> notFollowedBy (inList >> listStart) >> notFollowedBy' blankline@@ -1683,7 +1692,7 @@ guardDisabled Ext_intraword_underscores <|> guard (c == '*') <|> (guard =<< notAfterString)- cs <- many1Char (char c)+ cs <- takeWhile1P (== c) (return (B.str cs) <>) <$> whitespace <|> case T.length cs of@@ -1783,7 +1792,7 @@ mmdShortSubscript = try $ do guardEnabled Ext_short_subsuperscripts char '~'- result <- T.pack <$> many1 alphaNum+ result <- takeWhile1P isAlphaNum return $ return $ B.str result whitespace :: PandocMonad m => MarkdownParser m (F Inlines)@@ -1798,7 +1807,7 @@ str :: PandocMonad m => MarkdownParser m (F Inlines) str = do !result <- mconcat <$> many1- ( T.pack <$> (many1 alphaNum)+ ( takeWhile1P isAlphaNum <|> "." <$ try (char '.' <* notFollowedBy (char '.')) ) updateLastStrPos (do guardEnabled Ext_smart@@ -1862,7 +1871,8 @@ try parenthesizedChars <|> (notFollowedBy (oneOf "\n\r )") >> litChar) <|> (lookAhead (oneOf "\n\r") >> notFollowedBy linkTitle' >> litChar)- <|> try (many1Char spaceChar <* notFollowedBy (oneOf "\"')"))+ <|> try (takeWhile1P (\x -> x == ' ' || x == '\t')+ <* notFollowedBy (oneOf "\"')")) let sourceURL = T.unwords . T.words . T.concat <$> many urlChunk src <- try (litBetween '<' '>') <|> try base64DataURI <|> sourceURL tit <- option "" linkTitle'@@ -1872,13 +1882,21 @@ base64DataURI :: PandocMonad m => ParsecT Sources s m Text base64DataURI = do- Sources ((pos, txt):rest) <- getInput- let r = A.parse (fst <$> A.match pBase64DataURI) txt- case r of- A.Done remaining consumed -> do- let pos' = incSourceColumn pos (T.length consumed)- setInput $ Sources ((pos', remaining):rest)- return consumed+ inp <- getInput+ case inp of+ Sources ((pos, txt):rest) ->+ -- feed mempty to force a result if attoparsec returns Partial:+ case A.feed (A.parse (fst <$> A.match pBase64DataURI) txt) mempty of+ A.Done remaining consumed -> do+ -- keep the chunk's position unchanged: chunk positions are+ -- invariant (see uncons in T.P.Sources), and withRaw's+ -- sourcesDifference relies on this. Instead, advance parsec's+ -- own position (a data URI cannot contain newlines):+ setInput $ Sources ((pos, remaining):rest)+ curPos <- getPosition+ setPosition $ incSourceColumn curPos (T.length consumed)+ return consumed+ _ -> mzero _ -> mzero linkTitle :: PandocMonad m => MarkdownParser m Text@@ -1917,7 +1935,8 @@ bracketedSpan = do guardEnabled Ext_bracketed_spans try $ do- (lab,_) <- reference+ guardDisabled Ext_footnotes <|> notFollowedBy' noteMarker+ lab <- inBalancedBrackets inlines attr <- attributes return $ wrapSpan attr <$> lab @@ -2021,6 +2040,20 @@ bareURL = do guardEnabled Ext_autolink_bare_uris getState >>= guard . stateAllowLinks+ -- Fast rejection: a bare URI must contain ':' (after the scheme) and+ -- an email address '@', in both cases before any whitespace, since+ -- neither can contain whitespace. So if the whitespace-delimited+ -- token ahead contains neither ':' nor '@', both parsers must fail.+ -- (If the token extends beyond the current input chunk, we skip the+ -- check and just try the parsers.)+ inp <- getInput+ case unSources inp of+ (_,t):_ ->+ case T.find (\c -> isSpace c || c == ':' || c == '@') t of+ Just ':' -> return ()+ Just '@' -> return ()+ _ -> mzero+ [] -> return () try $ do (cls, (orig, src)) <- (("uri",) <$> uri) <|> (("email",) <$> emailAddress) notFollowedBy $ try $ spaces >> htmlTag (~== TagClose ("a" :: Text))@@ -2050,7 +2083,9 @@ isFragment = T.take 1 path == "#" path' = T.unpack path isAbsolutePath = Posix.isAbsolute path' || Windows.isAbsolute path'- in if T.null path || isFragment || isAbsolutePath || isURI path+ in if T.null path || isFragment || isAbsolutePath || isURI (T.unpack path)+ -- note: we use Network.URI.isURI instead of T.P.URI.isURI+ -- because it doesn't whitelist schemes; see #11858. then path else case takeDirectory fp of@@ -2121,7 +2156,7 @@ rawConTeXtEnvironment = try $ do string "\\start" completion <- inBrackets (letter <|> digit <|> spaceChar)- <|> many1Char letter+ <|> takeWhile1P isLetter !contents <- manyTill (rawConTeXtEnvironment <|> countChar 1 anyChar) (try $ string "\\stop" >> textStr completion) return $! "\\start" <> completion <> T.concat contents <> "\\stop" <> completion@@ -2174,7 +2209,9 @@ string ":::" skipMany (char ':') skipMany spaceChar- attribs <- attributes <|> ((\x -> ("",[x],[])) <$> many1Char nonspaceChar)+ attribs <- attributes <|> ((\x -> ("",[x],[])) <$>+ takeWhile1P (\x -> x /= ' ' && x /= '\t' &&+ x /= '\n' && x /= '\r')) skipMany spaceChar skipMany (char ':') blankline@@ -2219,7 +2256,7 @@ guardEnabled Ext_emoji try $ do char ':'- emojikey <- many1Char (alphaNum <|> oneOf "_+-")+ emojikey <- takeWhile1P (\x -> isAlphaNum x || x `elem` ("_+-" :: [Char])) char ':' case emojiToInline emojikey of Just i -> return (return $ B.singleton i)
@@ -42,7 +42,7 @@ import Text.Parsec (modifyState) import qualified Text.Pandoc.Parsing as P import qualified Data.Foldable as Foldable-import Text.Pandoc.Shared (stringify)+import Text.Pandoc.Shared (stringify, stringifyInlines) #if !MIN_VERSION_base(4,19,0) unsnoc :: [a] -> Maybe ([a], a)@@ -270,7 +270,7 @@ (Macro m _) <- lookAhead $ macro "Sh" <|> macro "Ss" txt <- lineEnclosure m id let lvl = if m == "Sh" then 1 else 2- when (lvl == 1) $ modifyState $ \s -> s{currentSection = (shToSectionMode . stringify) txt}+ when (lvl == 1) $ modifyState $ \s -> s{currentSection = (shToSectionMode . stringifyInlines) txt} return $ B.header lvl txt parseNameSection :: PandocMonad m => MdocParser m Blocks@@ -474,7 +474,7 @@ return $ openDelim <> xform inlines <> closeDelim codeLikeInline' :: PandocMonad m => T.Text -> T.Text -> MdocParser m Inlines-codeLikeInline' nm cl = simpleInline nm (eliminateEmpty (B.codeWith (cls cl) . stringify))+codeLikeInline' nm cl = simpleInline nm (eliminateEmpty (B.codeWith (cls cl) . stringifyInlines)) codeLikeInline :: PandocMonad m => T.Text -> MdocParser m Inlines codeLikeInline nm = codeLikeInline' nm nm@@ -671,7 +671,7 @@ parseMt :: PandocMonad m => MdocParser m Inlines parseMt = simpleInline "Mt" mailto where mailto x | null x = B.link ("mailto:~") "" "~"- | otherwise = B.link ("mailto:" <> stringify x) "" x+ | otherwise = B.link ("mailto:" <> stringifyInlines x) "" x parsePa :: PandocMonad m => MdocParser m Inlines parsePa = simpleInline "Pa" p@@ -713,7 +713,7 @@ parseAr :: PandocMonad m => MdocParser m Inlines parseAr = simpleInline "Ar" ar where ar x | null x = B.codeWith (cls "variable") "file ..."- | otherwise = B.codeWith (cls "variable") $ stringify x+ | otherwise = B.codeWith (cls "variable") $ stringifyInlines x parseCm :: PandocMonad m => MdocParser m Inlines@@ -729,7 +729,7 @@ parseCd = codeLikeInline "Cd" parseQl :: PandocMonad m => MdocParser m Inlines-parseQl = lineEnclosure "Ql" $ B.codeWith (cls "Ql") . stringify+parseQl = lineEnclosure "Ql" $ B.codeWith (cls "Ql") . stringifyInlines parseDq :: PandocMonad m => MdocParser m Inlines parseDq = lineEnclosure "Dq" B.doubleQuoted@@ -785,7 +785,7 @@ parseDl :: PandocMonad m => MdocParser m Blocks parseDl = do inner <- lineEnclosure "Dl" id- return $ B.codeBlock (stringify inner)+ return $ B.codeBlock (stringifyInlines inner) parseD1 :: PandocMonad m => MdocParser m Blocks parseD1 = do@@ -801,7 +801,7 @@ (Just nm, x) | null x -> op <> ok nm <> cl (_, x) ->- op <> (ok . stringify) x <> cl+ op <> (ok . stringifyInlines) x <> cl where ok = B.codeWith (cls "Nm") @@ -1193,7 +1193,7 @@ eol lns <- many $ Just . toString <$> (str <|> blank) <|> Nothing <$ parseSmToggle- <|> Just . stringify <$> parseInline+ <|> Just . stringifyInlines <$> parseInline <|> Just "" <$ emptyMacro "Pp" return $ B.codeBlock (T.unlines (catMaybes lns)) @@ -1218,7 +1218,7 @@ emptyMacro "Ef" return $ xform ins where- code = B.code . stringify+ code = B.code . stringifyInlines skipListArgument :: (PandocMonad m) => MdocParser m () skipListArgument =@@ -1307,7 +1307,7 @@ referenceField m field = do macro m reference <- currentReference <$> getState- contents <- stringify <$> litsAndDelimsToInlines+ contents <- stringifyInlines <$> litsAndDelimsToInlines eol modifyState $ \s -> s{currentReference = M.insertWith (++) field [contents] reference} return ()
@@ -133,7 +133,9 @@ anyText = escape <|> regularText <|> quoteChar <|> spaceTabChar regularText :: PandocMonad m => Lexer m T.Text-regularText = many1Char $ noneOf "\n\r\t \\\""+regularText = takeWhile1P (\c -> not (c == '\n' || c == '\r' ||+ c == '\t' || c == ' ' ||+ c == '\\' || c == '"')) quoteChar :: PandocMonad m => Lexer m T.Text quoteChar = T.singleton <$> char '"'@@ -142,7 +144,7 @@ mdocToken = lexComment <|> lexControlLine <|> lexTextLine lexMacroName :: PandocMonad m => Lexer m T.Text-lexMacroName = many1Char (satisfy isMacroChar)+lexMacroName = takeWhile1P isMacroChar where isMacroChar '%' = True isMacroChar x = isAlphaNum x
@@ -37,8 +37,8 @@ import Text.Pandoc.Options import Text.Pandoc.Parsing hiding (tableCaption) import Text.Pandoc.Readers.HTML (htmlTag, isCommentTag, toAttr)-import Text.Pandoc.Shared (formatCode, safeRead, splitTextBy, stringify,- stripTrailingNewlines, trim, tshow)+import Text.Pandoc.Shared (formatCode, safeRead, splitTextBy, stringifyInlines,+ stripTrailingNewlines, trim, tshow, compactifyTable) import Text.Pandoc.XML (fromEntities) -- | Read mediawiki from an input string and return a Pandoc document.@@ -94,8 +94,31 @@ specialChars :: [Char] specialChars = "'[]<=&*{}|\":\\_" -spaceChars :: [Char]-spaceChars = " \n\t"+-- | True for characters that terminate a run of plain text, i.e. the+-- 'specialChars' plus space, newline and tab. Written as a @case@ so that+-- GHC compiles it to a jump table; this is applied to every single+-- character of the input, so a linear scan over the list here is a+-- significant cost. Must be kept in sync with 'specialChars'.+isStrEnd :: Char -> Bool+isStrEnd c = case c of+ '\t' -> True+ '\n' -> True+ ' ' -> True+ '"' -> True+ '&' -> True+ '\'' -> True+ '*' -> True+ ':' -> True+ '<' -> True+ '=' -> True+ '[' -> True+ '\\' -> True+ ']' -> True+ '_' -> True+ '{' -> True+ '|' -> True+ '}' -> True+ _ -> False sym :: PandocMonad m => Text -> MWParser m () sym s = () <$ try (string $ T.unpack s)@@ -299,7 +322,8 @@ else ([], hdr:rows') let toRow = Row nullAttr toHeaderRow l = [toRow l | not (null l)]- return $ B.table (B.simpleCaption $ B.plain caption)+ return $ compactifyTable+ $ B.table (B.simpleCaption $ B.plain caption) cellspecs (TableHead nullAttr $ toHeaderRow headers) [TableBody nullAttr 0 [] $ map toRow rows]@@ -324,7 +348,7 @@ char '=' skipMany spaceChar v <- (char '"' >> manyTillChar (satisfy (/='\n')) (char '"'))- <|> many1Char (satisfy $ \c -> not (isSpace c) && c /= '|')+ <|> takeWhile1P (\c -> not (isSpace c) && c /= '|') return (k,v) tableStart :: PandocMonad m => MWParser m ()@@ -406,7 +430,8 @@ string "{{" notFollowedBy (char '{') lookAhead $ letter <|> digit <|> char ':'- let chunk = template <|> variable <|> many1Char (noneOf "{}") <|> countChar 1 anyChar+ let chunk = template <|> variable <|> takeWhile1P (\c -> c /= '{' && c /= '}')+ <|> countChar 1 anyChar contents <- manyTill chunk (try $ string "}}") return $ "{{" <> T.concat contents <> "}}" @@ -614,7 +639,7 @@ <|> special str :: PandocMonad m => MWParser m Inlines-str = B.str <$> many1Char (noneOf $ specialChars ++ spaceChars)+str = B.str <$> takeWhile1P (not . isStrEnd) math :: PandocMonad m => MWParser m Inlines math = (B.displayMath . trim <$> try (many1 (char ':') >> textInTags "math"))@@ -711,9 +736,9 @@ image = try $ do sym "[[" imageIdentifier- fname <- addUnderscores <$> many1Char (noneOf "|]")+ fname <- addUnderscores <$> takeWhile1P (\c -> c /= '|' && c /= ']') _ <- many imageOption- dims <- try (char '|' *> sepBy (manyChar digit) (char 'x') <* string "px")+ dims <- try (char '|' *> sepBy (takeWhileP isDigit) (char 'x') <* string "px") <|> return [] _ <- many imageOption let kvs = case dims of@@ -723,7 +748,7 @@ let attr = ("", [], kvs) caption <- (B.str fname <$ sym "]]") <|> try (char '|' *> (mconcat <$> manyTill inline (sym "]]")))- return $ B.imageWith attr fname (stringify caption) caption+ return $ B.imageWith attr fname (stringifyInlines caption) caption imageOption :: PandocMonad m => MWParser m Text imageOption = try $ char '|' *> opt@@ -744,7 +769,7 @@ internalLink :: PandocMonad m => MWParser m Inlines internalLink = try $ do sym "[["- pagename <- T.unwords . T.words <$> manyChar (noneOf "|]")+ pagename <- T.unwords . T.words <$> takeWhileP (\c -> c /= '|' && c /= ']') label <- option (B.text pagename) $ char '|' *> ( (mconcat <$> many1 (notFollowedBy (char ']') *> inline)) -- the "pipe trick"@@ -752,8 +777,8 @@ <|> return (B.text $ T.drop 1 $ T.dropWhile (/=':') pagename) ) sym "]]" -- see #8525:- linktrail <- B.text <$> manyChar (satisfy (\c -> isLetter c && not (isCJK c)))- let link = B.linkWith (mempty, ["wikilink"], mempty) (addUnderscores pagename) (stringify label) (label <> linktrail)+ linktrail <- B.text <$> takeWhileP (\c -> isLetter c && not (isCJK c))+ let link = B.linkWith (mempty, ["wikilink"], mempty) (addUnderscores pagename) (stringifyInlines label) (label <> linktrail) if "Category:" `T.isPrefixOf` pagename then do updateState $ \st -> st{ mwCategoryLinks = link : mwCategoryLinks st }
@@ -1,5 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-} {- | Module : Text.Pandoc.Readers.Metadata Copyright : Copyright (C) 2006-2024 John MacFarlane@@ -20,24 +21,22 @@ import Control.Monad.Except (throwError) import qualified Data.ByteString as B+import qualified Data.List as L import qualified Data.Map as M import qualified Data.Vector as V import Data.Text (Text) import qualified Data.Text as T-import qualified Data.Yaml as Yaml-import qualified Data.Yaml.Internal as Yaml-import qualified Text.Libyaml as Y-import Data.Aeson (Value(..), Object, Result(..), fromJSON, (.:?), withObject,- FromJSON)-import Data.Aeson.Types (formatRelativePath, parse)+import qualified Data.List.NonEmpty as NE+import qualified Yamlet as Y+import Yamlet.Aeson ()+import Data.Aeson (Value(..), Object, Result(..), fromJSON, (.:?), withObject)+import Data.Aeson.Types (parse) import Text.Pandoc.Shared (tshow, blocksToInlines)-import Text.Pandoc.Class (PandocMonad (..), report)+import Text.Pandoc.Class (PandocMonad (..)) import Text.Pandoc.Definition import Text.Pandoc.Error-import Text.Pandoc.Logging (LogMessage(YamlWarning)) import Text.Pandoc.Parsing hiding (tableWith, parse) import qualified Text.Pandoc.UTF8 as UTF8-import System.IO.Unsafe (unsafePerformIO) yamlBsToMeta :: (PandocMonad m, HasLastStrPosition st) => ParsecT Sources st m (Future st MetaValue)@@ -45,37 +44,28 @@ -> ParsecT Sources st m (Future st Meta) yamlBsToMeta pMetaValue bstr = do pos <- getPosition- case decodeAllWithWarnings bstr of- Right (warnings, xs) -> do- mapM_ (\(Yaml.DuplicateKey jpath) ->- report (YamlWarning pos $ "Duplicate key: " <>- T.pack (formatRelativePath jpath)))- warnings+ case Y.decodeAll @Value bstr of+ Right xs -> case xs of (Object o : _) -> fmap Meta <$> yamlMap pMetaValue o [Null] -> return . return $ mempty [] -> return . return $ mempty _ -> Prelude.fail "expected YAML object"- Left err' -> do- let msg = T.pack $ "Error parsing YAML metadata at " <>- show pos <> ":\n" <>- Yaml.prettyPrintParseException err'- throwError $ PandocParseError $- if "did not find expected key" `T.isInfixOf` msg- then msg <>- "\nConsider enclosing the entire field in 'single quotes'"- else msg+ Left errs -> throwError $ PandocParseError $ T.pack $+ "Error parsing YAML metadata at " <> show pos <> ":\n" <>+ renderYamlErrors pos errs -decodeAllWithWarnings :: FromJSON a- => B.ByteString- -> (Either Yaml.ParseException ([Yaml.Warning], [a]))-decodeAllWithWarnings = either Left (\(ws,res)- -> case res of- Left s -> Left (Yaml.AesonException s)- Right v -> Right (ws, v))- . unsafePerformIO- . Yaml.decodeAllHelper- . Y.decode+-- | Render the errors with their lines in the source, given the position of+-- the first line of the YAML input.+renderYamlErrors :: SourcePos -> NE.NonEmpty Y.Error -> String+renderYamlErrors pos =+ L.intercalate "\n" . map (Y.prettyError (sourceName pos) . shift) . NE.toList+ where+ shift :: Y.Error -> Y.Error+ shift err@Y.Error{Y.location = loc@Y.Location{Y.line = l}}+ -- Line 0 means that the error has no position.+ | l == 0 = err+ | otherwise = err{Y.location = loc{Y.line = l + sourceLine pos - 1}} -- Returns filtered list of references. yamlBsToRefs :: (PandocMonad m, HasLastStrPosition st)@@ -83,8 +73,9 @@ -> (Text -> Bool) -- ^ Filter for id -> B.ByteString -> ParsecT Sources st m (Future st [MetaValue])-yamlBsToRefs pMetaValue idpred bstr =- case Yaml.decodeAllEither' bstr of+yamlBsToRefs pMetaValue idpred bstr = do+ pos <- getPosition+ case Y.decodeAll @Value bstr of Right (Object m : _) -> do case parse (withObject "metadata" (.:? "references")) (Object m) of Success (Just refs) -> sequence <$>@@ -94,8 +85,8 @@ let refs = filter hasSelectedId $ V.toList v sequence <$> mapM (yamlToMetaValue pMetaValue) (filter hasSelectedId refs) Right _ -> return . return $ []- Left err' -> throwError $ PandocParseError- $ T.pack $ Yaml.prettyPrintParseException err'+ Left errs -> throwError $ PandocParseError+ $ T.pack $ renderYamlErrors pos errs where isSelected (String t) = idpred t isSelected _ = False
@@ -22,6 +22,7 @@ import Control.Monad.Reader import Control.Monad.Except (throwError) import Data.Bifunctor+import Data.Char (isAlphaNum, isDigit, isLetter) import Data.Default import Data.List (transpose) import qualified Data.Map as M@@ -36,7 +37,7 @@ import Text.Pandoc.Logging import Text.Pandoc.Options import Text.Pandoc.Parsing-import Text.Pandoc.Shared (trimr, tshow)+import Text.Pandoc.Shared (trimr, tshow, compactifyTable) -- | Read Muse from an input string and return a Pandoc document. readMuse :: (PandocMonad m, ToSources a)@@ -170,7 +171,7 @@ where attr = try $ (,) <$ many1 spaceChar- <*> many1Char (noneOf "=\n")+ <*> takeWhile1P (\c -> c /= '=' && c /= '\n') <* string "=\"" <*> manyTillChar (noneOf "\"") (char '"') @@ -198,7 +199,7 @@ -- While not documented, Emacs Muse allows "-" in directive name parseDirectiveKey :: PandocMonad m => MuseParser m Text-parseDirectiveKey = char '#' *> manyChar (letter <|> char '-')+parseDirectiveKey = char '#' *> takeWhileP (\c -> isLetter c || c == '-') parseEmacsDirective :: PandocMonad m => MuseParser m (Text, F Inlines) parseEmacsDirective = (,)@@ -645,6 +646,7 @@ museToPandocTable :: MuseTable -> Blocks museToPandocTable (MuseTable caption headers body footers) =+ compactifyTable $ B.table (B.simpleCaption $ B.plain caption) attrs (TableHead nullAttr $ toHeaderRow headRow)@@ -723,10 +725,29 @@ tableParseRow :: PandocMonad m => Int -- ^ Number of separator characters -> MuseParser m (F [Blocks])-tableParseRow n = try $ sequence <$> tableCells+tableParseRow n = try $ do+ -- A table row must contain a cell separator (whitespace followed by+ -- pipes), which cannot span lines. Scanning the raw line for one+ -- before parsing cells avoids expensive inline parsing (that would+ -- fail and be discarded) of every line this parser is tried on.+ lineMayBeRow <- rawLineContainsSeparator+ guard lineMayBeRow+ sequence <$> tableCells where tableCells = (:) <$> tableCell sep <*> (tableCells <|> fmap pure (tableCell eol)) tableCell p = try $ fmap B.plain . trimInlinesF . mconcat <$> manyTill inline' p sep = try $ many1 spaceChar *> count n (char '|') *> lookAhead (void (many1 spaceChar) <|> void eol)+ pipes = T.replicate n "|"+ rawLineContainsSeparator = do+ Sources inps <- getInput+ return $ case inps of+ (_,t):rest ->+ case T.break (== '\n') t of+ (this, remainder)+ | T.null remainder, not (null rest) ->+ True -- line may span input chunks; don't reject+ | otherwise -> (" " <> pipes) `T.isInfixOf` this ||+ ("\t" <> pipes) `T.isInfixOf` this+ [] -> False -- | Parse a table header row. tableParseHeader :: PandocMonad m => MuseParser m (F MuseTableElement)@@ -751,6 +772,7 @@ inline' :: PandocMonad m => MuseParser m (F Inlines) inline' = whitespace+ <|> str -- tried early: all other alternatives start with non-alphanumerics <|> br <|> anchor <|> footnote@@ -773,7 +795,6 @@ <|> codeTag <|> mathTag <|> inlineLiteralTag- <|> str <|> asterisks <|> symbol <?> "inline"@@ -790,7 +811,7 @@ <$ firstColumn <* char '#' <*> letter- <*> manyChar (letter <|> digit <|> char '-')+ <*> takeWhileP (\c -> isLetter c || isDigit c || c == '-') anchor :: PandocMonad m => MuseParser m (F Inlines) anchor = try $ do@@ -931,13 +952,13 @@ <*> manyTillChar anyChar (closeTag "literal") str :: PandocMonad m => MuseParser m (F Inlines)-str = return . B.str <$> many1Char alphaNum <* updateLastStrPos+str = return . B.str <$> takeWhile1P isAlphaNum <* updateLastStrPos -- | Consume asterisks that were not used as emphasis opening. -- This prevents series of asterisks from being split into -- literal asterisk and emphasis opening. asterisks :: PandocMonad m => MuseParser m (F Inlines)-asterisks = pure . B.str <$> many1Char (char '*')+asterisks = pure . B.str <$> takeWhile1P (== '*') symbol :: PandocMonad m => MuseParser m (F Inlines) symbol = pure . B.str . T.singleton <$> nonspaceChar@@ -986,6 +1007,6 @@ return (ext, width, align) imageAttrs = (,) <$ many1 spaceChar- <*> optionMaybe (many1Char digit)+ <*> optionMaybe (takeWhile1P isDigit) <* many spaceChar <*> optionMaybe (oneOf "rlf")
@@ -11,10 +11,11 @@ Entry point to the odt reader. -} -module Text.Pandoc.Readers.ODT ( readODT ) where+module Text.Pandoc.Readers.ODT ( readODT, readFODT ) where import Codec.Archive.Zip import Text.Pandoc.XML.Light+import qualified Text.Pandoc.XML.Light as XML import Text.Pandoc.Walk import Data.Char (isDigit)@@ -22,9 +23,11 @@ import System.FilePath +import Control.Monad (unless) import Control.Monad.Except (throwError) import qualified Data.Text as T+import qualified Data.Text.Lazy as TL import Text.Pandoc.Class.PandocMonad (PandocMonad) import qualified Text.Pandoc.Class.PandocMonad as P@@ -40,6 +43,7 @@ import Text.Pandoc.Readers.ODT.Generic.Fallible import Text.Pandoc.Readers.ODT.Generic.XMLConverter import Text.Pandoc.Shared (filteredFilesFromArchive)+import Text.Pandoc.Sources (ToSources(..), sourcesToText) readODT :: PandocMonad m => ReaderOptions@@ -51,6 +55,41 @@ return $ walk makeFigure doc Left e -> throwError e +-- | Read a flat OpenDocument text file, i.e. ODF's representation as a+-- single XML file rather than a zip archive.+readFODT :: (PandocMonad m, ToSources a)+ => ReaderOptions+ -> a+ -> m Pandoc+readFODT opts inp = do+ root <- either (throwError . PandocXMLError "") pure+ (parseXMLElement (TL.fromStrict (sourcesToText (toSources inp))))+ case elementToODT opts root of+ Right (doc, mb) -> do+ P.setMediaBag mb+ return $ walk makeFigure doc+ Left e -> throwError e++-- | A flat file has exactly one of each of the top-level elements that a+-- package distributes over @content.xml@ and @styles.xml@, and they are+-- all children of the root. The converter addresses everything relative+-- to the element it is given, so it can simply be handed that root.+elementToODT :: ReaderOptions -> Element -> Either PandocError (Pandoc, MediaBag)+elementToODT opts root = do+ -- the local name only, consistently with the URI-based namespace+ -- matching downstream; office:document-content is correctly rejected+ unless (XML.qName (XML.elName root) == "document") $+ Left $ PandocParseError+ "Expected office:document at the root of the flat OpenDocument file"+ styles <- either+ (\_ -> Left $ PandocParseError "Could not read styles")+ Right+ (readStylesAt root)+ -- the media list is empty: all media arrives in office:binary-data+ either (\_ -> Left $ PandocParseError "Could not convert opendocument") Right+ (runConverter read_body+ (readerState (isEnabled Ext_styles opts) styles []) root)+ -- the ODT parser uses old-style figures: an image with title beginning -- "fig:" in a paragraph by itself. Convert these to new Figure elements. makeFigure :: Block -> Block@@ -70,20 +109,14 @@ readODT' :: ReaderOptions -> B.ByteString -> Either PandocError (Pandoc, MediaBag)-readODT' _ bytes = bytesToODT bytes-- of--- Right (pandoc, mediaBag) -> Right (pandoc , mediaBag)--- Left err -> Left err-----bytesToODT :: B.ByteString -> Either PandocError (Pandoc, MediaBag)-bytesToODT bytes = case toArchiveOrFail bytes of- Right archive -> archiveToODT archive+readODT' opts bytes = case toArchiveOrFail bytes of+ Right archive -> archiveToODT opts archive Left err -> Left $ PandocParseError $ "Could not unzip ODT: " <> T.pack err ---archiveToODT :: Archive -> Either PandocError (Pandoc, MediaBag)-archiveToODT archive = do+archiveToODT :: ReaderOptions -> Archive -> Either PandocError (Pandoc, MediaBag)+archiveToODT opts archive = do let onFailure msg Nothing = Left $ PandocParseError msg onFailure _ (Just x) = Right x contentEntry <- onFailure "Could not find content.xml"@@ -101,9 +134,9 @@ let (dir, name) = splitFileName fp in (dir == "Pictures/") || (dir /= "./" && name == "content.xml") let media = filteredFilesFromArchive archive filePathIsODTMedia- let startState = readerState styles media+ let startState = readerState (isEnabled Ext_styles opts) styles media either (\_ -> Left $ PandocParseError "Could not convert opendocument") Right- (runConverter' read_body startState contentElem)+ (runConverter read_body startState contentElem) --
@@ -1,153 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE TupleSections #-}-{- |- Module : Text.Pandoc.Readers.ODT.Arrows.State- Copyright : Copyright (C) 2015 Martin Linnemann- License : GNU GPL, version 2 or above-- Maintainer : Martin Linnemann <theCodingMarlin@googlemail.com>- Stability : alpha- Portability : portable--An arrow that transports a state. It is in essence a more powerful version of-the standard state monad. As it is such a simple extension, there are-other version out there that do exactly the same.-The implementation is duplicated, though, to add some useful features.-Most of these might be implemented without access to innards, but it's much-faster and easier to implement this way.--}--module Text.Pandoc.Readers.ODT.Arrows.State- ( ArrowState(..)- , withState- , modifyState- , ignoringState- , fromState- , extractFromState- , tryModifyState- , withSubStateF- , withSubStateF'- , foldS- , iterateS- , iterateSL- , iterateS'- ) where--import qualified Data.List as L-import Control.Arrow-import qualified Control.Category as Cat-import Control.Monad-import Text.Pandoc.Readers.ODT.Arrows.Utils-import Text.Pandoc.Readers.ODT.Generic.Fallible---newtype ArrowState state a b = ArrowState- { runArrowState :: (state, a) -> (state, b) }---- | Constructor-withState :: (state -> a -> (state, b)) -> ArrowState state a b-withState = ArrowState . uncurry---- | Constructor-modifyState :: (state -> state ) -> ArrowState state a a-modifyState = ArrowState . first---- | Constructor-ignoringState :: ( a -> b ) -> ArrowState state a b-ignoringState = ArrowState . second---- | Constructor-fromState :: (state -> (state, b)) -> ArrowState state a b-fromState = ArrowState . (.fst)---- | Constructor-extractFromState :: (state -> b ) -> ArrowState state x b-extractFromState f = ArrowState $ \(state,_) -> (state, f state)---- | Constructor-tryModifyState :: (state -> Either f state)- -> ArrowState state a (Either f a)-tryModifyState f = ArrowState $ \(state,a)- -> (state,).Left ||| (,Right a) $ f state--instance Cat.Category (ArrowState s) where- id = ArrowState id- arrow2 . arrow1 = ArrowState $ runArrowState arrow2 . runArrowState arrow1--instance Arrow (ArrowState state) where- arr = ignoringState- first a = ArrowState $ \(s,(aF,aS))- -> second (,aS) $ runArrowState a (s,aF)- second a = ArrowState $ \(s,(aF,aS))- -> second (aF,) $ runArrowState a (s,aS)--instance ArrowChoice (ArrowState state) where- left a = ArrowState $ \(s,e) -> case e of- Left l -> second Left $ runArrowState a (s,l)- Right r -> (s, Right r)- right a = ArrowState $ \(s,e) -> case e of- Left l -> (s, Left l)- Right r -> second Right $ runArrowState a (s,r)--instance ArrowApply (ArrowState state) where- app = ArrowState $ \(s, (f,b)) -> runArrowState f (s,b)---- | Switches the type of the state temporarily.--- Drops the intermediate result state, behaving like a fallible--- identity arrow, save for side effects in the state.-withSubStateF :: ArrowState s x (Either f s')- -> ArrowState s' s (Either f s )- -> ArrowState s x (Either f x )-withSubStateF unlift a = keepingTheValue (withSubStateF' unlift a)- >>^ spreadChoice- >>^ fmap fst---- | Switches the type of the state temporarily.--- Returns the resulting sub-state.-withSubStateF' :: ArrowState s x (Either f s')- -> ArrowState s' s (Either f s )- -> ArrowState s x (Either f s')-withSubStateF' unlift a = ArrowState go- where go p@(s,_) = tryRunning unlift- ( tryRunning a (second Right) )- p- where tryRunning a' b v = case runArrowState a' v of- (_ , Left f) -> (s, Left f)- (x , Right y) -> b (y,x)---- | Fold a state arrow through something 'Foldable'. Collect the results--- in a 'Monoid'.--- Intermediate form of a fold between one with "only" a 'Monoid'--- and one with any function.-foldS :: (Foldable f, Monoid m) => ArrowState s x m -> ArrowState s (f x) m-foldS a = ArrowState $ \(s,f) -> foldr a' (s,mempty) f- where a' x (s',m) = second (mappend m) $ runArrowState a (s',x)---- | Fold a state arrow through something 'Foldable'. Collect the results in a--- 'MonadPlus'.-iterateS :: (Foldable f, MonadPlus m)- => ArrowState s x y- -> ArrowState s (f x) (m y)-iterateS a = ArrowState $ \(s,f) -> foldr a' (s,mzero) f- where a' x (s',m) = second (mplus m.return) $ runArrowState a (s',x)---- | Fold a state arrow through something 'Foldable'. Collect the results in a--- 'MonadPlus'.-iterateSL :: (Foldable f, MonadPlus m)- => ArrowState s x y- -> ArrowState s (f x) (m y)-iterateSL a = ArrowState $ \(s,f) -> L.foldl' a' (s,mzero) f- where a' (s',m) x = second (mplus m.return) $ runArrowState a (s',x)----- | Fold a fallible state arrow through something 'Foldable'.--- Collect the results in a 'MonadPlus'.--- If the iteration fails, the state will be reset to the initial one.-iterateS' :: (Foldable f, MonadPlus m)- => ArrowState s x (Either e y )- -> ArrowState s (f x) (Either e (m y))-iterateS' a = ArrowState $ \(s,f) -> foldr (a' s) (s,Right mzero) f- where a' s x (s',Right m) = case runArrowState a (s',x) of- (s'',Right m') -> (s'',Right $ mplus m $ return m')- (_ ,Left e ) -> (s ,Left e )- a' _ _ e = e
@@ -1,241 +0,0 @@-{- |- Module : Text.Pandoc.Readers.ODT.Arrows.Utils- Copyright : Copyright (C) 2015 Martin Linnemann- License : GNU GPL, version 2 or above-- Maintainer : Martin Linnemann <theCodingMarlin@googlemail.com>- Stability : alpha- Portability : portable--Utility functions for Arrows (Kleisli monads).--Some general notes on notation:--* "^" is meant to stand for a pure function that is lifted into an arrow-based on its usage for that purpose in "Control.Arrow".-* "?" is meant to stand for the usage of a 'FallibleArrow' or a pure function-with an equivalent return value.-* "_" stands for the dropping of a value.--}---- We export everything-module Text.Pandoc.Readers.ODT.Arrows.Utils- ( and2- , and3- , and4- , and5- , and6- , liftA2- , liftA3- , liftA4- , liftA5- , liftA6- , liftA- , duplicate- , (>>%)- , keepingTheValue- , (^|||)- , (|||^)- , (^|||^)- , (^&&&)- , (&&&^)- , choiceToMaybe- , maybeToChoice- , returnV- , FallibleArrow- , liftAsSuccess- , (>>?)- , (>>?^)- , (>>?^?)- , (^>>?)- , (>>?!)- , (>>?%)- , (>>?%?)- , ifFailedDo- ) where--import Prelude hiding (Applicative(..))-import Control.Arrow-import Control.Monad (join)--import Text.Pandoc.Readers.ODT.Generic.Fallible-import Text.Pandoc.Readers.ODT.Generic.Utils--and2 :: (Arrow a) => a b c -> a b c' -> a b (c,c')-and2 = (&&&)--and3 :: (Arrow a)- => a b c0->a b c1->a b c2- -> a b (c0,c1,c2 )-and4 :: (Arrow a)- => a b c0->a b c1->a b c2->a b c3- -> a b (c0,c1,c2,c3 )-and5 :: (Arrow a)- => a b c0->a b c1->a b c2->a b c3->a b c4- -> a b (c0,c1,c2,c3,c4 )-and6 :: (Arrow a)- => a b c0->a b c1->a b c2->a b c3->a b c4->a b c5- -> a b (c0,c1,c2,c3,c4,c5 )--and3 a b c = and2 a b &&& c- >>^ \((z,y ) , x) -> (z,y,x )-and4 a b c d = and3 a b c &&& d- >>^ \((z,y,x ) , w) -> (z,y,x,w )-and5 a b c d e = and4 a b c d &&& e- >>^ \((z,y,x,w ) , v) -> (z,y,x,w,v )-and6 a b c d e f = and5 a b c d e &&& f- >>^ \((z,y,x,w,v ) , u) -> (z,y,x,w,v,u )--liftA2 :: (Arrow a) => (x -> y -> z) -> a b x -> a b y -> a b z-liftA2 f a b = a &&& b >>^ uncurry f--liftA3 :: (Arrow a) => (z->y->x -> r)- -> a b z->a b y->a b x- -> a b r-liftA4 :: (Arrow a) => (z->y->x->w -> r)- -> a b z->a b y->a b x->a b w- -> a b r-liftA5 :: (Arrow a) => (z->y->x->w->v -> r)- -> a b z->a b y->a b x->a b w->a b v- -> a b r-liftA6 :: (Arrow a) => (z->y->x->w->v->u -> r)- -> a b z->a b y->a b x->a b w->a b v->a b u- -> a b r--liftA3 fun a b c = and3 a b c >>^ uncurry3 fun-liftA4 fun a b c d = and4 a b c d >>^ uncurry4 fun-liftA5 fun a b c d e = and5 a b c d e >>^ uncurry5 fun-liftA6 fun a b c d e f = and6 a b c d e f >>^ uncurry6 fun--liftA :: (Arrow a) => (y -> z) -> a b y -> a b z-liftA fun a = a >>^ fun----- | Duplicate a value to subsequently feed it into different arrows.--- Can almost always be replaced with '(&&&)', 'keepingTheValue',--- or even '(|||)'.--- Equivalent to--- > returnA &&& returnA-duplicate :: (Arrow a) => a b (b,b)-duplicate = arr $ join (,)---- | Applies a function to the uncurried result-pair of an arrow-application.--- (The %-symbol was chosen to evoke an association with pairs.)-(>>%) :: (Arrow a) => a x (b,c) -> (b -> c -> d) -> a x d-a >>% f = a >>^ uncurry f--infixr 2 >>%----- | Duplicate a value and apply an arrow to the second instance.--- Equivalent to--- > \a -> duplicate >>> second a--- or--- > \a -> returnA &&& a-keepingTheValue :: (Arrow a) => a b c -> a b (b,c)-keepingTheValue a = returnA &&& a--( ^||| ) :: (ArrowChoice a) => (b -> d) -> a c d -> a (Either b c) d-( |||^ ) :: (ArrowChoice a) => a b d -> (c -> d) -> a (Either b c) d-( ^|||^ ) :: (ArrowChoice a) => (b -> d) -> (c -> d) -> a (Either b c) d--l ^||| r = arr l ||| r-l |||^ r = l ||| arr r-l ^|||^ r = arr l ||| arr r--infixr 2 ^||| , |||^, ^|||^--( ^&&& ) :: (Arrow a) => (b -> c) -> a b c' -> a b (c,c')-( &&&^ ) :: (Arrow a) => a b c -> (b -> c') -> a b (c,c')--l ^&&& r = arr l &&& r-l &&&^ r = l &&& arr r--infixr 3 ^&&&, &&&^----- | Converts @Right a@ into @Just a@ and @Left _@ into @Nothing@.-choiceToMaybe :: (ArrowChoice a) => a (Either l r) (Maybe r)-choiceToMaybe = arr eitherToMaybe---- | Converts @Nothing@ into @Left ()@ and @Just a@ into @Right a@.-maybeToChoice :: (ArrowChoice a) => a (Maybe b) (Fallible b)-maybeToChoice = arr maybeToEither---- | Lifts a constant value into an arrow-returnV :: (Arrow a) => c -> a x c-returnV = arr.const---- | Defines Left as failure, Right as success-type FallibleArrow a input failure success = a input (Either failure success)-----liftAsSuccess :: (ArrowChoice a)- => a x success- -> FallibleArrow a x failure success-liftAsSuccess a = a >>^ Right---- | Execute the second arrow if the first succeeds-(>>?) :: (ArrowChoice a)- => FallibleArrow a x failure success- -> FallibleArrow a success failure success'- -> FallibleArrow a x failure success'-a >>? b = a >>> Left ^||| b---- | Execute the lifted second arrow if the first succeeds-(>>?^) :: (ArrowChoice a)- => FallibleArrow a x failure success- -> (success -> success')- -> FallibleArrow a x failure success'-a >>?^ f = a >>^ Left ^|||^ Right . f---- | Execute the lifted second arrow if the first succeeds-(>>?^?) :: (ArrowChoice a)- => FallibleArrow a x failure success- -> (success -> Either failure success')- -> FallibleArrow a x failure success'-a >>?^? b = a >>> Left ^|||^ b---- | Execute the second arrow if the lifted first arrow succeeds-(^>>?) :: (ArrowChoice a)- => (x -> Either failure success)- -> FallibleArrow a success failure success'- -> FallibleArrow a x failure success'-a ^>>? b = a ^>> Left ^||| b---- | Execute the second, non-fallible arrow if the first arrow succeeds-(>>?!) :: (ArrowChoice a)- => FallibleArrow a x failure success- -> a success success'- -> FallibleArrow a x failure success'-a >>?! f = a >>> right f------(>>?%) :: (ArrowChoice a)- => FallibleArrow a x f (b,b')- -> (b -> b' -> c)- -> FallibleArrow a x f c-a >>?% f = a >>?^ uncurry f-------(>>?%?) :: (ArrowChoice a)- => FallibleArrow a x f (b,b')- -> (b -> b' -> Either f c)- -> FallibleArrow a x f c-a >>?%? f = a >>?^? uncurry f--infixr 1 >>?, >>?^, >>?^?-infixr 1 ^>>?, >>?!-infixr 1 >>?%, >>?%?---- | An arrow version of a short-circuit (<|>)-ifFailedDo :: (ArrowChoice a)- => FallibleArrow a x f y- -> FallibleArrow a x f y- -> FallibleArrow a x f y-ifFailedDo a b = keepingTheValue a >>> repackage ^>> (b |||^ Right)- where repackage (x , Left _) = Left x- repackage (_ , Right y) = Right y--infixr 1 `ifFailedDo`
@@ -1,26 +0,0 @@-{- |- Module : Text.Pandoc.Readers.ODT.Base- Copyright : Copyright (C) 2015 Martin Linnemann- License : GNU GPL, version 2 or above-- Maintainer : Martin Linnemann <theCodingMarlin@googlemail.com>- Stability : alpha- Portability : portable--Core types of the odt reader.--}--module Text.Pandoc.Readers.ODT.Base- ( ODTConverterState- , XMLReader- , XMLReaderSafe- ) where--import Text.Pandoc.Readers.ODT.Generic.XMLConverter-import Text.Pandoc.Readers.ODT.Namespaces--type ODTConverterState s = XMLConverterState Namespace s--type XMLReader s a b = FallibleXMLConverter Namespace s a b--type XMLReaderSafe s a b = XMLConverter Namespace s a b
@@ -1,1050 +1,1206 @@-{-# LANGUAGE Arrows #-}-{-# LANGUAGE CPP #-}-{-# LANGUAGE DeriveFoldable #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE PatternGuards #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE ViewPatterns #-}-{-# LANGUAGE OverloadedStrings #-}-{- |- Module : Text.Pandoc.Readers.ODT.ContentReader- Copyright : Copyright (C) 2015 Martin Linnemann- License : GNU GPL, version 2 or above-- Maintainer : Martin Linnemann <theCodingMarlin@googlemail.com>- Stability : alpha- Portability : portable--The core of the odt reader that converts odt features into Pandoc types.--}--module Text.Pandoc.Readers.ODT.ContentReader-( readerState-, read_body-) where--import Prelude hiding (Applicative(..))-import Control.Applicative hiding (liftA, liftA2, liftA3)-import Control.Arrow-import Control.Monad ((<=<))--import qualified Data.ByteString.Lazy as B-import Data.Foldable (fold)-import Data.List (find)-import qualified Data.Map as M-import qualified Data.Text as T-import Data.Maybe-import Data.Monoid (Alt (..))--import Text.TeXMath (readMathML, writeTeX)-import qualified Text.Pandoc.XML.Light as XML--import Text.Pandoc.Builder hiding (underline)-import Text.Pandoc.MediaBag (MediaBag, insertMedia)-import Text.Pandoc.Shared-import Text.Pandoc.Extensions (extensionsFromList, Extension(..))-import qualified Text.Pandoc.UTF8 as UTF8--import Text.Pandoc.Readers.Docx.Combine (combineBlocks)--import Text.Pandoc.Readers.ODT.Base-import Text.Pandoc.Readers.ODT.Namespaces-import Text.Pandoc.Readers.ODT.StyleReader--import Text.Pandoc.Readers.ODT.Arrows.State (foldS)-import Text.Pandoc.Readers.ODT.Arrows.Utils-import Text.Pandoc.Readers.ODT.Generic.Fallible-import Text.Pandoc.Readers.ODT.Generic.Utils-import Text.Pandoc.Readers.ODT.Generic.XMLConverter--import Network.URI (parseRelativeReference, URI(uriPath))-import qualified Data.Set as Set------------------------------------------------------------------------------------- State-----------------------------------------------------------------------------------type Anchor = T.Text-type Media = [(FilePath, B.ByteString)]--data ReaderState- = ReaderState { -- | A collection of styles read somewhere else.- -- It is only queried here, not modified.- styleSet :: Styles- -- | A stack of the styles of parent elements.- -- Used to look up inherited style properties.- , styleTrace :: [Style]- -- | Keeps track of the current depth in nested lists- , currentListLevel :: ListLevel- -- | Keeps track of the previous list start counters,- -- so whenever a new list continues numbering,- -- we know what number to start from.- -- If list does not continue numbering, the counter- -- is being reset.- , listContinuationStartCounters :: M.Map ListLevel Int- -- | Lists may provide their own style, but they don't have- -- to. If they do not, the style of a parent list may be used- -- or even a default list style from the paragraph style.- -- This value keeps track of the closest list style there- -- currently is.- , currentListStyle :: Maybe ListStyle- -- | A map from internal anchor names to "pretty" ones.- -- The mapping is a purely cosmetic one.- , bookmarkAnchors :: M.Map Anchor Anchor- -- | A map of files / binary data from the archive- , envMedia :: Media- -- | Hold binary resources used in the document- , odtMediaBag :: MediaBag- }- deriving ( Show )--readerState :: Styles -> Media -> ReaderState-readerState styles media = ReaderState styles [] 0 M.empty Nothing M.empty media mempty-----pushStyle' :: Style -> ReaderState -> ReaderState-pushStyle' style state = state { styleTrace = style : styleTrace state }-----popStyle' :: ReaderState -> ReaderState-popStyle' state = case styleTrace state of- _:trace -> state { styleTrace = trace }- _ -> state----modifyListLevel :: (ListLevel -> ListLevel) -> (ReaderState -> ReaderState)-modifyListLevel f state = state { currentListLevel = f (currentListLevel state) }-----modifyListContinuationStartCounter :: ListLevel -> Int -> (ReaderState -> ReaderState)-modifyListContinuationStartCounter listLevel count state =- state { listContinuationStartCounters = M.insert listLevel count (listContinuationStartCounters state) }-----shiftListLevel :: ListLevel -> (ReaderState -> ReaderState)-shiftListLevel diff = modifyListLevel (+ diff)-----swapCurrentListStyle :: Maybe ListStyle -> ReaderState- -> (ReaderState, Maybe ListStyle)-swapCurrentListStyle mListStyle state = ( state { currentListStyle = mListStyle }- , currentListStyle state- )-----lookupPrettyAnchor :: Anchor -> ReaderState -> Maybe Anchor-lookupPrettyAnchor anchor ReaderState{..} = M.lookup anchor bookmarkAnchors-----putPrettyAnchor :: Anchor -> Anchor -> ReaderState -> ReaderState-putPrettyAnchor ugly pretty state@ReaderState{..}- = state { bookmarkAnchors = M.insert ugly pretty bookmarkAnchors }-----usedAnchors :: ReaderState -> [Anchor]-usedAnchors ReaderState{..} = M.elems bookmarkAnchors--getMediaBag :: ReaderState -> MediaBag-getMediaBag ReaderState{..} = odtMediaBag--getMediaEnv :: ReaderState -> Media-getMediaEnv ReaderState{..} = envMedia--insertMedia' :: (FilePath, B.ByteString) -> ReaderState -> ReaderState-insertMedia' (fp, bs) state@ReaderState{..}- = state { odtMediaBag = insertMedia fp Nothing bs odtMediaBag }------------------------------------------------------------------------------------- Reader type and associated tools-----------------------------------------------------------------------------------type ODTReader a b = XMLReader ReaderState a b--type ODTReaderSafe a b = XMLReaderSafe ReaderState a b---- | Extract something from the styles-fromStyles :: (a -> Styles -> b) -> ODTReaderSafe a b-fromStyles f = keepingTheValue- (getExtraState >>^ styleSet)- >>% f-----getStyleByName :: ODTReader StyleName Style-getStyleByName = fromStyles lookupStyle >>^ maybeToChoice-----findStyleFamily :: ODTReader Style StyleFamily-findStyleFamily = fromStyles getStyleFamily >>^ maybeToChoice-----lookupListStyle :: ODTReader StyleName ListStyle-lookupListStyle = fromStyles lookupListStyleByName >>^ maybeToChoice-----switchCurrentListStyle :: ODTReaderSafe (Maybe ListStyle) (Maybe ListStyle)-switchCurrentListStyle = keepingTheValue getExtraState- >>% swapCurrentListStyle- >>> first setExtraState- >>^ snd-----pushStyle :: ODTReaderSafe Style Style-pushStyle = keepingTheValue (- ( keepingTheValue getExtraState- >>% pushStyle'- )- >>> setExtraState- )- >>^ fst-----popStyle :: ODTReaderSafe x x-popStyle = keepingTheValue (- getExtraState- >>> arr popStyle'- >>> setExtraState- )- >>^ fst-----getCurrentListLevel :: ODTReaderSafe a ListLevel-getCurrentListLevel = getExtraState >>^ currentListLevel-----getListContinuationStartCounters :: ODTReaderSafe a (M.Map ListLevel Int)-getListContinuationStartCounters = getExtraState >>^ listContinuationStartCounters------getPreviousListStartCounter :: ODTReaderSafe ListLevel Int-getPreviousListStartCounter = proc listLevel -> do- counts <- getListContinuationStartCounters -< ()- returnA -< M.findWithDefault 0 listLevel counts-----updateMediaWithResource :: ODTReaderSafe (FilePath, B.ByteString) (FilePath, B.ByteString)-updateMediaWithResource = keepingTheValue (- (keepingTheValue getExtraState- >>% insertMedia'- )- >>> setExtraState- )- >>^ fst--lookupResource :: ODTReaderSafe FilePath (FilePath, B.ByteString)-lookupResource = proc target -> do- state <- getExtraState -< ()- case lookup target (getMediaEnv state) of- Just bs -> returnV (target, bs) -<< ()- Nothing -> returnV ("", B.empty) -< ()--type AnchorPrefix = T.Text---- | An adaptation of 'uniqueIdent' from "Text.Pandoc.Shared" that generates a--- unique identifier but without assuming that the id should be for a header.--- Second argument is a list of already used identifiers.-uniqueIdentFrom :: AnchorPrefix -> [Anchor] -> Anchor-uniqueIdentFrom baseIdent usedIdents =- let numIdent n = baseIdent <> "-" <> T.pack (show n)- in if baseIdent `elem` usedIdents- then maybe baseIdent numIdent- $ find (\x -> numIdent x `notElem` usedIdents) ([1..60000] :: [Int])- -- if we have more than 60,000, allow repeats- else baseIdent---- | First argument: basis for a new "pretty" anchor if none exists yet--- Second argument: a key ("ugly" anchor)--- Returns: saved "pretty" anchor or created new one-getPrettyAnchor :: ODTReaderSafe (AnchorPrefix, Anchor) Anchor-getPrettyAnchor = proc (baseIdent, uglyAnchor) -> do- state <- getExtraState -< ()- case lookupPrettyAnchor uglyAnchor state of- Just prettyAnchor -> returnA -< prettyAnchor- Nothing -> do- let newPretty = uniqueIdentFrom baseIdent (usedAnchors state)- modifyExtraState (putPrettyAnchor uglyAnchor newPretty) -<< newPretty---- | Input: basis for a new header anchor--- Output: saved new anchor-getHeaderAnchor :: ODTReaderSafe Inlines Anchor-getHeaderAnchor = proc title -> do- state <- getExtraState -< ()- let exts = extensionsFromList [Ext_auto_identifiers]- let anchor = uniqueIdent exts (toList title)- (Set.fromList $ usedAnchors state)- modifyExtraState (putPrettyAnchor anchor anchor) -<< anchor-------------------------------------------------------------------------------------- Working with styles--------------------------------------------------------------------------------------readStyleByName :: ODTReader a (StyleName, Style)-readStyleByName =- findAttr NsText "style-name" >>? keepingTheValue getStyleByName >>^ liftE- where- liftE :: (StyleName, Fallible Style) -> Fallible (StyleName, Style)- liftE (name, Right v) = Right (name, v)- liftE (_, Left v) = Left v-----isStyleToTrace :: ODTReader Style Bool-isStyleToTrace = findStyleFamily >>?^ (==FaText)-----withNewStyle :: ODTReaderSafe x Inlines -> ODTReaderSafe x Inlines-withNewStyle a = proc x -> do- fStyle <- readStyleByName -< ()- case fStyle of- Right (styleName, _) | isCodeStyle styleName -> do- inlines <- a -< x- arr inlineCode -<< inlines- Right (_, style) -> do- mFamily <- arr styleFamily -< style- fTextProps <- arr ( maybeToChoice- . textProperties- . styleProperties- ) -< style- case fTextProps of- Right textProps -> do- state <- getExtraState -< ()- let triple = (state, textProps, mFamily)- modifier <- arr modifierFromStyleDiff -< triple- fShouldTrace <- isStyleToTrace -< style- case fShouldTrace of- Right shouldTrace ->- if shouldTrace- then do- pushStyle -< style- inlines <- a -< x- popStyle -< ()- arr modifier -<< inlines- else- -- In case anything goes wrong- a -< x- Left _ -> a -< x- Left _ -> a -< x- Left _ -> a -< x- where- isCodeStyle :: StyleName -> Bool- isCodeStyle "Source_Text" = True- isCodeStyle "Source_20_Text" = True- isCodeStyle _ = False-- inlineCode :: Inlines -> Inlines- inlineCode = code . T.concat . map stringify . toList--type PropertyTriple = (ReaderState, TextProperties, Maybe StyleFamily)-type InlineModifier = Inlines -> Inlines---- | Given data about the local style changes, calculates how to modify--- an instance of 'Inlines'-modifierFromStyleDiff :: PropertyTriple -> InlineModifier-modifierFromStyleDiff propertyTriple =- composition $- getVPosModifier propertyTriple- : map (first ($ propertyTriple) >>> ifThen_else ignore)- [ (hasEmphChanged , emph )- , (hasChanged isStrong , strong )- , (hasChanged strikethrough , strikeout )- ]- where- ifThen_else else' (if',then') = if if' then then' else else'-- ignore = id :: InlineModifier-- getVPosModifier :: PropertyTriple -> InlineModifier- getVPosModifier triple@(_,textProps,_) =- let getVPos = Just . verticalPosition- in case lookupPreviousValueM getVPos triple of- Nothing -> ignore- Just oldVPos -> getVPosModifier' (oldVPos, verticalPosition textProps)-- getVPosModifier' (oldVPos , newVPos ) | oldVPos == newVPos = ignore- getVPosModifier' ( _ , VPosSub ) = subscript- getVPosModifier' ( _ , VPosSuper ) = superscript- getVPosModifier' ( _ , _ ) = ignore-- hasEmphChanged :: PropertyTriple -> Bool- hasEmphChanged = swing any [ hasChanged isEmphasised- , hasChanged underline- ]-- hasChanged property triple@(_, property -> newProperty, _) =- (/= Just newProperty) (lookupPreviousValue property triple)-- lookupPreviousValue f = lookupPreviousStyleValue (fmap f . textProperties)-- lookupPreviousValueM f = lookupPreviousStyleValue (f <=< textProperties)-- lookupPreviousStyleValue f (ReaderState{..},_,mFamily)- = findBy f (extendedStylePropertyChain styleTrace styleSet)- <|> (f . lookupDefaultStyle' styleSet =<< mFamily)---type ParaModifier = Blocks -> Blocks--_MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_MM_ :: Int-_MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_PERCENT_ :: Int-_MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_MM_ = 5-_MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_PERCENT_ = 5---- | Returns either 'id' or 'blockQuote' depending if any of the StyleProperties--- are indented at quote level.-getParaModifier :: ListLevel -> [StyleProperties] -> ParaModifier-getParaModifier listLevel props- | listLevel > 0 = id -- see #9505, list paragraphs need indentation- | any isBlockQuote props = blockQuote- | otherwise = id- where- isBlockQuote SProps {..} | Just paraProps <- paraProperties- , isQuoteWidth (margin_left paraProps)- = True- | otherwise- = False- isQuoteWidth mMargin- | LengthValueMM margin <- mMargin- , margin > _MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_MM_- = True- | PercentValue margin <- mMargin- , margin > _MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_PERCENT_- = True- | otherwise- = False-----constructPara :: ODTReaderSafe a Blocks -> ODTReaderSafe a Blocks-constructPara reader = proc blocks -> do- fStyle <- readStyleByName -< blocks- case fStyle of- Left _ -> reader -< blocks- Right (styleName, _) | isTableCaptionStyle styleName -> do- blocks' <- reader -< blocks- arr tableCaptionP -< blocks'- Right (_, style) -> do- props <- fromStyles extendedStylePropertyChain -< [style]- listLevel <- getCurrentListLevel -< ()- let modifier = getParaModifier listLevel props- blocks' <- reader -< blocks- arr modifier -<< blocks'- where- isTableCaptionStyle :: StyleName -> Bool- isTableCaptionStyle "Table" = True- isTableCaptionStyle _ = False- tableCaptionP b = divWith ("", ["caption"], []) b--type ListConstructor = [Blocks] -> Blocks--getListConstructor :: ListLevelStyle -> Int -> ListConstructor-getListConstructor ListLevelStyle{..} startNum =- case listLevelType of- LltBullet -> bulletList- LltImage -> bulletList- LltNumbered -> let listNumberStyle = toListNumberStyle listItemFormat- listNumberDelim = toListNumberDelim listItemPrefix- listItemSuffix- in orderedListWith (startNum, listNumberStyle, listNumberDelim)- where- toListNumberStyle LinfNone = DefaultStyle- toListNumberStyle LinfNumber = Decimal- toListNumberStyle LinfRomanLC = LowerRoman- toListNumberStyle LinfRomanUC = UpperRoman- toListNumberStyle LinfAlphaLC = LowerAlpha- toListNumberStyle LinfAlphaUC = UpperAlpha- toListNumberStyle (LinfString _) = Example-- toListNumberDelim Nothing (Just ".") = Period- toListNumberDelim (Just "" ) (Just ".") = Period- toListNumberDelim Nothing (Just ")") = OneParen- toListNumberDelim (Just "" ) (Just ")") = OneParen- toListNumberDelim (Just "(") (Just ")") = TwoParens- toListNumberDelim _ _ = DefaultDelim---- | Determines which style to use for a list, which level to use of that--- style, and which type of list to create as a result of this information.--- Then prepares the state for eventual child lists and constructs the list from--- the results.--- Two main cases are handled: The list may provide its own style or it may--- rely on a parent list's style. I the former case the current style in the--- state must be switched before and after the call to the child converter--- while in the latter the child converter can be called directly.--- If anything goes wrong, a default ordered-list-constructor is used.-constructList :: ODTReaderSafe x [Blocks] -> ODTReaderSafe x Blocks-constructList reader = proc x -> do- modifyExtraState (shiftListLevel 1) -< ()- listLevel <- getCurrentListLevel -< ()- listContinuationStartCounter <- getPreviousListStartCounter -< listLevel- fStyleName <- findAttr NsText "style-name" -< ()- fContNumbering <- findAttr NsText "continue-numbering" -< ()- listItemCount <- reader >>^ length -< x-- let continueNumbering = case fContNumbering of- Right "true" -> True- _ -> False-- let startNumForListLevelStyle = listStartingNumber continueNumbering listContinuationStartCounter- let defaultOrderedListConstructor = constructOrderedList (startNumForListLevelStyle Nothing) listLevel listItemCount-- case fStyleName of- Right styleName -> do- fListStyle <- lookupListStyle -< styleName- case fListStyle of- Right listStyle -> do- fListLevelStyle <- arr (uncurry getListLevelStyle) -< (listLevel, listStyle)- case fListLevelStyle of- Just listLevelStyle -> do- let startNum = startNumForListLevelStyle $ Just listLevelStyle- oldListStyle <- switchCurrentListStyle -< Just listStyle- blocks <- constructListWith listLevelStyle startNum listLevel listItemCount -<< x- switchCurrentListStyle -< oldListStyle- returnA -< blocks- Nothing -> defaultOrderedListConstructor -<< x- Left _ -> defaultOrderedListConstructor -<< x- Left _ -> do- state <- getExtraState -< ()- mListStyle <- arr currentListStyle -< state- case mListStyle of- Just listStyle -> do- fListLevelStyle <- arr (uncurry getListLevelStyle) -< (listLevel, listStyle)- case fListLevelStyle of- Just listLevelStyle -> do- let startNum = startNumForListLevelStyle $ Just listLevelStyle- constructListWith listLevelStyle startNum listLevel listItemCount -<< x- Nothing -> defaultOrderedListConstructor -<< x- Nothing -> defaultOrderedListConstructor -<< x- where- listStartingNumber continueNumbering listContinuationStartCounter mListLevelStyle- | continueNumbering = listContinuationStartCounter- | isJust mListLevelStyle = listItemStart (fromJust mListLevelStyle)- | otherwise = 1- constructOrderedList startNum listLevel listItemCount =- reader- >>> modifyExtraState (shiftListLevel (-1))- >>> modifyExtraState (modifyListContinuationStartCounter listLevel (startNum + listItemCount))- >>^ orderedListWith (startNum, DefaultStyle, DefaultDelim)- constructListWith listLevelStyle startNum listLevel listItemCount =- reader- >>> getListConstructor listLevelStyle startNum- ^>> modifyExtraState (shiftListLevel (-1))- >>> modifyExtraState (modifyListContinuationStartCounter listLevel (startNum + listItemCount))------------------------------------------------------------------------------------- Readers-----------------------------------------------------------------------------------type ElementMatcher result = (Namespace, ElementName, ODTReader result result)--type InlineMatcher = ElementMatcher Inlines--type BlockMatcher = ElementMatcher Blocks--newtype FirstMatch a = FirstMatch (Alt Maybe a)- deriving (Foldable, Monoid, Semigroup)--firstMatch :: a -> FirstMatch a-firstMatch = FirstMatch . Alt . Just--newtype CombiningBlocks = CombiningBlocks { unCombiningBlocks :: Blocks }--instance Semigroup CombiningBlocks where- CombiningBlocks l <> CombiningBlocks r = CombiningBlocks (combineBlocks l r)--instance Monoid CombiningBlocks where- mempty = CombiningBlocks mempty-----matchingElement :: (Monoid e)- => Namespace -> ElementName- -> ODTReaderSafe e e- -> ElementMatcher e-matchingElement ns name reader = (ns, name, asResultAccumulator reader)- where- asResultAccumulator :: (ArrowChoice a, Monoid m) => a m m -> a m (Fallible m)- asResultAccumulator a = liftAsSuccess $ keepingTheValue a >>% mappend-----matchChildContent' :: (Monoid result)- => [ElementMatcher result]- -> ODTReaderSafe a result-matchChildContent' ls = returnV mempty >>> matchContent' ls-----matchSmushedChildBlocks' :: [ElementMatcher CombiningBlocks] -> ODTReaderSafe a Blocks-matchSmushedChildBlocks' ls = liftA unCombiningBlocks- $ returnV mempty- >>> matchContent' ls-----matchChildContent :: (Monoid result)- => [ElementMatcher result]- -> ODTReaderSafe (result, XML.Content) result- -> ODTReaderSafe a result-matchChildContent ls fallback = returnV mempty >>> matchContent ls fallback------------------------------------------------- Matchers------------------------------------------------------------------------ Basics------------------------------ | Open Document allows several consecutive spaces if they are marked up-read_plain_text :: ODTReaderSafe (Inlines, XML.Content) Inlines-read_plain_text = fst ^&&& read_plain_text' >>% recover- where- -- fallible version- read_plain_text' :: ODTReader (Inlines, XML.Content) Inlines- read_plain_text' = ( second ( arr extractText )- >>^ spreadChoice >>?! second text- )- >>?% mappend- --- extractText :: XML.Content -> Fallible T.Text- extractText (XML.Text cData) = succeedWith (XML.cdData cData)- extractText _ = failEmpty--read_text_seq :: InlineMatcher-read_text_seq = matchingElement NsText "sequence"- $ matchChildContent [] read_plain_text----- specifically. I honor that, although the current implementation of 'mappend'--- for 'Inlines' in "Text.Pandoc.Builder" will collapse them again.--- The rational is to be prepared for future modifications.-read_spaces :: InlineMatcher-read_spaces = matchingElement NsText "s" (- readAttrWithDefault NsText "c" 1 -- how many spaces?- >>^ fromList.(`replicate` Space)- )----read_line_break :: InlineMatcher-read_line_break = matchingElement NsText "line-break"- $ returnV linebreak----read_tab :: InlineMatcher-read_tab = matchingElement NsText "tab"- $ returnV space----read_span :: InlineMatcher-read_span = matchingElement NsText "span"- $ withNewStyle- $ matchChildContent [ read_span- , read_spaces- , read_line_break- , read_tab- , read_link- , read_frame- , read_note- , read_citation- , read_bookmark- , read_bookmark_start- , read_reference_start- , read_bookmark_ref- , read_reference_ref- ] read_plain_text-----read_paragraph :: ElementMatcher CombiningBlocks-read_paragraph = matchingElement NsText "p" $- liftA CombiningBlocks $ proc blocks -> do- fStyle <- readStyleByName -< blocks- case fStyle of- Right style | isPreformattedStyle style -> do- liftA (codeBlock . stringify) $ matchParagraphContent -< blocks- _ ->- constructPara $ liftA para $ withNewStyle matchParagraphContent -< blocks- where- isPreformattedStyle :: (StyleName, Style) -> Bool- isPreformattedStyle ("Preformatted_20_Text", _) = True- isPreformattedStyle (_, Style { styleParentName = Just "Preformatted_20_Text" }) = True- isPreformattedStyle _ = False---matchParagraphContent :: ODTReaderSafe a Inlines-matchParagraphContent = matchChildContent [ read_span- , read_spaces- , read_line_break- , read_tab- , read_link- , read_note- , read_citation- , read_bookmark- , read_bookmark_start- , read_reference_start- , read_bookmark_ref- , read_reference_ref- , read_frame- , read_text_seq- ] read_plain_text---------------------------- Headers----------------------------read_header :: ElementMatcher CombiningBlocks-read_header = matchingElement NsText "h"- $ proc blocks -> do- level <- ( readAttrWithDefault NsText "outline-level" 1- ) -< blocks- children <- ( matchChildContent [ read_span- , read_spaces- , read_line_break- , read_tab- , read_link- , read_note- , read_citation- , read_bookmark- , read_bookmark_start- , read_reference_start- , read_bookmark_ref- , read_reference_ref- , read_frame- ] read_plain_text- ) -< blocks- anchor <- getHeaderAnchor -< children- let idAttr = (anchor, [], []) -- no classes, no key-value pairs- arr (CombiningBlocks . uncurry3 headerWith) -< (idAttr, level, children)--------------------------- Lists----------------------------read_list :: ElementMatcher CombiningBlocks-read_list = matchingElement NsText "list"- $ liftA CombiningBlocks- $ constructList- $ matchChildContent' [ read_list_item- , read_list_header- ]----read_list_item :: ElementMatcher [Blocks]-read_list_item = read_list_element "list-item"--read_list_header :: ElementMatcher [Blocks]-read_list_header = read_list_element "list-header"--read_list_element :: ElementName -> ElementMatcher [Blocks]-read_list_element listElement = matchingElement NsText listElement- $ liftA (compactify.(:[]))- ( matchSmushedChildBlocks' [ read_paragraph- , read_header- , read_list- , read_section- ]- )--------------------------- Sections-------------------------read_section :: ElementMatcher CombiningBlocks-read_section = matchingElement NsText "section"- $ liftA (CombiningBlocks . divWith nullAttr)- $ matchSmushedChildBlocks' [ read_paragraph- , read_header- , read_list- , read_table- , read_section- ]---------------------------- Links-------------------------read_link :: InlineMatcher-read_link = matchingElement NsText "a"- $ liftA3 link- ( findAttrTextWithDefault NsXLink "href" ""- >>> arr fixRelativeLink )- ( findAttrTextWithDefault NsOffice "title" "" )- ( matchChildContent [ read_span- , read_note- , read_citation- , read_bookmark- , read_bookmark_start- , read_reference_start- , read_bookmark_ref- , read_reference_ref- ] read_plain_text )--fixRelativeLink :: T.Text -> T.Text-fixRelativeLink uri =- case parseRelativeReference (T.unpack uri) of- Nothing -> uri- Just u ->- case uriPath u of- '.':'.':'/':xs -> tshow $ u{ uriPath = xs }- _ -> uri------------------------------ Footnotes----------------------------read_note :: InlineMatcher-read_note = matchingElement NsText "note"- $ liftA note- $ matchChildContent' [ read_note_body ]--read_note_body :: BlockMatcher-read_note_body = matchingElement NsText "note-body"- $ matchSmushedChildBlocks' [ read_paragraph ]------------------------------ Citations----------------------------read_citation :: InlineMatcher-read_citation = matchingElement NsText "bibliography-mark"- $ liftA2 cite- ( liftA2 makeCitation- ( findAttrTextWithDefault NsText "identifier" "" )- ( readAttrWithDefault NsText "number" 0 )- )- ( matchChildContent [] read_plain_text )- where- makeCitation :: T.Text -> Int -> [Citation]- makeCitation citeId num = [Citation citeId [] [] NormalCitation num 0]---------------------------- Tables----------------------------read_table :: ElementMatcher CombiningBlocks-read_table = matchingElement NsTable "table"- $ liftA (CombiningBlocks . table')- $ (matchChildContent' [read_table_header]) &&&- (matchChildContent' [read_table_row])---- | A table without a caption.-table' :: ([[Cell]], [[Cell]]) -> Blocks-table' (headers, rows) =- table emptyCaption (replicate numcols defaults) th [tb] tf- where- defaults = (AlignDefault, ColWidthDefault)- numcols = maximum $ map length $ headers ++ rows- toRow = Row nullAttr- th = TableHead nullAttr $ map toRow headers- tb = TableBody nullAttr 0 [] $ map toRow rows- tf = TableFoot nullAttr []-----read_table_header :: ElementMatcher [[Cell]]-read_table_header = matchingElement NsTable "table-header-rows"- $ matchChildContent' [ read_table_row- ]-----read_table_row :: ElementMatcher [[Cell]]-read_table_row = matchingElement NsTable "table-row"- $ liftA (:[])- $ matchChildContent' [ read_table_cell- ]-----read_table_cell :: ElementMatcher [Cell]-read_table_cell = matchingElement NsTable "table-cell"- $ liftA3 cell'- (readAttrWithDefault NsTable "number-rows-spanned" 1 >>^ RowSpan)- (readAttrWithDefault NsTable "number-columns-spanned" 1 >>^ ColSpan)- $ matchSmushedChildBlocks' [ read_paragraph- , read_list- ]- where- cell' rowSpan colSpan blocks = map (cell AlignDefault rowSpan colSpan) $ compactify [blocks]--------------------------- Frames----------------------------read_frame :: InlineMatcher-read_frame = matchingElement NsDraw "frame"- $ filterChildrenName' NsDraw (`elem` ["image", "object", "text-box"])- >>> foldS read_frame_child- >>> arr fold--read_frame_child :: ODTReaderSafe XML.Element (FirstMatch Inlines)-read_frame_child =- proc child -> case elName child of- "image" -> read_frame_img -< child- "object" -> read_frame_mathml -< child- "text-box" -> read_frame_text_box -< child- _ -> returnV mempty -< ()--read_frame_img :: ODTReaderSafe XML.Element (FirstMatch Inlines)-read_frame_img =- proc img -> do- src <- executeIn (findAttr' NsXLink "href") -< img- case fold src of- "" -> returnV mempty -< ()- src' -> do- let exts = extensionsFromList [Ext_auto_identifiers]- src'' = fixRelativeLink src'- resource <- lookupResource -< T.unpack src''- _ <- updateMediaWithResource -< resource- w <- findAttrText' NsSVG "width" -< ()- h <- findAttrText' NsSVG "height" -< ()- titleNodes <- matchChildContent' [ read_frame_title ] -< ()- alt <- matchChildContent [] read_plain_text -< ()- arr (firstMatch . uncurry4 imageWith) -<- (image_attributes w h, src'', inlineListToIdentifier exts (toList titleNodes), alt)--read_frame_title :: InlineMatcher-read_frame_title = matchingElement NsSVG "title" (matchChildContent [] read_plain_text)--image_attributes :: Maybe T.Text -> Maybe T.Text -> Attr-image_attributes x y =- ( "", [], dim "width" x ++ dim "height" y)- where- dim _ (Just "") = []- dim name (Just v) = [(name, v)]- dim _ Nothing = []--read_frame_mathml :: ODTReaderSafe XML.Element (FirstMatch Inlines)-read_frame_mathml =- proc obj -> do- src <- executeIn (findAttr' NsXLink "href") -< obj- case fold src of- "" -> returnV mempty -< ()- src' -> do- let path = T.unpack $- fromMaybe src' (T.stripPrefix "./" src') <> "/content.xml"- (_, mathml) <- lookupResource -< path- case readMathML (UTF8.toText $ B.toStrict mathml) of- Left _ -> returnV mempty -< ()- Right exps -> arr (firstMatch . displayMath . writeTeX) -< exps--read_frame_text_box :: ODTReaderSafe XML.Element (FirstMatch Inlines)-read_frame_text_box = proc box -> do- paragraphs <- executeIn (matchSmushedChildBlocks' [ read_paragraph ]) -< box- arr read_img_with_caption -< toList paragraphs--read_img_with_caption :: [Block] -> FirstMatch Inlines-read_img_with_caption (Para [Image attr alt (src,title)] : _) =- firstMatch $ singleton (Image attr alt (src, "fig:" <> title)) -- no text, default caption-read_img_with_caption (Para (Image attr _ (src,title) : txt) : _) =- firstMatch $ singleton (Image attr txt (src, "fig:" <> title) ) -- override caption with the text that follows-read_img_with_caption ( Para (_ : xs) : ys) =- read_img_with_caption (Para xs : ys)-read_img_with_caption _ =- mempty--------------------------- Internal links-------------------------_ANCHOR_PREFIX_ :: T.Text-_ANCHOR_PREFIX_ = "anchor"-----readAnchorAttr :: ODTReader a Anchor-readAnchorAttr = findAttrText NsText "name"---- | Beware: may fail-findAnchorName :: ODTReader AnchorPrefix Anchor-findAnchorName = ( keepingTheValue readAnchorAttr- >>^ spreadChoice- ) >>?! getPrettyAnchor------maybeAddAnchorFrom :: ODTReader Inlines AnchorPrefix- -> ODTReaderSafe Inlines Inlines-maybeAddAnchorFrom anchorReader =- keepingTheValue (anchorReader >>? findAnchorName >>?^ toAnchorElem)- >>>- proc (inlines, fAnchorElem) -> do- case fAnchorElem of- Right anchorElem -> returnA -< anchorElem- Left _ -> returnA -< inlines- where- toAnchorElem :: Anchor -> Inlines- toAnchorElem anchorID = spanWith (anchorID, [], []) mempty- -- no classes, no key-value pairs-----read_bookmark :: InlineMatcher-read_bookmark = matchingElement NsText "bookmark"- $ maybeAddAnchorFrom (liftAsSuccess $ returnV _ANCHOR_PREFIX_)-----read_bookmark_start :: InlineMatcher-read_bookmark_start = matchingElement NsText "bookmark-start"- $ maybeAddAnchorFrom (liftAsSuccess $ returnV _ANCHOR_PREFIX_)-----read_reference_start :: InlineMatcher-read_reference_start = matchingElement NsText "reference-mark-start"- $ maybeAddAnchorFrom readAnchorAttr---- | Beware: may fail-findAnchorRef :: ODTReader a Anchor-findAnchorRef = ( findAttrText NsText "ref-name"- >>?^ (_ANCHOR_PREFIX_,)- ) >>?! getPrettyAnchor------maybeInAnchorRef :: ODTReaderSafe Inlines Inlines-maybeInAnchorRef = proc inlines -> do- fRef <- findAnchorRef -< ()- case fRef of- Right anchor ->- arr (toAnchorRef anchor) -<< inlines- Left _ -> returnA -< inlines- where- toAnchorRef :: Anchor -> Inlines -> Inlines- toAnchorRef anchor = link ("#" <> anchor) "" -- no title-----read_bookmark_ref :: InlineMatcher-read_bookmark_ref = matchingElement NsText "bookmark-ref"- $ maybeInAnchorRef- <<< matchChildContent [] read_plain_text-----read_reference_ref :: InlineMatcher-read_reference_ref = matchingElement NsText "reference-ref"- $ maybeInAnchorRef- <<< matchChildContent [] read_plain_text---------------------------- Entry point-------------------------read_text :: ODTReaderSafe a Pandoc-read_text = matchSmushedChildBlocks' [ read_header- , read_paragraph- , read_list- , read_section- , read_table- ]- >>^ doc--post_process :: Pandoc -> Pandoc-post_process (Pandoc m blocks) =- Pandoc m (post_process' blocks)--post_process' :: [Block] -> [Block]-post_process' (Table attr _ specs th tb tf : Div ("", ["caption"], _) blks : xs)- = Table attr (Caption Nothing blks) specs th tb tf : post_process' xs-post_process' bs = bs--read_body :: ODTReader a (Pandoc, MediaBag)-read_body = executeInSub NsOffice "body"- $ executeInSub NsOffice "text"- $ liftAsSuccess- $ proc inlines -> do- txt <- read_text -< inlines- state <- getExtraState -< ()- returnA -< (post_process txt, getMediaBag state)+{-# LANGUAGE DeriveFoldable #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ViewPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+{- |+ Module : Text.Pandoc.Readers.ODT.ContentReader+ Copyright : Copyright (C) 2015 Martin Linnemann+ License : GNU GPL, version 2 or above++ Maintainer : Martin Linnemann <theCodingMarlin@googlemail.com>+ Stability : alpha+ Portability : portable++The core of the odt reader that converts odt features into Pandoc types.+-}++module Text.Pandoc.Readers.ODT.ContentReader+( readerState+, read_body+) where++import Control.Applicative ((<|>))+import Control.Monad ((<=<))++import Data.ByteString.Base64 (decodeLenient)+import qualified Data.ByteString.Lazy as B+import Data.Char (isSpace)+import Data.Foldable (fold)+import Data.List (find)+import Data.List.NonEmpty (nonEmpty)+import qualified Data.Map as M+import qualified Data.Text as T+import Data.Maybe+import Data.Monoid (Alt (..))++import Text.TeXMath (readMathML, writeTeX)+import qualified Text.Pandoc.XML.Light as XML++import Text.Pandoc.Builder hiding (underline)+import Text.Pandoc.ImageSize (ImageType(..), imageType)+import Text.Pandoc.MediaBag (MediaBag, insertMedia, mediaItems)+import Text.Pandoc.MIME (extensionFromMimeType)+import Text.Pandoc.Shared+import Text.Pandoc.Walk (walk)+import Text.Pandoc.Extensions (extensionsFromList, Extension(..))+import qualified Text.Pandoc.UTF8 as UTF8++import Text.Pandoc.Readers.Docx.Combine (combineBlocks)++import Text.Pandoc.Readers.ODT.Namespaces+import Text.Pandoc.Readers.ODT.StyleReader++import Text.Pandoc.Readers.ODT.Generic.Utils (findBy)+import Text.Pandoc.Readers.ODT.Generic.XMLConverter++import Network.URI (parseRelativeReference, URI(uriPath))+import qualified Data.Set as Set++--------------------------------------------------------------------------------+-- State+--------------------------------------------------------------------------------++type Anchor = T.Text+type Media = M.Map FilePath B.ByteString++data ReaderState+ = ReaderState { -- | A collection of styles read somewhere else.+ -- It is only queried here, not modified.+ styleSet :: Styles+ -- | A stack of the styles of parent elements.+ -- Used to look up inherited style properties.+ , styleTrace :: [Style]+ -- | Keeps track of the current depth in nested lists+ , currentListLevel :: ListLevel+ -- | Keeps track of the previous list start counters+ -- for each list style and level, so whenever a new+ -- list continues numbering, we know what number to+ -- start from. The counters for deeper levels are+ -- reset whenever a new list item starts.+ , listContinuationStartCounters+ :: M.Map (Maybe StyleName, ListLevel) Int+ -- | Lists may provide their own style, but they don't have+ -- to. If they do not, the style of a parent list may be used+ -- or even a default list style from the paragraph style.+ -- This value keeps track of the closest list style there+ -- currently is.+ , currentListStyle :: Maybe ListStyle+ -- | The name of 'currentListStyle', if known.+ , currentListStyleName :: Maybe StyleName+ -- | A map from internal anchor names to "pretty" ones.+ -- The mapping is a purely cosmetic one.+ , bookmarkAnchors :: M.Map Anchor Anchor+ -- | The "pretty" anchors handed out so far, i.e. the+ -- values of 'bookmarkAnchors'. Kept separately so that+ -- checking an anchor for uniqueness does not cost a+ -- traversal of the whole map.+ , usedAnchors :: Set.Set Anchor+ -- | A map of files / binary data from the archive+ , envMedia :: Media+ -- | Hold binary resources used in the document+ , odtMediaBag :: MediaBag+ -- | Whether to record named styles in @custom-style@+ -- attributes (the @styles@ extension)+ , keepStyles :: Bool+ }+ deriving ( Show )++readerState :: Bool -> Styles -> [(FilePath, B.ByteString)] -> ReaderState+readerState keepStyles' styles media =+ ReaderState styles [] 0 M.empty Nothing Nothing M.empty Set.empty+ (M.fromList media) mempty keepStyles'++--+pushStyle' :: Style -> ReaderState -> ReaderState+pushStyle' style state = state { styleTrace = style : styleTrace state }++--+popStyle' :: ReaderState -> ReaderState+popStyle' state = case styleTrace state of+ _:trace -> state { styleTrace = trace }+ _ -> state+--+modifyListLevel :: (ListLevel -> ListLevel) -> (ReaderState -> ReaderState)+modifyListLevel f state = state { currentListLevel = f (currentListLevel state) }++--+modifyListContinuationStartCounter :: Maybe StyleName -> ListLevel -> Int+ -> (ReaderState -> ReaderState)+modifyListContinuationStartCounter mStyleName listLevel count state =+ state { listContinuationStartCounters =+ M.insert (mStyleName, listLevel) count+ (listContinuationStartCounters state) }++-- | Reset the counters of all list levels deeper than the given one.+resetDeeperListContinuationStartCounters :: ListLevel+ -> (ReaderState -> ReaderState)+resetDeeperListContinuationStartCounters listLevel state =+ state { listContinuationStartCounters =+ M.filterWithKey (\(_, lvl) _ -> lvl <= listLevel)+ (listContinuationStartCounters state) }++--+shiftListLevel :: ListLevel -> (ReaderState -> ReaderState)+shiftListLevel diff = modifyListLevel (+ diff)++--+lookupPrettyAnchor :: Anchor -> ReaderState -> Maybe Anchor+lookupPrettyAnchor anchor ReaderState{..} = M.lookup anchor bookmarkAnchors++--+putPrettyAnchor :: Anchor -> Anchor -> ReaderState -> ReaderState+putPrettyAnchor ugly pretty state@ReaderState{..}+ = state { bookmarkAnchors = M.insert ugly pretty bookmarkAnchors+ , usedAnchors = Set.insert pretty usedAnchors+ }++getMediaBag :: ReaderState -> MediaBag+getMediaBag ReaderState{..} = odtMediaBag++getMediaEnv :: ReaderState -> Media+getMediaEnv ReaderState{..} = envMedia++insertMedia' :: (FilePath, B.ByteString) -> ReaderState -> ReaderState+insertMedia' (fp, bs) state@ReaderState{..}+ = state { odtMediaBag = insertMedia fp Nothing bs odtMediaBag }++--------------------------------------------------------------------------------+-- Reader type and associated tools+--------------------------------------------------------------------------------++type ODTReader a = XMLConverter Namespace ReaderState a++-- | Extract the styles from the reader state+getStyles :: ODTReader Styles+getStyles = styleSet <$> getExtraState++--+getStyleByName :: StyleName -> ODTReader Style+getStyleByName name = getStyles >>= fromMaybeF . lookupStyle name++--+--+lookupListStyle :: StyleName -> ODTReader ListStyle+lookupListStyle name = getStyles >>= fromMaybeF . lookupListStyleByName name++--+switchCurrentListStyle :: Maybe (StyleName, ListStyle)+ -> ODTReader (Maybe (StyleName, ListStyle))+switchCurrentListStyle mListStyle = do+ state <- getExtraState+ setExtraState $ state { currentListStyle = snd <$> mListStyle+ , currentListStyleName = fst <$> mListStyle }+ return $ (,) <$> currentListStyleName state <*> currentListStyle state++--+pushStyle :: Style -> ODTReader ()+pushStyle style = modifyExtraState (pushStyle' style)++--+popStyle :: ODTReader ()+popStyle = modifyExtraState popStyle'++--+getCurrentListLevel :: ODTReader ListLevel+getCurrentListLevel = currentListLevel <$> getExtraState++--+getPreviousListStartCounter :: Maybe StyleName -> ListLevel+ -> ODTReader (Maybe Int)+getPreviousListStartCounter mStyleName listLevel =+ M.lookup (mStyleName, listLevel) . listContinuationStartCounters+ <$> getExtraState++--+updateMediaWithResource :: (FilePath, B.ByteString) -> ODTReader ()+updateMediaWithResource resource = modifyExtraState (insertMedia' resource)++--+lookupResource :: FilePath -> ODTReader (Maybe B.ByteString)+lookupResource target = M.lookup target . getMediaEnv <$> getExtraState++type AnchorPrefix = T.Text++-- | An adaptation of 'uniqueIdent' from "Text.Pandoc.Shared" that generates a+-- unique identifier but without assuming that the id should be for a header.+-- Second argument is a list of already used identifiers.+uniqueIdentFrom :: AnchorPrefix -> Set.Set Anchor -> Anchor+uniqueIdentFrom baseIdent usedIdents =+ let numIdent n = baseIdent <> "-" <> T.pack (show n)+ in if baseIdent `Set.member` usedIdents+ then maybe baseIdent numIdent+ $ find (\x -> numIdent x `Set.notMember` usedIdents)+ ([1..60000] :: [Int])+ -- if we have more than 60,000, allow repeats+ else baseIdent++-- | First argument: basis for a new "pretty" anchor if none exists yet+-- Second argument: a key ("ugly" anchor)+-- Returns: saved "pretty" anchor or created new one+getPrettyAnchor :: AnchorPrefix -> Anchor -> ODTReader Anchor+getPrettyAnchor baseIdent uglyAnchor = do+ state <- getExtraState+ case lookupPrettyAnchor uglyAnchor state of+ Just prettyAnchor -> return prettyAnchor+ Nothing -> do+ let newPretty = uniqueIdentFrom baseIdent (usedAnchors state)+ modifyExtraState (putPrettyAnchor uglyAnchor newPretty)+ return newPretty++-- | Input: basis for a new header anchor+-- Output: saved new anchor+getHeaderAnchor :: Inlines -> ODTReader Anchor+getHeaderAnchor title = do+ state <- getExtraState+ let exts = extensionsFromList [Ext_auto_identifiers]+ let anchor = uniqueIdent exts (toList title) (usedAnchors state)+ modifyExtraState (putPrettyAnchor anchor anchor)+ return anchor+++--------------------------------------------------------------------------------+-- Working with styles+--------------------------------------------------------------------------------++-- | Read the style referenced by the current element's+-- style-name attribute. Fails if there is no such attribute or if the+-- style cannot be found.+readStyleByName :: ODTReader (StyleName, Style)+readStyleByName = do+ name <- findAttr NsText "style-name"+ style <- getStyleByName name+ return (name, style)++-- | With the @styles@ extension, the name (as shown to the user) of the+-- named style that the current element is an instance of, unless the+-- first argument says to ignore it. If the element's style is an automatic+-- one, its named parent is used. A style that is not defined at all is+-- taken to be a named one, since automatic styles are always defined.+getCustomStyle :: (StyleName -> Bool) -> ODTReader (Maybe T.Text)+getCustomStyle ignored = do+ keep <- keepStyles <$> getExtraState+ if not keep+ then return Nothing+ else do+ fName <- tryC (findAttr NsText "style-name")+ styles <- getStyles+ return $ case fName of+ Right name+ | Just (name', mbStyle) <- lookupNamedStyle name styles+ , not (ignored name')+ -> Just (styleUIName name' mbStyle)+ _ -> Nothing++customStyleAttr :: T.Text -> Attr+customStyleAttr sty = ("", [], [("custom-style", sty)])++-- | Paragraph styles not recorded with the @styles@ extension: the+-- default style, the default styles of table cells, and styles that+-- pandoc interprets itself.+ignoredParaStyle :: StyleName -> Bool+ignoredParaStyle name = name `elem` [ "Standard"+ , "Table_20_Contents"+ , "Table_20_Heading"+ ] || isTableCaptionStyle name++-- | Character styles not recorded with the @styles@ extension.+ignoredCharStyle :: StyleName -> Bool+ignoredCharStyle name = name `elem` [ "Internet_20_link"+ , "Visited_20_Internet_20_Link"+ , "Emphasis"+ , "Strong_20_Emphasis"+ ] || isCodeStyle name++isCodeStyle :: StyleName -> Bool+isCodeStyle "Source_Text" = True+isCodeStyle "Source_20_Text" = True+isCodeStyle _ = False++isTableCaptionStyle :: StyleName -> Bool+isTableCaptionStyle "Table" = True+isTableCaptionStyle _ = False++-- | Apply the text formatting of the current element's style. If the+-- first argument is 'True', the formatting of a named (non-automatic)+-- style is not applied, since it is recorded in a @custom-style@+-- attribute instead; the direct formatting of an automatic style still is.+withNewStyle :: Bool -> ODTReader Inlines -> ODTReader Inlines+withNewStyle styleRecorded reader = do+ fStyle <- tryC readStyleByName+ case fStyle of+ Right (styleName, _) | isCodeStyle styleName ->+ inlineCode <$> reader+ Right (_, style)+ | Just textProps <- textProperties (styleProperties style) -> do+ state <- getExtraState+ let mFamily = styleFamily style+ modifier+ | styleRecorded && not (styleAutomatic style) = id+ | otherwise = modifierFromStyleDiff (state, textProps, mFamily)+ pushStyle style+ inlines <- reader+ popStyle+ return $ modifier inlines+ _ -> reader+ where+ inlineCode :: Inlines -> Inlines+ inlineCode = code . stringifyInlines++type PropertyTriple = (ReaderState, TextProperties, Maybe StyleFamily)+type InlineModifier = Inlines -> Inlines++-- | Given data about the local style changes, calculates how to modify+-- an instance of 'Inlines'+modifierFromStyleDiff :: PropertyTriple -> InlineModifier+modifierFromStyleDiff propertyTriple =+ foldr (.) id $+ getVPosModifier propertyTriple+ : map (\(hasChanged', modifier) ->+ if hasChanged' propertyTriple then modifier else ignore)+ [ (hasEmphChanged , emph )+ , (hasChanged isStrong , strong )+ , (hasChanged strikethrough , strikeout )+ ]+ where+ ignore = id :: InlineModifier++ getVPosModifier :: PropertyTriple -> InlineModifier+ getVPosModifier triple@(_,textProps,_) =+ let getVPos = Just . verticalPosition+ in case lookupPreviousValueM getVPos triple of+ Nothing -> ignore+ Just oldVPos -> getVPosModifier' (oldVPos, verticalPosition textProps)++ getVPosModifier' (oldVPos , newVPos ) | oldVPos == newVPos = ignore+ getVPosModifier' ( _ , VPosSub ) = subscript+ getVPosModifier' ( _ , VPosSuper ) = superscript+ getVPosModifier' ( _ , _ ) = ignore++ hasEmphChanged :: PropertyTriple -> Bool+ hasEmphChanged triple = any ($ triple) [ hasChanged isEmphasised+ , hasChanged underline+ ]++ hasChanged property triple@(_, property -> newProperty, _) =+ (/= Just newProperty) (lookupPreviousValue property triple)++ lookupPreviousValue f = lookupPreviousStyleValue (fmap f . textProperties)++ lookupPreviousValueM f = lookupPreviousStyleValue (f <=< textProperties)++ lookupPreviousStyleValue f (ReaderState{..},_,mFamily)+ = findBy f (extendedStylePropertyChain styleTrace styleSet)+ <|> (f . lookupDefaultStyle' styleSet =<< mFamily)+++type ParaModifier = Blocks -> Blocks++_MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_MM_ :: Int+_MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_PERCENT_ :: Int+_MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_MM_ = 5+_MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_PERCENT_ = 5++-- | Returns either 'id' or 'blockQuote' depending if any of the StyleProperties+-- are indented at quote level.+getParaModifier :: ListLevel -> [StyleProperties] -> ParaModifier+getParaModifier listLevel props+ | listLevel > 0 = id -- see #9505, list paragraphs need indentation+ | any isBlockQuote props = blockQuote+ | otherwise = id+ where+ isBlockQuote SProps {..} | Just paraProps <- paraProperties+ , isQuoteWidth (margin_left paraProps)+ = True+ | otherwise+ = False+ isQuoteWidth mMargin+ | LengthValueMM margin <- mMargin+ , margin > _MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_MM_+ = True+ | PercentValue margin <- mMargin+ , margin > _MINIMUM_INDENTATION_FOR_BLOCKQUOTES_IN_PERCENT_+ = True+ | otherwise+ = False++--+constructPara :: ODTReader Blocks -> ODTReader Blocks+constructPara reader = do+ fStyle <- tryC readStyleByName+ case fStyle of+ Left _ -> reader+ Right (styleName, _) | isTableCaptionStyle styleName ->+ tableCaptionP <$> reader+ Right (_, style) -> do+ styles <- getStyles+ let props = extendedStylePropertyChain [style] styles+ listLevel <- getCurrentListLevel+ getParaModifier listLevel props <$> reader+ where+ tableCaptionP b = divWith ("", ["caption"], []) b++type ListConstructor = [Blocks] -> Blocks++getListConstructor :: ListLevelStyle -> Int -> ListConstructor+getListConstructor ListLevelStyle{..} startNum =+ case listLevelType of+ LltBullet -> bulletList+ LltImage -> bulletList+ LltNumbered -> let listNumberStyle = toListNumberStyle listItemFormat+ listNumberDelim = toListNumberDelim listItemPrefix+ listItemSuffix+ in orderedListWith (startNum, listNumberStyle, listNumberDelim)+ where+ toListNumberStyle LinfNone = DefaultStyle+ toListNumberStyle LinfNumber = Decimal+ toListNumberStyle LinfRomanLC = LowerRoman+ toListNumberStyle LinfRomanUC = UpperRoman+ toListNumberStyle LinfAlphaLC = LowerAlpha+ toListNumberStyle LinfAlphaUC = UpperAlpha+ toListNumberStyle (LinfString _) = Example++ toListNumberDelim Nothing (Just ".") = Period+ toListNumberDelim (Just "" ) (Just ".") = Period+ toListNumberDelim Nothing (Just ")") = OneParen+ toListNumberDelim (Just "" ) (Just ")") = OneParen+ toListNumberDelim (Just "(") (Just ")") = TwoParens+ toListNumberDelim _ _ = DefaultDelim++-- | Determines which style to use for a list, which level to use of that+-- style, and which type of list to create as a result of this information.+-- Then prepares the state for eventual child lists and constructs the list from+-- the results.+-- Two main cases are handled: The list may provide its own style or it may+-- rely on a parent list's style. In the former case the current style in the+-- state must be switched before and after the call to the child converter+-- while in the latter the child converter can be called directly.+-- If anything goes wrong, a default ordered-list-constructor is used.+constructList :: ODTReader [Blocks] -> ODTReader Blocks+constructList reader = do+ modifyExtraState (shiftListLevel 1)+ listLevel <- getCurrentListLevel+ fStyleName <- tryC (findAttr NsText "style-name")+ fContNumbering <- tryC (findAttr NsText "continue-numbering")+ inheritedStyleName <- currentListStyleName <$> getExtraState++ -- Numbering is continued from the previous list with the same+ -- list style (and level), if there is one:+ let mStyleName = either (const inheritedStyleName) Just fStyleName+ listContinuationStartCounter <- getPreviousListStartCounter mStyleName+ listLevel++ let continueNumbering = fContNumbering == Right "true"++ startNumForListLevelStyle mListLevelStyle+ | continueNumbering+ , Just n <- listContinuationStartCounter = n+ | otherwise = maybe 1 listItemStart mListLevelStyle++ constructWith startNum constructor = do+ items <- reader+ modifyExtraState (shiftListLevel (-1))+ modifyExtraState (modifyListContinuationStartCounter mStyleName+ listLevel (startNum + length items))+ return $ constructor items++ constructOrderedList =+ let startNum = startNumForListLevelStyle Nothing+ in constructWith startNum+ (orderedListWith (startNum, DefaultStyle, DefaultDelim))++ constructListWith listLevelStyle =+ let startNum = startNumForListLevelStyle (Just listLevelStyle)+ in constructWith startNum (getListConstructor listLevelStyle startNum)++ case fStyleName of+ Right styleName -> do+ fListStyle <- tryC (lookupListStyle styleName)+ case fListStyle of+ Right listStyle ->+ case getListLevelStyle listLevel listStyle of+ Just listLevelStyle -> do+ oldListStyle <- switchCurrentListStyle+ (Just (styleName, listStyle))+ blocks <- constructListWith listLevelStyle+ _ <- switchCurrentListStyle oldListStyle+ return blocks+ Nothing -> constructOrderedList+ Left _ -> constructOrderedList+ Left _ -> do+ mListStyle <- currentListStyle <$> getExtraState+ case mListStyle of+ Just listStyle ->+ case getListLevelStyle listLevel listStyle of+ Just listLevelStyle -> constructListWith listLevelStyle+ Nothing -> constructOrderedList+ Nothing -> constructOrderedList++--------------------------------------------------------------------------------+-- Readers+--------------------------------------------------------------------------------++type Matcher result = ElementMatcher Namespace ReaderState result++type InlineMatcher = Matcher Inlines++type BlockMatcher = Matcher Blocks++newtype FirstMatch a = FirstMatch (Alt Maybe a)+ deriving (Foldable, Monoid, Semigroup)++firstMatch :: a -> FirstMatch a+firstMatch = FirstMatch . Alt . Just++newtype CombiningBlocks = CombiningBlocks { unCombiningBlocks :: Blocks }++instance Semigroup CombiningBlocks where+ CombiningBlocks l <> CombiningBlocks r = CombiningBlocks (combineBlocks l r)++instance Monoid CombiningBlocks where+ mempty = CombiningBlocks mempty++--+matchingElement :: Namespace -> ElementName+ -> ODTReader e+ -> Matcher e+matchingElement ns name reader = (ns, name, reader)++--+matchSmushedChildBlocks' :: [Matcher CombiningBlocks] -> ODTReader Blocks+matchSmushedChildBlocks' ls = unCombiningBlocks <$> matchContent' ls++--------------------------------------------+-- Matchers+--------------------------------------------++----------------------+-- Basics+----------------------++--+-- | Open Document allows several consecutive spaces if they are marked up.+-- Text content of the current element is read with this fallback converter.+read_plain_text :: XML.Content -> ODTReader Inlines+read_plain_text (XML.Text cData) = return $ text $ XML.cdData cData+read_plain_text _ = return mempty++read_text_seq :: InlineMatcher+read_text_seq = matchingElement NsText "sequence"+ $ matchContent [] read_plain_text+++-- specifically. I honor that, although the current implementation of 'mappend'+-- for 'Inlines' in "Text.Pandoc.Builder" will collapse them again.+-- The rational is to be prepared for future modifications.+-- The repeat count is attacker-controlled and unbounded in ODF, so cap+-- it: a 832-byte document with @text:c="200000000"@ would otherwise+-- exhaust memory.+_MAX_SPACES_ :: Int+_MAX_SPACES_ = 1000++read_spaces :: InlineMatcher+read_spaces = matchingElement NsText "s" $ do+ count <- readAttrWithDefault NsText "c" 1 -- how many spaces?+ return $ fromList (replicate (min count _MAX_SPACES_) Space)+--+read_line_break :: InlineMatcher+read_line_break = matchingElement NsText "line-break"+ $ return linebreak+--+read_tab :: InlineMatcher+read_tab = matchingElement NsText "tab"+ $ return space+--+read_span :: InlineMatcher+read_span = matchingElement NsText "span" $ do+ mbCustom <- getCustomStyle ignoredCharStyle+ inlines <- withNewStyle (isJust mbCustom)+ matchInlineContent+ return $ case mbCustom of+ Just sty | not (null inlines)+ -> spanWith (customStyleAttr sty) inlines+ _ -> inlines++-- | ODF wraps inline content in a few elements that carry nothing but+-- metadata: @text:meta@ holds RDFa annotations, @text:meta-field@ marks+-- up bibliographic data. They are transparent as far as we are concerned.+read_meta :: InlineMatcher+read_meta = matchingElement NsText "meta" matchInlineContent++read_meta_field :: InlineMatcher+read_meta_field = matchingElement NsText "meta-field" matchInlineContent++-- | A ruby annotation glosses a base text. Pandoc cannot represent the+-- gloss, so keep the base text rather than dropping both.+read_ruby :: InlineMatcher+read_ruby = matchingElement NsText "ruby"+ $ matchContent' [ matchingElement NsText "ruby-base"+ matchInlineContent ]++-- | Fields hold a value that the producing application computes, such as+-- a page number, a document property or a user variable. The element+-- content is the text that application last displayed for the field,+-- which is the best rendering available to us.+read_fields :: [InlineMatcher]+read_fields = [ matchingElement NsText name (matchContent [] read_plain_text)+ | name <- fieldElements ]++fieldElements :: [ElementName]+fieldElements =+ [ "page-number", "page-count", "page-continuation", "page-variable-get"+ , "date", "time", "creation-date", "creation-time"+ , "modification-date", "modification-time"+ , "print-date", "print-time", "printed-by"+ , "editing-cycles", "editing-duration"+ , "author-name", "author-initials", "initial-creator", "creator"+ , "title", "subject", "description", "keywords"+ , "chapter", "file-name", "template-name", "sheet-name"+ , "variable-get", "variable-set", "variable-input"+ , "user-defined", "user-field-get", "user-field-input"+ , "expression", "text-input", "placeholder"+ , "word-count", "character-count", "paragraph-count"+ , "table-count", "image-count", "object-count"+ , "database-display", "database-name", "database-row-number"+ , "sequence-ref", "note-ref"+ , "sender-firstname", "sender-lastname", "sender-initials"+ , "sender-title", "sender-position", "sender-email"+ , "sender-company", "sender-street", "sender-city"+ , "sender-postal-code", "sender-country", "sender-state-or-province"+ , "sender-phone-private", "sender-phone-work", "sender-fax"+ ]++--+read_paragraph :: Matcher CombiningBlocks+read_paragraph = read_paragraph' True++-- | Read a paragraph. If the argument is 'True' and the @styles@ extension+-- is enabled, a paragraph with a named style (other than one of the+-- 'ignoredParaStyle's) is wrapped in a Div with a @custom-style@+-- attribute. This is not done for the paragraphs of list items, which+-- would make every list loose.+read_paragraph' :: Bool -> Matcher CombiningBlocks+read_paragraph' recordStyle = matchingElement NsText "p" $ fmap CombiningBlocks $ do+ fStyle <- tryC readStyleByName+ listLevel <- getCurrentListLevel+ mbCustom <- if recordStyle && listLevel == 0+ then getCustomStyle ignoredParaStyle+ else return Nothing+ maybe id (divWith . customStyleAttr) mbCustom <$>+ case fStyle of+ Right style | isPreformattedStyle style ->+ codeBlock . stringifyInlines <$> matchInlineContent+ _ ->+ constructPara (para <$> withNewStyle (isJust mbCustom)+ matchInlineContent)+ where+ isPreformattedStyle :: (StyleName, Style) -> Bool+ isPreformattedStyle ("Preformatted_20_Text", _) = True+ isPreformattedStyle (_, Style { styleParentName = Just "Preformatted_20_Text" }) = True+ isPreformattedStyle _ = False+++-- | Reads the inline content of a paragraph, heading or span.+matchInlineContent :: ODTReader Inlines+matchInlineContent = matchContent inlineMatchers read_plain_text++inlineMatchers :: [InlineMatcher]+inlineMatchers = [ read_span+ , read_spaces+ , read_line_break+ , read_tab+ , read_link+ , read_note+ , read_citation+ , read_bookmark+ , read_bookmark_start+ , read_reference_start+ , read_bookmark_ref+ , read_reference_ref+ , read_frame+ , read_text_seq+ , read_meta+ , read_meta_field+ , read_ruby+ ] ++ read_fields++-- | Reads the block-level content that ODF permits wherever it permits a+-- paragraph: in the body of the document, and in a section, a list item, a+-- footnote or a table cell.+matchBlockContent :: ODTReader Blocks+matchBlockContent = matchSmushedChildBlocks' blockMatchers++blockMatchers :: [Matcher CombiningBlocks]+blockMatchers = [ read_paragraph+ , read_header+ , read_list+ , read_table+ , read_section+ ]+++----------------------+-- Headers+----------------------++--+read_header :: Matcher CombiningBlocks+read_header = matchingElement NsText "h" $ do+ level <- readAttrWithDefault NsText "outline-level" 1+ children <- matchInlineContent+ anchor <- getHeaderAnchor children+ -- with the styles extension, record any style other than the+ -- standard one for the heading's level+ mbCustom <- getCustomStyle (\name -> name == "Heading_20_" <> tshow level+ || name == "Heading")+ let idAttr = (anchor, [], maybe [] (\sty -> [("custom-style", sty)]) mbCustom)+ return $ CombiningBlocks $ headerWith idAttr level children++----------------------+-- Lists+----------------------++--+read_list :: Matcher CombiningBlocks+read_list = matchingElement NsText "list"+ $ CombiningBlocks+ <$> constructList+ ( matchContent' [ read_list_item+ , read_list_header+ ] )+--+read_list_item :: Matcher [Blocks]+read_list_item = read_list_element "list-item"++read_list_header :: Matcher [Blocks]+read_list_header = read_list_element "list-header"++read_list_element :: ElementName -> Matcher [Blocks]+read_list_element listElement = matchingElement NsText listElement+ $ do+ -- a new item restarts the numbering+ -- of nested lists:+ listLevel <- getCurrentListLevel+ modifyExtraState+ (resetDeeperListContinuationStartCounters+ listLevel)+ compactify . (:[]) <$> matchBlockContent++----------------------+-- Sections+----------------------++read_section :: Matcher CombiningBlocks+read_section = matchingElement NsText "section"+ $ CombiningBlocks . divWith nullAttr+ <$> matchBlockContent+++----------------------+-- Links+----------------------++read_link :: InlineMatcher+read_link = matchingElement NsText "a"+ $ link+ <$> (fixRelativeLink+ <$> findAttrWithDefault NsXLink "href" "")+ <*> findAttrWithDefault NsOffice "title" ""+ <*> matchContent [ read_span+ , read_note+ , read_citation+ , read_bookmark+ , read_bookmark_start+ , read_reference_start+ , read_bookmark_ref+ , read_reference_ref+ ] read_plain_text++fixRelativeLink :: T.Text -> T.Text+fixRelativeLink uri =+ case parseRelativeReference (T.unpack uri) of+ Nothing -> uri+ Just u ->+ case uriPath u of+ '.':'.':'/':xs -> tshow $ u{ uriPath = xs }+ _ -> uri++-------------------------+-- Footnotes+-------------------------++read_note :: InlineMatcher+read_note = matchingElement NsText "note"+ $ note <$> matchContent' [ read_note_body ]++read_note_body :: BlockMatcher+read_note_body = matchingElement NsText "note-body" matchBlockContent++-------------------------+-- Citations+-------------------------++read_citation :: InlineMatcher+read_citation = matchingElement NsText "bibliography-mark"+ $ cite+ <$> ( makeCitation+ <$> findAttrWithDefault NsText "identifier" ""+ <*> readAttrWithDefault NsText "number" 0 )+ <*> matchContent [] read_plain_text+ where+ makeCitation :: T.Text -> Int -> [Citation]+ makeCitation citeId num = [Citation citeId [] [] NormalCitation num 0]+++----------------------+-- Tables+----------------------++--+read_table :: Matcher CombiningBlocks+read_table = matchingElement NsTable "table"+ $ fmap (CombiningBlocks . table')+ $ (,) <$> matchContent' read_header_rows+ <*> matchContent' read_body_rows++-- | A table without a caption.+table' :: ([[Cell]], [[Cell]]) -> Blocks+table' (headers, rows) = compactifyTable $+ table emptyCaption (replicate numcols defaults) th [tb] tf+ where+ defaults = (AlignDefault, ColWidthDefault)+ -- A table element need not contain any rows, so guard against+ -- taking the maximum of an empty list.+ numcols = maybe 0 maximum $ nonEmpty $ map rowWidth $ headers ++ rows+ -- A cell spanning several columns occupies all of them.+ rowWidth = sum . map (\(Cell _ _ _ (ColSpan cs) _) -> cs)+ toRow = Row nullAttr+ th = TableHead nullAttr $ map toRow headers+ tb = TableBody nullAttr 0 [] $ map toRow rows+ tf = TableFoot nullAttr []++-- | Rows need not be immediate children of the table: ODF lets them be+-- wrapped in any number of @table:table-row-group@ and @table:table-rows@+-- elements, which only group them for the benefit of outlining and+-- formatting. The children are walked twice, once picking up the rows that a+-- @table:table-header-rows@ marks as headers and once the remaining ones.+read_header_rows :: [Matcher [[Cell]]]+read_header_rows = [ matchingElement NsTable "table-header-rows"+ $ matchContent' [ read_table_row ]+ , matchingElement NsTable "table-row-group"+ $ matchContent' read_header_rows+ ]++read_body_rows :: [Matcher [[Cell]]]+read_body_rows = [ read_table_row+ , matchingElement NsTable "table-rows"+ $ matchContent' [ read_table_row ]+ , matchingElement NsTable "table-row-group"+ $ matchContent' read_body_rows+ ]++-- | ODF abbreviates a run of identical rows or cells with a repeat count.+-- The counts are unbounded in the format -- spreadsheets pad out to the end+-- of the sheet with them -- so a handful of bytes could otherwise ask for an+-- arbitrarily large table. Bound both the width a single cell element stands+-- for and the number of cells a single row element expands to; capping the+-- two counts separately would not do, as they multiply.+_MAX_COLUMNS_, _MAX_ROW_CELLS_ :: Int+_MAX_COLUMNS_ = 1000+_MAX_ROW_CELLS_ = 10000++repeated :: ElementName -> ODTReader Int+repeated attrName = max 1 <$> readAttrWithDefault NsTable attrName 1++--+read_table_row :: Matcher [[Cell]]+read_table_row = matchingElement NsTable "table-row"+ $ row'+ <$> matchContent' [ read_table_cell+ ]+ <*> repeated "number-rows-repeated"+ where+ row' cells repeat' =+ replicate (max 1 $ min repeat'+ $ _MAX_ROW_CELLS_ `div` max 1 (length cells)) cells++--+read_table_cell :: Matcher [Cell]+read_table_cell = matchingElement NsTable "table-cell"+ $ cell'+ <$> (RowSpan <$> readAttrWithDefault NsTable "number-rows-spanned" 1)+ <*> (ColSpan <$> readAttrWithDefault NsTable "number-columns-spanned" 1)+ <*> (min _MAX_COLUMNS_ <$> repeated "number-columns-repeated")+ <*> matchBlockContent+ where+ cell' rowSpan colSpan repeat' blocks =+ concat $ replicate repeat'+ $ map (cell AlignDefault rowSpan colSpan) (compactify [blocks])++----------------------+-- Frames+----------------------++--+read_frame :: InlineMatcher+read_frame = matchingElement NsDraw "frame" $ do+ children <- filterChildrenName' NsDraw (`elem` ["image", "object", "text-box"])+ fold . mconcat <$> mapM read_frame_child children++read_frame_child :: XML.Element -> ODTReader (FirstMatch Inlines)+read_frame_child child =+ case elName child of+ "image" -> read_frame_img child+ "object" -> read_frame_mathml child+ "text-box" -> read_frame_text_box child+ _ -> return mempty++read_frame_img :: XML.Element -> ODTReader (FirstMatch Inlines)+read_frame_img img = do+ mbSrc <- executeIn img resolveImageSrc+ case mbSrc of+ Nothing -> return mempty+ Just src -> do+ -- note: the remaining attributes are read in the context of the+ -- enclosing draw:frame, not of the draw:image+ let exts = extensionsFromList [Ext_auto_identifiers]+ w <- findAttr' NsSVG "width"+ h <- findAttr' NsSVG "height"+ titleNodes <- matchContent' [ read_frame_title ]+ -- trimmed because a pretty-printed file indents the children of+ -- the frame, and that whitespace is not alt text+ alt <- trimInlines <$> matchContent [] read_plain_text+ return $ firstMatch+ $ imageWith (image_attributes w h) src+ (inlineListToIdentifier exts (toList titleNodes))+ alt++-- | Determine the media bag path of the image of the current+-- @draw:image@ element. The image data is either in another file of the+-- archive, referenced by @xlink:href@, or embedded in the element as+-- base64 in an @office:binary-data@ child (which is how a flat+-- OpenDocument file must carry it).+resolveImageSrc :: ODTReader (Maybe T.Text)+resolveImageSrc = do+ href <- fold <$> findAttr' NsXLink "href"+ if not (T.null href)+ then do+ let src = fixRelativeLink href+ -- The archive need not actually contain the referenced file; if it+ -- does not, leave the media bag alone and just emit the link.+ mapM_ (updateMediaWithResource . (,) (T.unpack src))+ =<< lookupResource (T.unpack src)+ return (Just src)+ else do+ drawImage <- getCurrentElement+ findChild' NsOffice "binary-data"+ >>= traverse (embedBase64Image drawImage)++-- | Add the base64-encoded image data of an @office:binary-data@ element+-- to the media bag and return the path under which it was filed. The+-- mime type is declared on the enclosing @draw:image@.+embedBase64Image :: XML.Element -> XML.Element -> ODTReader T.Text+embedBase64Image drawImage binaryData = do+ let bytes = decodeLenient . UTF8.fromText+ . T.filter (not . isSpace) . XML.strContent $ binaryData+ -- the attribute is draw:mime-type in ODF 1.3 but loext:mime-type in+ -- older LibreOffice output, so match on the local name only+ let mbExtension =+ ((("." <>) <$>) . extensionFromMimeType+ =<< XML.findAttrBy ((== "mime-type") . XML.qName) drawImage)+ <|> (extensionForImageType <$> imageType bytes)+ n <- length . mediaItems . getMediaBag <$> getExtraState+ let path = "Pictures/image" <> show (n + 1) <> T.unpack (fromMaybe ".bin" mbExtension)+ updateMediaWithResource (path, B.fromStrict bytes)+ return (T.pack path)++extensionForImageType :: ImageType -> T.Text+extensionForImageType imgType =+ case imgType of+ Png -> ".png"+ Jpeg -> ".jpeg"+ Gif -> ".gif"+ Pdf -> ".pdf"+ Eps -> ".eps"+ Svg -> ".svg"+ Emf -> ".emf"+ Tiff -> ".tiff"+ Webp -> ".webp"+ Avif -> ".avif"++read_frame_title :: InlineMatcher+read_frame_title = matchingElement NsSVG "title" (matchContent [] read_plain_text)++image_attributes :: Maybe T.Text -> Maybe T.Text -> Attr+image_attributes x y =+ ( "", [], dim "width" x ++ dim "height" y)+ where+ dim _ (Just "") = []+ dim name (Just v) = [(name, v)]+ dim _ Nothing = []++read_frame_mathml :: XML.Element -> ODTReader (FirstMatch Inlines)+read_frame_mathml obj = do+ href <- fold <$> executeIn obj (findAttr' NsXLink "href")+ mbMathML <-+ if T.null href+ then -- a flat OpenDocument file has to embed the formula in the+ -- draw:object itself+ return $ XML.showElement . addMathMLNamespace <$> findMathML obj+ else do+ -- note that pandoc's own odt writer uses a trailing slash+ let dir = T.dropWhileEnd (== '/') $+ fromMaybe href (T.stripPrefix "./" href)+ fmap (UTF8.toText . B.toStrict) <$>+ lookupResource (T.unpack (dir <> "/content.xml"))+ case readMathML <$> mbMathML of+ Just (Right exps) -> return $ firstMatch $ displayMath $ writeTeX exps+ _ -> return mempty++-- | Find the MathML of a formula embedded in a @draw:object@. It is+-- searched for as a descendant, not as a direct child, because the+-- @math@ element may be wrapped in an @office:document@ element+-- describing an embedded formula document.+findMathML :: XML.Element -> Maybe XML.Element+findMathML = XML.filterElementName ((== "math") . XML.qName)++-- | XML.Light carries namespace prefixes literally and does not+-- synthesise declarations, so the declaration of the MathML namespace+-- may have been left behind on the root of the flat file.+addMathMLNamespace :: XML.Element -> XML.Element+addMathMLNamespace el =+ el{ XML.elAttribs =+ XML.Attr (XML.QName "xmlns" Nothing Nothing)+ "http://www.w3.org/1998/Math/MathML"+ : filter (not . isDefaultNS . XML.attrKey) (XML.elAttribs el) }+ where+ isDefaultNS qn = XML.qName qn == "xmlns" && isNothing (XML.qPrefix qn)++read_frame_text_box :: XML.Element -> ODTReader (FirstMatch Inlines)+read_frame_text_box box = do+ -- caption paragraphs must not be wrapped in Divs (see+ -- 'read_img_with_caption')+ paragraphs <- executeIn box (matchSmushedChildBlocks' [ read_paragraph' False ])+ return $ read_img_with_caption $ toList paragraphs++read_img_with_caption :: [Block] -> FirstMatch Inlines+read_img_with_caption (Para [Image attr alt (src,title)] : _) =+ firstMatch $ singleton (Image attr alt (src, "fig:" <> title)) -- no text, default caption+read_img_with_caption (Para (Image attr _ (src,title) : txt) : _) =+ firstMatch $ singleton (Image attr txt (src, "fig:" <> title) ) -- override caption with the text that follows+read_img_with_caption ( Para (_ : xs) : ys) =+ read_img_with_caption (Para xs : ys)+read_img_with_caption _ =+ mempty++----------------------+-- Internal links+----------------------++_ANCHOR_PREFIX_ :: T.Text+_ANCHOR_PREFIX_ = "anchor"++--+readAnchorAttr :: ODTReader Anchor+readAnchorAttr = findAttr NsText "name"++-- | Beware: may fail+findAnchorName :: AnchorPrefix -> ODTReader Anchor+findAnchorName anchorPrefix = do+ uglyAnchor <- readAnchorAttr+ getPrettyAnchor anchorPrefix uglyAnchor++--+maybeAddAnchorFrom :: ODTReader AnchorPrefix -> ODTReader Inlines+maybeAddAnchorFrom anchorReader =+ (toAnchorElem <$> (anchorReader >>= findAnchorName))+ <|> return mempty+ where+ toAnchorElem :: Anchor -> Inlines+ toAnchorElem anchorID = spanWith (anchorID, [], []) mempty+ -- no classes, no key-value pairs++--+read_bookmark :: InlineMatcher+read_bookmark = matchingElement NsText "bookmark"+ $ maybeAddAnchorFrom (return _ANCHOR_PREFIX_)++--+read_bookmark_start :: InlineMatcher+read_bookmark_start = matchingElement NsText "bookmark-start"+ $ maybeAddAnchorFrom (return _ANCHOR_PREFIX_)++--+read_reference_start :: InlineMatcher+read_reference_start = matchingElement NsText "reference-mark-start"+ $ maybeAddAnchorFrom readAnchorAttr++-- | Beware: may fail+findAnchorRef :: ODTReader Anchor+findAnchorRef = do+ uglyAnchor <- findAttr NsText "ref-name"+ getPrettyAnchor _ANCHOR_PREFIX_ uglyAnchor++--+maybeInAnchorRef :: Inlines -> ODTReader Inlines+maybeInAnchorRef inlines = do+ fRef <- tryC findAnchorRef+ case fRef of+ Right anchor -> return $ toAnchorRef anchor inlines+ Left _ -> return inlines+ where+ toAnchorRef :: Anchor -> Inlines -> Inlines+ toAnchorRef anchor = link ("#" <> anchor) "" -- no title++--+read_bookmark_ref :: InlineMatcher+read_bookmark_ref = matchingElement NsText "bookmark-ref"+ $ matchContent [] read_plain_text >>= maybeInAnchorRef++--+read_reference_ref :: InlineMatcher+read_reference_ref = matchingElement NsText "reference-ref"+ $ matchContent [] read_plain_text >>= maybeInAnchorRef+++----------------------+-- Entry point+----------------------++read_text :: ODTReader Pandoc+read_text = doc <$> matchBlockContent++post_process :: Pandoc -> Pandoc+post_process = walk (unwrapCaptions . attachCaptions)++-- | Paragraphs styled as table captions are read as a Div with class+-- @caption@ (see 'constructPara'). Attach each of them to the adjacent+-- table; ODF puts the caption either before or after its table.+attachCaptions :: [Block] -> [Block]+attachCaptions (Table attr _ specs th tb tf : b : xs)+ | Just blks <- captionBlocks b+ = Table attr (Caption Nothing blks) specs th tb tf : attachCaptions xs+attachCaptions (b : Table attr _ specs th tb tf : xs)+ | Just blks <- captionBlocks b+ = Table attr (Caption Nothing blks) specs th tb tf : attachCaptions xs+attachCaptions (b : bs) = b : attachCaptions bs+attachCaptions [] = []++-- | The Div is only an internal marker, so a caption paragraph that+-- turned out not to belong to a table is emitted as an ordinary+-- paragraph rather than leaking the marker into the output.+unwrapCaptions :: [Block] -> [Block]+unwrapCaptions = concatMap (\b -> fromMaybe [b] (captionBlocks b))++captionBlocks :: Block -> Maybe [Block]+captionBlocks (Div ("", ["caption"], _) blks) = Just blks+captionBlocks _ = Nothing++read_body :: ODTReader (Pandoc, MediaBag)+read_body = executeInSub NsOffice "body"+ $ executeInSub NsOffice "text"+ $ do+ txt <- read_text+ state <- getExtraState+ return (post_process txt, getMediaBag state)
@@ -9,28 +9,13 @@ Data types and utilities representing failure. Most of it is based on the "Either" type in its usual configuration (left represents failure).--In most cases, the failure type is implied or required to be a "Monoid".--The choice of "Either" instead of a custom type makes it easier to write-compatible instances of "ArrowChoice". -} --- We export everything module Text.Pandoc.Readers.ODT.Generic.Fallible ( Failure , Fallible- , maybeToEither- , eitherToMaybe- , recover- , failWith- , failEmpty- , succeedWith- , collapseEither , chooseMax , chooseMaxWith- , ChoiceVector(..)- , SuccessList(..) ) where -- | Default for now. Will probably become a class at some point.@@ -38,41 +23,6 @@ type Fallible a = Either Failure a -----maybeToEither :: Maybe a -> Fallible a-maybeToEither (Just a) = Right a-maybeToEither Nothing = Left ()-----eitherToMaybe :: Either _l a -> Maybe a-eitherToMaybe (Left _) = Nothing-eitherToMaybe (Right a) = Just a---- | > recover a === either (const a) id-recover :: a -> Either _f a -> a-recover a (Left _) = a-recover _ (Right a) = a---- | I would love to use 'fail'. Alas, 'Monad.fail'...-failWith :: failure -> Either failure _x-failWith f = Left f-----failEmpty :: (Monoid failure) => Either failure _x-failEmpty = failWith mempty-----succeedWith :: a -> Either _x a-succeedWith = Right-----collapseEither :: Either failure (Either failure x)- -> Either failure x-collapseEither (Left f ) = Left f-collapseEither (Right (Left f)) = Left f-collapseEither (Right (Right x)) = Right x- -- | If either of the values represents a non-error, the result is a -- (possibly combined) non-error. If both values represent an error, an error -- is returned.@@ -90,24 +40,3 @@ chooseMaxWith _ (Left a) (Left b) = Left $ a `mappend` b chooseMaxWith _ (Right a) _ = Right a chooseMaxWith _ _ (Right b) = Right b----- | Class of containers that can escalate contained 'Either's.--- The word "Vector" is meant in the sense of a disease transmitter.-class ChoiceVector v where- spreadChoice :: v (Either f a) -> Either f (v a)--instance ChoiceVector ((,) a) where- spreadChoice (_, Left f) = Left f- spreadChoice (x, Right y) = Right (x,y)- -- Wasn't there a newtype somewhere with the elements flipped?---- | Wrapper for a list. While the normal list instance of 'ChoiceVector'--- fails whenever it can, this type will never fail.-newtype SuccessList a = SuccessList { collectNonFailing :: [a] }- deriving ( Eq, Ord, Show )--instance ChoiceVector SuccessList where- spreadChoice = Right . SuccessList . foldr unTagRight [] . collectNonFailing- where unTagRight (Right x) = (x:)- unTagRight _ = id
@@ -12,71 +12,24 @@ -} module Text.Pandoc.Readers.ODT.Generic.Utils-( uncurry3-, uncurry4-, uncurry5-, uncurry6-, swap-, reverseComposition-, tryToRead+( tryToRead , Lookupable(..) , readLookupable , readPercent , findBy-, swing-, composition ) where -import Control.Category (Category, (<<<), (>>>))-import qualified Control.Category as Cat (id) import Data.Char (isSpace)-import qualified Data.Foldable as F (Foldable, foldr) import Data.Maybe import Data.Text (Text) import qualified Data.Text as T --- | Equivalent to--- > foldr (.) id--- where '(.)' are 'id' are the ones from "Control.Category"--- and 'foldr' is the one from "Data.Foldable".--- The noun-form was chosen to be consistent with 'sum', 'product' etc--- based on the discussion at--- <https://groups.google.com/forum/#!topic/haskell-cafe/VkOZM1zaHOI>--- (that I was not part of)-composition :: (Category cat, F.Foldable f) => f (cat a a) -> cat a a-composition = F.foldr (<<<) Cat.id---- | Equivalent to--- > foldr (flip (.)) id--- where '(.)' are 'id' are the ones from "Control.Category"--- and 'foldr' is the one from "Data.Foldable".--- A reversed version of 'composition'.-reverseComposition :: (Category cat, F.Foldable f) => f (cat a a) -> cat a a-reverseComposition = F.foldr (>>>) Cat.id---- | This function often makes it possible to switch values with the functions--- that are applied to them.------ Examples:--- > swing map :: [a -> b] -> a -> [b]--- > swing any :: [a -> Bool] -> a -> Bool--- > swing foldr :: b -> a -> [a -> b -> b] -> b--- > swing scanr :: c -> a -> [a -> c -> c] -> c--- > swing zipWith :: [a -> b -> c] -> a -> [b] -> [c]--- > swing find :: [a -> Bool] -> a -> Maybe (a -> Bool)------ Stolen from <https://wiki.haskell.org/Pointfree>-swing :: (((a -> b) -> b) -> c -> d) -> c -> a -> d-swing = flip.(.flip id)--- swing f c a = f ($ a) c-- -- | Alternative to 'read'/'reads'. The former of these throws errors -- (nobody wants that) while the latter returns "to much" for simple purposes. -- This function instead applies 'reads' and returns the first match (if any) -- in a 'Maybe'. tryToRead :: (Read r) => Text -> Maybe r-tryToRead = (reads . T.unpack) >>> listToMaybe >>> fmap fst+tryToRead = fmap fst . listToMaybe . reads . T.unpack -- | A version of 'reads' that requires a '%' sign after the number readPercent :: ReadS Int@@ -93,19 +46,6 @@ readLookupable :: (Lookupable a) => Text -> Maybe a readLookupable s = lookup (T.takeWhile (not . isSpace) $ T.dropWhile isSpace s) lookupTable--uncurry3 :: (a->b->c -> z) -> (a,b,c ) -> z-uncurry4 :: (a->b->c->d -> z) -> (a,b,c,d ) -> z-uncurry5 :: (a->b->c->d->e -> z) -> (a,b,c,d,e ) -> z-uncurry6 :: (a->b->c->d->e->f -> z) -> (a,b,c,d,e,f ) -> z--uncurry3 fun (a,b,c ) = fun a b c-uncurry4 fun (a,b,c,d ) = fun a b c d-uncurry5 fun (a,b,c,d,e ) = fun a b c d e-uncurry6 fun (a,b,c,d,e,f ) = fun a b c d e f--swap :: (a,b) -> (b,a)-swap (a,b) = (b,a) -- | A version of "Data.List.find" that uses a converter to a Maybe instance. -- The returned value is the first which the converter returns in a 'Just'
@@ -1,8 +1,4 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE GADTs #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE PatternGuards #-} {- | Module : Text.Pandoc.Readers.ODT.Generic.XMLConverter Copyright : Copyright (C) 2015 Martin Linnemann@@ -12,35 +8,34 @@ Stability : alpha Portability : portable -A generalized XML parser based on stateful arrows.-It might be sufficient to define this reader as a comonad, but there is-not a lot of use in trying.+A generalized monadic XML parser. The parser navigates through an XML+tree, always looking at a \"current element\", and carries some+additional, converter-specific state. -} module Text.Pandoc.Readers.ODT.Generic.XMLConverter ( ElementName-, XMLConverterState , XMLConverter-, FallibleXMLConverter-, runConverter'+, runConverter+, fromMaybeF+, fromFallible+, tryC , getExtraState , setExtraState , modifyExtraState-, producingExtraState+, getCurrentElement+, elName , findChild' , filterChildrenName' , isSet' , isSetWithDefault-, elName , searchAttr , lookupAttr , lookupAttr' , lookupDefaultingAttr , findAttr'-, findAttrText' , findAttr-, findAttrText-, findAttrTextWithDefault+, findAttrWithDefault , readAttr , readAttr' , readAttrWithDefault@@ -49,271 +44,157 @@ , executeInSub , withEveryL , tryAll+, ElementMatcher , matchContent' , matchContent ) where -import Prelude hiding (Applicative(..))-import Control.Applicative hiding ( liftA, liftA2 )-import Control.Monad ( MonadPlus )-import Control.Arrow+import Control.Applicative ( Alternative(..), optional )+import Control.Monad ( filterM, foldM )+import Control.Monad.Except ( ExceptT, runExceptT, throwError, catchError )+import Control.Monad.State ( State, evalState, get, gets, put, modify ) -import Data.Bool ( bool )-import Data.Either ( rights )-import qualified Data.Map as M-import Data.Text (Text)-import Data.Default-import Data.Maybe-import qualified Data.List as L-import qualified Data.List.NonEmpty as NonEmpty-import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.Map as M+import Data.Text (Text)+import Data.Default+import Data.Maybe import qualified Text.Pandoc.XML.Light as XML -import Text.Pandoc.Readers.ODT.Arrows.State-import Text.Pandoc.Readers.ODT.Arrows.Utils-import Text.Pandoc.Readers.ODT.Generic.Namespaces-import Text.Pandoc.Readers.ODT.Generic.Utils-import Text.Pandoc.Readers.ODT.Generic.Fallible+import Text.Pandoc.Readers.ODT.Generic.Namespaces+import Text.Pandoc.Readers.ODT.Generic.Utils+import Text.Pandoc.Readers.ODT.Generic.Fallible -------------------------------------------------------------------------------- -- Basis types for readability -------------------------------------------------------------------------------- --- type ElementName = Text type AttributeName = Text type AttributeValue = Text-type TextAttributeValue = Text --- type NameSpacePrefix = Text ----type NameSpacePrefixes nsID = M.Map nsID NameSpacePrefix- ----------------------------------------------------------------------------------- Main converter state+-- Converter state -------------------------------------------------------------------------------- --- GADT so some of the NameSpaceID restrictions can be deduced-data XMLConverterState nsID extraState where- XMLConverterState :: NameSpaceID nsID- => { -- | A stack of parent elements. The top element is the current one.- -- Arguably, a real Zipper would be better. But that is an- -- optimization that can be made at a later time, e.g. when- -- replacing Text.XML.Light.- parentElements :: NonEmpty XML.Element- -- | A map from internal namespace IDs to the namespace prefixes- -- used in XML elements- , namespacePrefixes :: NameSpacePrefixes nsID- -- | A map from internal namespace IDs to namespace IRIs- -- (Only necessary for matching namespace IDs and prefixes)- , namespaceIRIs :: NameSpaceIRIs nsID- -- | A place to put "something else". This feature is used heavily- -- to keep the main code cleaner. More specifically, the main reader- -- is divided into different stages. Each stage lifts something up- -- here, which the next stage can then use. This could of course be- -- generalized to a state-tree or used for the namespace IRIs. The- -- border between states and values is an imaginary one, after all.- -- But the separation as it is seems to be enough for now.- , moreState :: extraState- }- -> XMLConverterState nsID extraState+data XMLConverterState nsID extraState = XMLConverterState+ { -- | The element that is currently being read+ currentElement :: XML.Element+ -- | A map from internal namespace IDs to the namespace prefixes+ -- used in XML elements+ , namespacePrefixes :: M.Map nsID NameSpacePrefix+ -- | A map from internal namespace IDs to namespace IRIs+ -- (Only necessary for matching namespace IDs and prefixes)+ , namespaceIRIs :: NameSpaceIRIs nsID+ -- | Converter-specific state+ , moreState :: extraState+ } --- createStartState :: (NameSpaceID nsID)- => XML.Element- -> extraState- -> XMLConverterState nsID extraState+ => XML.Element+ -> extraState+ -> XMLConverterState nsID extraState createStartState element extraState = XMLConverterState- { parentElements = element :| []+ { currentElement = element , namespacePrefixes = M.empty , namespaceIRIs = getInitialIRImap , moreState = extraState } --- | Functor over extra state-instance Functor (XMLConverterState nsID) where- fmap f ( XMLConverterState parents prefixes iRIs extraState )- = XMLConverterState parents prefixes iRIs (f extraState)-----replaceExtraState :: extraState- -> XMLConverterState nsID _x- -> XMLConverterState nsID extraState-replaceExtraState x s- = fmap (const x) s-----currentElement :: XMLConverterState nsID extraState- -> XML.Element-currentElement state = NonEmpty.head (parentElements state)---- | Replace the current position by another, modifying the extra state--- in the process-swapStack' :: XMLConverterState nsID extraState- -> NonEmpty XML.Element- -> ( XMLConverterState nsID extraState- , NonEmpty XML.Element )-swapStack' state stack- = ( state { parentElements = stack }- , parentElements state- )-----pushElement :: XML.Element- -> XMLConverterState nsID extraState- -> XMLConverterState nsID extraState-pushElement e state = state { parentElements =- NonEmpty.cons e (parentElements state) }---- | Pop the top element from the call stack, unless it is the last one.-popElement :: XMLConverterState nsID extraState- -> Maybe (XMLConverterState nsID extraState)-popElement state- | _ :| (e:es) <- parentElements state- = Just $ state { parentElements = e :| es }- | otherwise = Nothing- -------------------------------------------------------------------------------- -- Main type -------------------------------------------------------------------------------- --- It might be a good idea to pack the converters in a GADT--- Downside: data instead of type--- Upside: 'Failure' could be made a parameter as well.+-- | A converter that can read from an XML tree, may fail (with+-- 'throwError' \/ 'empty'), and carries some additional state.+-- Note that state modifications survive failure; in particular, the+-- current element must be restored explicitly where necessary+-- (see 'executeIn').+type XMLConverter nsID extraState+ = ExceptT () (State (XMLConverterState nsID extraState)) ----type XMLConverter nsID extraState input output- = ArrowState (XMLConverterState nsID extraState ) input output+-- | Run a converter on an XML element, with a given initial extra state.+runConverter :: (NameSpaceID nsID)+ => XMLConverter nsID extraState a+ -> extraState+ -> XML.Element+ -> Fallible a+runConverter converter extraState element+ = evalState (runExceptT (readNSattributes >> converter))+ (createStartState element extraState) -type FallibleXMLConverter nsID extraState input output- = XMLConverter nsID extraState input (Fallible output)+-- | Lift a 'Maybe' value into the converter, failing on 'Nothing'.+fromMaybeF :: Maybe a -> XMLConverter nsID extraState a+fromMaybeF = maybe (throwError ()) return ----runConverter :: XMLConverter nsID extraState input output- -> XMLConverterState nsID extraState- -> input- -> output-runConverter converter state input = snd $ runArrowState converter (state,input)+-- | Lift a 'Fallible' value into the converter.+fromFallible :: Fallible a -> XMLConverter nsID extraState a+fromFallible = either throwError return -runConverter' :: (NameSpaceID nsID)- => FallibleXMLConverter nsID extraState () success- -> extraState- -> XML.Element- -> Fallible success-runConverter' converter extraState element = runConverter (readNSattributes >>? converter) (createStartState element extraState) ()+-- | Run a converter, catching failure.+tryC :: XMLConverter nsID extraState a+ -> XMLConverter nsID extraState (Fallible a)+tryC converter = catchError (Right <$> converter) (return . Left) ---getCurrentElement :: XMLConverter nsID extraState x XML.Element-getCurrentElement = extractFromState currentElement+getCurrentElement :: XMLConverter nsID extraState XML.Element+getCurrentElement = gets currentElement ---getExtraState :: XMLConverter nsID extraState x extraState-getExtraState = extractFromState moreState+getExtraState :: XMLConverter nsID extraState extraState+getExtraState = gets moreState ---setExtraState :: XMLConverter nsID extraState extraState extraState-setExtraState = withState $ \state extra- -> (replaceExtraState extra state , extra)----- | Lifts a function to the extra state.-modifyExtraState :: (extraState -> extraState)- -> XMLConverter nsID extraState x x-modifyExtraState = modifyState.fmap-+setExtraState :: extraState -> XMLConverter nsID extraState ()+setExtraState x = modify $ \state -> state { moreState = x } --- | First sets the extra state to the new value. Then modifies the original--- extra state with a converter that uses the new state. Finally, the--- intermediate state is dropped and the extra state is lifted into the--- state as it was at the beginning of the function.--- As a result, exactly the extra state and nothing else is changed.--- The resulting converter even behaves like an identity converter on the--- value level. ----- (The -ing form is meant to be mnemonic in a sequence of arrows as in--- convertingExtraState () converter >>> doOtherStuff)----convertingExtraState :: extraState'- -> FallibleXMLConverter nsID extraState' extraState extraState- -> FallibleXMLConverter nsID extraState x x-convertingExtraState v a = withSubStateF setVAsExtraState modifyWithA- where- setVAsExtraState = liftAsSuccess $ extractFromState id >>^ replaceExtraState v- modifyWithA = keepingTheValue (moreState ^>> a)- >>^ spreadChoice >>?% flip replaceExtraState---- | First sets the extra state to the new value. Then produces a new--- extra state with a converter that uses the new state. Finally, the--- intermediate state is dropped and the extra state is lifted into the--- state as it was at the beginning of the function.--- As a result, exactly the extra state and nothing else is changed.--- The resulting converter even behaves like an identity converter on the--- value level.------ Equivalent to------ > \v x a -> convertingExtraState v (returnV x >>> a)------ (The -ing form is meant to be mnemonic in a sequence of arrows as in--- producingExtraState () () producer >>> doOtherStuff)----producingExtraState :: extraState'- -> a- -> FallibleXMLConverter nsID extraState' a extraState- -> FallibleXMLConverter nsID extraState x x-producingExtraState v x a = convertingExtraState v (returnV x >>> a)-+modifyExtraState :: (extraState -> extraState)+ -> XMLConverter nsID extraState ()+modifyExtraState f = modify $ \state -> state { moreState = f (moreState state) } -------------------------------------------------------------------------------- -- Work in namespaces -------------------------------------------------------------------------------- --- | Arrow version of 'getIRI'-lookupNSiri :: (NameSpaceID nsID)- => nsID- -> XMLConverter nsID extraState x (Maybe NameSpaceIRI)-lookupNSiri nsID = extractFromState- $ \state -> getIRI nsID $ namespaceIRIs state+--+lookupNSiri :: (NameSpaceID nsID)+ => nsID+ -> XMLConverter nsID extraState (Maybe NameSpaceIRI)+lookupNSiri nsID = gets $ getIRI nsID . namespaceIRIs ---lookupNSprefix :: (NameSpaceID nsID)- => nsID- -> XMLConverter nsID extraState x (Maybe NameSpacePrefix)-lookupNSprefix nsID = extractFromState- $ \state -> M.lookup nsID $ namespacePrefixes state+lookupNSprefix :: (NameSpaceID nsID)+ => nsID+ -> XMLConverter nsID extraState (Maybe NameSpacePrefix)+lookupNSprefix nsID = gets $ M.lookup nsID . namespacePrefixes -- | Extracts namespace attributes from the current element and tries to -- update the current mapping accordingly-readNSattributes :: (NameSpaceID nsID)- => FallibleXMLConverter nsID extraState x ()-readNSattributes = fromState $ \state -> maybe (state, failEmpty )- ( , succeedWith ())- (extractNSAttrs state )+readNSattributes :: (NameSpaceID nsID) => XMLConverter nsID extraState ()+readNSattributes = do+ state <- get+ maybe (throwError ()) put (extractNSAttrs state) where- extractNSAttrs :: (NameSpaceID nsID)- => XMLConverterState nsID extraState- -> Maybe (XMLConverterState nsID extraState)- extractNSAttrs startState- = L.foldl' (\state d -> state >>= addNS d)- (Just startState)- nsAttribs- where nsAttribs = mapMaybe readNSattr (XML.elAttribs element)- element = currentElement startState+ extractNSAttrs :: (NameSpaceID nsID)+ => XMLConverterState nsID extraState+ -> Maybe (XMLConverterState nsID extraState)+ extractNSAttrs startState = foldM addNS startState nsAttribs+ where nsAttribs = mapMaybe readNSattr+ (XML.elAttribs $ currentElement startState) readNSattr (XML.Attr (XML.QName name _ (Just "xmlns")) iri) = Just (name, iri) readNSattr _ = Nothing- addNS (prefix, iri) state = fmap updateState- $ getNamespaceID iri- $ namespaceIRIs state- where updateState (iris,nsID)- = state { namespaceIRIs = iris- , namespacePrefixes = M.insert nsID prefix- $ namespacePrefixes state- }+ addNS state (prefix, iri) = updateState+ <$> getNamespaceID iri (namespaceIRIs state)+ where updateState (iris, nsID)+ = state { namespaceIRIs = iris+ , namespacePrefixes = M.insert nsID prefix+ $ namespacePrefixes state+ } -------------------------------------------------------------------------------- -- Common namespace accessors@@ -321,29 +202,30 @@ -- | Given a namespace id and an element name, creates a 'XML.QName' for -- internal use-qualifyName :: (NameSpaceID nsID)- => nsID -> ElementName- -> XMLConverter nsID extraState x XML.QName-qualifyName nsID name = lookupNSiri nsID- &&& lookupNSprefix nsID- >>% XML.QName name+qualifyName :: (NameSpaceID nsID)+ => nsID -> ElementName+ -> XMLConverter nsID extraState XML.QName+qualifyName nsID name = XML.QName name <$> lookupNSiri nsID+ <*> lookupNSprefix nsID -- | Checks if a given element matches both a specified namespace id -- and a predicate-elemNameMatches :: (NameSpaceID nsID)- => nsID -> (ElementName -> Bool)- -> XMLConverter nsID extraState XML.Element Bool-elemNameMatches nsID f = keepingTheValue (lookupNSiri nsID) >>% hasMatchingName- where hasMatchingName e iri = let name = XML.elName e- in f (XML.qName name)- && XML.qURI name == iri+elemNameMatches :: (NameSpaceID nsID)+ => nsID -> (ElementName -> Bool)+ -> XML.Element+ -> XMLConverter nsID extraState Bool+elemNameMatches nsID f element = do+ iri <- lookupNSiri nsID+ let name = XML.elName element+ return $ f (XML.qName name) && XML.qURI name == iri -- | Checks if a given element matches both a specified namespace id -- and a specified element name-elemNameIs :: (NameSpaceID nsID)- => nsID -> ElementName- -> XMLConverter nsID extraState XML.Element Bool-elemNameIs nsID name = elemNameMatches nsID (== name)+elemNameIs :: (NameSpaceID nsID)+ => nsID -> ElementName+ -> XML.Element+ -> XMLConverter nsID extraState Bool+elemNameIs nsID name = elemNameMatches nsID (== name) -------------------------------------------------------------------------------- -- General content@@ -352,394 +234,245 @@ elName :: XML.Element -> ElementName elName = XML.qName . XML.elName ----elContent :: XMLConverter nsID extraState x [XML.Content]-elContent = getCurrentElement- >>^ XML.elContent- -------------------------------------------------------------------------------- -- Children -------------------------------------------------------------------------------- ------findChildren :: (NameSpaceID nsID)- => nsID -> ElementName- -> XMLConverter nsID extraState x [XML.Element]-findChildren nsID name = qualifyName nsID name- &&& getCurrentElement- >>% XML.findChildren+findChildren :: (NameSpaceID nsID)+ => nsID -> ElementName+ -> XMLConverter nsID extraState [XML.Element]+findChildren nsID name = XML.findChildren <$> qualifyName nsID name+ <*> getCurrentElement ---findChild' :: (NameSpaceID nsID)- => nsID- -> ElementName- -> XMLConverter nsID extraState x (Maybe XML.Element)-findChild' nsID name = qualifyName nsID name- &&& getCurrentElement- >>% XML.findChild+findChild' :: (NameSpaceID nsID)+ => nsID -> ElementName+ -> XMLConverter nsID extraState (Maybe XML.Element)+findChild' nsID name = XML.findChild <$> qualifyName nsID name+ <*> getCurrentElement ---findChild :: (NameSpaceID nsID)- => nsID -> ElementName- -> FallibleXMLConverter nsID extraState x XML.Element-findChild nsID name = findChild' nsID name- >>> maybeToChoice+findChild :: (NameSpaceID nsID)+ => nsID -> ElementName+ -> XMLConverter nsID extraState XML.Element+findChild nsID name = findChild' nsID name >>= fromMaybeF -filterChildrenName' :: (NameSpaceID nsID)- => nsID- -> (ElementName -> Bool)- -> XMLConverter nsID extraState x [XML.Element]-filterChildrenName' nsID f = getCurrentElement- >>> arr XML.elChildren- >>> iterateS (keepingTheValue (elemNameMatches nsID f))- >>> arr (map fst . filter snd)+--+filterChildrenName' :: (NameSpaceID nsID)+ => nsID+ -> (ElementName -> Bool)+ -> XMLConverter nsID extraState [XML.Element]+filterChildrenName' nsID f = getCurrentElement+ >>= filterM (elemNameMatches nsID f) . XML.elChildren -------------------------------------------------------------------------------- -- Attributes -------------------------------------------------------------------------------- ---isSet' :: (NameSpaceID nsID)- => nsID -> AttributeName- -> XMLConverter nsID extraState x (Maybe Bool)-isSet' nsID attrName = findAttr' nsID attrName- >>^ (>>= stringToBool')+isSet' :: (NameSpaceID nsID)+ => nsID -> AttributeName+ -> XMLConverter nsID extraState (Maybe Bool)+isSet' nsID attrName = (>>= stringToBool') <$> findAttr' nsID attrName -isSetWithDefault :: (NameSpaceID nsID)- => nsID -> AttributeName- -> Bool- -> XMLConverter nsID extraState x Bool-isSetWithDefault nsID attrName def'- = isSet' nsID attrName- >>^ fromMaybe def'+isSetWithDefault :: (NameSpaceID nsID)+ => nsID -> AttributeName+ -> Bool+ -> XMLConverter nsID extraState Bool+isSetWithDefault nsID attrName def' =+ fromMaybe def' <$> isSet' nsID attrName -- | Lookup value in a dictionary, fail if no attribute found or value -- not in dictionary-searchAttrIn :: (NameSpaceID nsID)- => nsID -> AttributeName- -> [(AttributeValue,a)]- -> FallibleXMLConverter nsID extraState x a-searchAttrIn nsID attrName dict- = findAttr nsID attrName- >>?^? maybeToChoice.(`lookup` dict )+searchAttrIn :: (NameSpaceID nsID)+ => nsID -> AttributeName+ -> [(AttributeValue,a)]+ -> XMLConverter nsID extraState a+searchAttrIn nsID attrName dict = do+ value <- findAttr nsID attrName+ fromMaybeF $ lookup value dict -- | Lookup value in a dictionary. If attribute or value not found, -- return default value-searchAttr :: (NameSpaceID nsID)- => nsID -> AttributeName- -> a- -> [(AttributeValue,a)]- -> XMLConverter nsID extraState x a-searchAttr nsID attrName defV dict- = searchAttrIn nsID attrName dict- >>> const defV ^|||^ id+searchAttr :: (NameSpaceID nsID)+ => nsID -> AttributeName+ -> a+ -> [(AttributeValue,a)]+ -> XMLConverter nsID extraState a+searchAttr nsID attrName defV dict =+ searchAttrIn nsID attrName dict <|> return defV -- | Read a 'Lookupable' attribute. Fail if no match.-lookupAttr :: (NameSpaceID nsID, Lookupable a)- => nsID -> AttributeName- -> FallibleXMLConverter nsID extraState x a-lookupAttr nsID attrName = lookupAttr' nsID attrName- >>^ maybeToChoice-+lookupAttr :: (NameSpaceID nsID, Lookupable a)+ => nsID -> AttributeName+ -> XMLConverter nsID extraState a+lookupAttr nsID attrName = lookupAttr' nsID attrName >>= fromMaybeF -- | Read a 'Lookupable' attribute. Return the result as a 'Maybe'.-lookupAttr' :: (NameSpaceID nsID, Lookupable a)- => nsID -> AttributeName- -> XMLConverter nsID extraState x (Maybe a)-lookupAttr' nsID attrName- = findAttr' nsID attrName- >>^ (>>= readLookupable)+lookupAttr' :: (NameSpaceID nsID, Lookupable a)+ => nsID -> AttributeName+ -> XMLConverter nsID extraState (Maybe a)+lookupAttr' nsID attrName =+ (>>= readLookupable) <$> findAttr' nsID attrName -- | Read a 'Lookupable' attribute with explicit default-lookupAttrWithDefault :: (NameSpaceID nsID, Lookupable a)- => nsID -> AttributeName- -> a- -> XMLConverter nsID extraState x a-lookupAttrWithDefault nsID attrName deflt- = lookupAttr' nsID attrName- >>^ fromMaybe deflt+lookupAttrWithDefault :: (NameSpaceID nsID, Lookupable a)+ => nsID -> AttributeName+ -> a+ -> XMLConverter nsID extraState a+lookupAttrWithDefault nsID attrName deflt =+ fromMaybe deflt <$> lookupAttr' nsID attrName -- | Read a 'Lookupable' attribute with implicit default-lookupDefaultingAttr :: (NameSpaceID nsID, Lookupable a, Default a)- => nsID -> AttributeName- -> XMLConverter nsID extraState x a-lookupDefaultingAttr nsID attrName- = lookupAttrWithDefault nsID attrName def---- | Return value as a (Maybe Text)-findAttr' :: (NameSpaceID nsID)- => nsID -> AttributeName- -> XMLConverter nsID extraState x (Maybe AttributeValue)-findAttr' nsID attrName = qualifyName nsID attrName- &&& getCurrentElement- >>% XML.findAttr+lookupDefaultingAttr :: (NameSpaceID nsID, Lookupable a, Default a)+ => nsID -> AttributeName+ -> XMLConverter nsID extraState a+lookupDefaultingAttr nsID attrName =+ lookupAttrWithDefault nsID attrName def -- | Return value as a (Maybe Text)-findAttrText' :: (NameSpaceID nsID)- => nsID -> AttributeName- -> XMLConverter nsID extraState x (Maybe TextAttributeValue)-findAttrText' nsID attrName- = qualifyName nsID attrName- &&& getCurrentElement- >>% XML.findAttr---- | Return value as string or fail-findAttr :: (NameSpaceID nsID)- => nsID -> AttributeName- -> FallibleXMLConverter nsID extraState x AttributeValue-findAttr nsID attrName = findAttr' nsID attrName- >>> maybeToChoice+findAttr' :: (NameSpaceID nsID)+ => nsID -> AttributeName+ -> XMLConverter nsID extraState (Maybe AttributeValue)+findAttr' nsID attrName = XML.findAttr <$> qualifyName nsID attrName+ <*> getCurrentElement --- | Return value as text or fail-findAttrText :: (NameSpaceID nsID)- => nsID -> AttributeName- -> FallibleXMLConverter nsID extraState x TextAttributeValue-findAttrText nsID attrName- = findAttr' nsID attrName- >>> maybeToChoice+-- | Return value or fail+findAttr :: (NameSpaceID nsID)+ => nsID -> AttributeName+ -> XMLConverter nsID extraState AttributeValue+findAttr nsID attrName = findAttr' nsID attrName >>= fromMaybeF --- | Return value as string or return provided default value-findAttrTextWithDefault :: (NameSpaceID nsID)- => nsID -> AttributeName- -> TextAttributeValue- -> XMLConverter nsID extraState x TextAttributeValue-findAttrTextWithDefault nsID attrName deflt- = findAttr' nsID attrName- >>^ fromMaybe deflt+-- | Return value or return provided default value+findAttrWithDefault :: (NameSpaceID nsID)+ => nsID -> AttributeName+ -> AttributeValue+ -> XMLConverter nsID extraState AttributeValue+findAttrWithDefault nsID attrName deflt =+ fromMaybe deflt <$> findAttr' nsID attrName -- | Read and return value or fail-readAttr :: (NameSpaceID nsID, Read attrValue)- => nsID -> AttributeName- -> FallibleXMLConverter nsID extraState x attrValue-readAttr nsID attrName = readAttr' nsID attrName- >>> maybeToChoice+readAttr :: (NameSpaceID nsID, Read attrValue)+ => nsID -> AttributeName+ -> XMLConverter nsID extraState attrValue+readAttr nsID attrName = readAttr' nsID attrName >>= fromMaybeF -- | Read and return value or return Nothing-readAttr' :: (NameSpaceID nsID, Read attrValue)- => nsID -> AttributeName- -> XMLConverter nsID extraState x (Maybe attrValue)-readAttr' nsID attrName = findAttr' nsID attrName- >>^ (>>= tryToRead)+readAttr' :: (NameSpaceID nsID, Read attrValue)+ => nsID -> AttributeName+ -> XMLConverter nsID extraState (Maybe attrValue)+readAttr' nsID attrName = (>>= tryToRead) <$> findAttr' nsID attrName -- | Read and return value or return provided default value-readAttrWithDefault :: (NameSpaceID nsID, Read attrValue)- => nsID -> AttributeName- -> attrValue- -> XMLConverter nsID extraState x attrValue-readAttrWithDefault nsID attrName deflt- = findAttr' nsID attrName- >>^ (>>= tryToRead)- >>^ fromMaybe deflt+readAttrWithDefault :: (NameSpaceID nsID, Read attrValue)+ => nsID -> AttributeName+ -> attrValue+ -> XMLConverter nsID extraState attrValue+readAttrWithDefault nsID attrName deflt =+ fromMaybe deflt <$> readAttr' nsID attrName -- | Read and return value or return default value from 'Default' instance-getAttr :: (NameSpaceID nsID, Read attrValue, Default attrValue)- => nsID -> AttributeName- -> XMLConverter nsID extraState x attrValue-getAttr nsID attrName = readAttrWithDefault nsID attrName def+getAttr :: (NameSpaceID nsID, Read attrValue, Default attrValue)+ => nsID -> AttributeName+ -> XMLConverter nsID extraState attrValue+getAttr nsID attrName = readAttrWithDefault nsID attrName def -------------------------------------------------------------------------------- -- Movements -------------------------------------------------------------------------------- ----jumpThere :: XMLConverter nsID extraState XML.Element XML.Element-jumpThere = withState (\state element- -> ( pushElement element state , element )- )-----swapStack :: XMLConverter nsID extraState (NonEmpty XML.Element)- (NonEmpty XML.Element)-swapStack = withState swapStack'-----jumpBack :: FallibleXMLConverter nsID extraState _x _x-jumpBack = tryModifyState (popElement >>> maybeToChoice)---- | Support function for "procedural" converters: jump to an element, execute--- a converter, jump back.--- This version is safer than 'executeThere', because it does not rely on the--- internal stack. As a result, the converter can not move around in arbitrary--- ways. The downside is of course that some of the environment is not--- accessible to the converter.-switchingTheStack :: XMLConverter nsID moreState a b- -> XMLConverter nsID moreState (a, XML.Element) b-switchingTheStack a = second ( (:| []) ^>> swapStack )- >>> first a- >>> second swapStack- >>^ fst---- | Support function for "procedural" converters: jumps to an element, executes--- a converter, jumps back.--- Make sure that the converter is well-behaved; that is it should--- return to the exact position it started from in /every possible path/ of--- execution, even if it "fails". If it does not, you may encounter--- strange bugs. If you are not sure about the behaviour or want to use--- shortcuts, you can often use 'switchingTheStack' instead.-executeThere :: FallibleXMLConverter nsID moreState a b- -> FallibleXMLConverter nsID moreState (a, XML.Element) b-executeThere a = second jumpThere- >>> fst- ^>> a- >>> jumpBack -- >>? jumpBack would not ensure the jump.- >>^ collapseEither----- | Do something in a specific element, then come back-executeIn :: XMLConverter nsID extraState XML.Element s- -> XMLConverter nsID extraState XML.Element s-executeIn a = duplicate >>> switchingTheStack a+-- | Execute a converter in a specific element, then come back.+-- The current element is restored even if the converter fails.+executeIn :: XML.Element+ -> XMLConverter nsID extraState a+ -> XMLConverter nsID extraState a+executeIn element converter = do+ oldElement <- getCurrentElement+ modify $ \state -> state { currentElement = element }+ result <- tryC converter+ modify $ \state -> state { currentElement = oldElement }+ fromFallible result --- | Do something in a sub-element, then come back-executeInSub :: (NameSpaceID nsID)- => nsID -> ElementName- -> FallibleXMLConverter nsID extraState f s- -> FallibleXMLConverter nsID extraState f s-executeInSub nsID name a = keepingTheValue- (findChild nsID name)- >>> ignoringState liftFailure- >>? switchingTheStack a- where liftFailure (_, Left f) = Left f- liftFailure (x, Right e) = Right (x, e)+-- | Execute a converter in a sub-element of the current element,+-- then come back. Fails if there is no such sub-element.+executeInSub :: (NameSpaceID nsID)+ => nsID -> ElementName+ -> XMLConverter nsID extraState a+ -> XMLConverter nsID extraState a+executeInSub nsID name converter = do+ child <- findChild nsID name+ executeIn child converter -------------------------------------------------------------------------------- -- Iterating over children -------------------------------------------------------------------------------- --- Helper converter to prepare different types of iterations.--- It lifts the children (of a certain type) of the current element--- into the value level and pairs each one with the current input value.-prepareIteration :: (NameSpaceID nsID)- => nsID -> ElementName- -> XMLConverter nsID extraState b [(b, XML.Element)]-prepareIteration nsID name = keepingTheValue- (findChildren nsID name)- >>% distributeValue-----withEveryL :: (NameSpaceID nsID)- => nsID -> ElementName- -> FallibleXMLConverter nsID extraState a b- -> FallibleXMLConverter nsID extraState a [b]-withEveryL = withEvery- -- | Applies a converter to every child element of a specific type.--- Collects results in a 'MonadPlus'. -- Fails completely if any conversion fails.-withEvery :: (NameSpaceID nsID, MonadPlus m)- => nsID -> ElementName- -> FallibleXMLConverter nsID extraState a b- -> FallibleXMLConverter nsID extraState a (m b)-withEvery nsID name a = prepareIteration nsID name- >>> iterateS' (switchingTheStack a)+withEveryL :: (NameSpaceID nsID)+ => nsID -> ElementName+ -> XMLConverter nsID extraState a+ -> XMLConverter nsID extraState [a]+withEveryL nsID name converter = do+ children <- findChildren nsID name+ mapM (`executeIn` converter) children -- | Applies a converter to every child element of a specific type. -- Collects all successful results in a list.-tryAll :: (NameSpaceID nsID)- => nsID -> ElementName- -> FallibleXMLConverter nsID extraState b a- -> XMLConverter nsID extraState b [a]-tryAll nsID name a = prepareIteration nsID name- >>> iterateS (switchingTheStack a)- >>^ rights+tryAll :: (NameSpaceID nsID)+ => nsID -> ElementName+ -> XMLConverter nsID extraState a+ -> XMLConverter nsID extraState [a]+tryAll nsID name converter = do+ children <- findChildren nsID name+ catMaybes <$> mapM (\child -> optional (executeIn child converter)) children -------------------------------------------------------------------------------- -- Matching children -------------------------------------------------------------------------------- -type IdXMLConverter nsID moreState x- = XMLConverter nsID moreState x x--type MaybeCConverter nsID moreState x- = Maybe (IdXMLConverter nsID moreState (x, XML.Content))---- Chainable converter that helps deciding which converter to actually use.-type ContentMatchConverter nsID extraState x- = IdXMLConverter nsID- extraState- (MaybeCConverter nsID extraState x, XML.Content)---- Helper function: The @c@ is actually a converter that is to be selected by--- matching XML content to the first two parameters.--- The fold used to match elements however is very simple, so to use it,--- this function wraps the converter in another converter that unifies--- the accumulator. Think of a lot of converters with the resulting type--- chained together. The accumulator not only transports the element--- unchanged to the next matcher, it also does the actual selecting by--- combining the intermediate results with '(<|>)'.-makeMatcherC :: (NameSpaceID nsID)- => nsID -> ElementName- -> FallibleXMLConverter nsID extraState a a- -> ContentMatchConverter nsID extraState a-makeMatcherC nsID name c = ( second ( contentToElem- >>> returnV Nothing- ||| ( elemNameIs nsID name- >>^ bool Nothing (Just cWithJump)- )- )- >>% (<|>)- ) &&&^ snd- where cWithJump = ( fst- ^&&& ( second contentToElem- >>> spreadChoice- ^>>? executeThere c- )- >>% recover)- &&&^ snd- contentToElem :: FallibleXMLConverter nsID extraState XML.Content XML.Element- contentToElem = arr $ \case- XML.Elem e' -> succeedWith e'- _ -> failEmpty---- Creates and chains a bunch of matchers-prepareMatchersC :: (NameSpaceID nsID)- => [(nsID, ElementName, FallibleXMLConverter nsID extraState x x)]- -> ContentMatchConverter nsID extraState x---prepareMatchersC = foldSs . (map $ uncurry3 makeMatcherC)-prepareMatchersC = reverseComposition . map (uncurry3 makeMatcherC)+-- | A converter for a child element with a specific name in a specific+-- namespace. The converter produces that element's contribution to the+-- overall result.+type ElementMatcher nsID extraState a+ = (nsID, ElementName, XMLConverter nsID extraState a) --- | Takes a list of element-data - converter groups and--- * Finds all content of the current element--- * Matches each group to each piece of content in order--- (at most one group per piece of content)--- * Filters non-matched content--- * Chains all found converters in content-order--- * Applies the chain to the input element-matchContent' :: (NameSpaceID nsID)- => [(nsID, ElementName, FallibleXMLConverter nsID extraState a a)]- -> XMLConverter nsID extraState a a-matchContent' lookups = matchContent lookups (arr fst)+-- | Like 'matchContent', but ignores non-matching content.+matchContent' :: (NameSpaceID nsID, Monoid a)+ => [ElementMatcher nsID extraState a]+ -> XMLConverter nsID extraState a+matchContent' lookups = matchContent lookups (\_ -> return mempty) --- | Takes a list of element-data - converter groups and--- * Finds all content of the current element--- * Matches each group to each piece of content in order--- (at most one group per piece of content)--- * Adds a default converter for all non-matched content--- * Chains all found converters in content-order--- * Applies the chain to the input element-matchContent :: (NameSpaceID nsID)- => [(nsID, ElementName, FallibleXMLConverter nsID extraState a a)]- -> XMLConverter nsID extraState (a,XML.Content) a- -> XMLConverter nsID extraState a a-matchContent lookups fallback- = let matcher = prepareMatchersC lookups- in keepingTheValue (- elContent- >>> map (Nothing,)- ^>> iterateSL matcher- >>^ map swallowOrFallback- -- >>> foldSs- >>> reverseComposition- )- >>> swap- ^>> app+-- | Takes a list of element matchers and a fallback converter, and+-- converts the content of the current element in order: for each child+-- element, the first matcher with a matching name (if any) is applied+-- in that element; all other content is passed to the fallback+-- converter. The results are combined with 'mappend'.+-- If a matched converter fails, the corresponding element contributes+-- nothing to the result.+matchContent :: (NameSpaceID nsID, Monoid a)+ => [ElementMatcher nsID extraState a]+ -> (XML.Content -> XMLConverter nsID extraState a)+ -> XMLConverter nsID extraState a+matchContent lookups fallback = do+ contents <- XML.elContent <$> getCurrentElement+ mconcat <$> mapM matchOne contents where- -- let the converter swallow the content and drop the content- -- in the return value- swallowOrFallback (Just converter,content) = (,content) ^>> converter >>^ fst- swallowOrFallback (Nothing ,content) = (,content) ^>> fallback+ matchOne content@(XML.Elem element) = do+ mConverter <- findConverter element lookups+ case mConverter of+ Just converter -> executeIn element converter <|> return mempty+ Nothing -> fallback content+ matchOne content = fallback content + findConverter _ [] = return Nothing+ findConverter element ((nsID, name, converter):rest) = do+ matches <- elemNameIs nsID name element+ if matches+ then return $ Just converter+ else findConverter element rest+ -------------------------------------------------------------------------------- -- Internals --------------------------------------------------------------------------------@@ -750,32 +483,3 @@ | otherwise = Nothing where trueValues = ["true" ,"on" ,"1"] falseValues = ["false","off","0"]---distributeValue :: a -> [b] -> [(a,b)]-distributeValue = map.(,)------------------------------------------------------------------------------------{--NOTES-It might be a good idea to refactor the namespace stuff.-E.g.: if a namespace constructor took a string as a parameter, things like-> a ?>/< (NsText,"body")-would be nicer.-Together with a rename and some trickery, something like-> |< NsText "body" >< NsText "p" ?> a </> </>|-might even be possible.--Some day, XML.Light should be replaced by something better.-While doing that, it might be useful to replace String as the type of element-names with something else, too. (Of course with OverloadedStrings).-While doing that, maybe the types can be created in a way that something like-> NsText:"body"-could be used. Overloading (:) does not sounds like the best idea, but if the-element name type was a list, this might be possible.-Of course that would be a bit hackish, so the "right" way would probably be-something like-> InNS NsText "body"-but isn't that a bit boring? ;)--}
@@ -1,5 +1,3 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE Arrows #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE OverloadedStrings #-}@@ -31,6 +29,8 @@ , ListLevelType (..) , LengthOrPercent (..) , lookupStyle+, lookupNamedStyle+, styleUIName , getListLevelStyle , getStyleFamily , lookupDefaultStyle'@@ -39,35 +39,31 @@ , readStylesAt ) where -import Prelude hiding (Applicative(..))-import Control.Applicative hiding (liftA, liftA2, liftA3)-import Control.Arrow+import Control.Applicative ((<|>), optional) +import Data.Char (chr, isHexDigit) import Data.Default import qualified Data.Foldable as F-import qualified Data.List as L import qualified Data.Map as M import Data.Maybe import Data.Text (Text) import qualified Data.Text as T import qualified Data.Set as S+import Numeric (readHex) import qualified Text.Pandoc.XML.Light as XML import Text.Pandoc.Shared (safeRead, tshow) -import Text.Pandoc.Readers.ODT.Arrows.Utils- import Text.Pandoc.Readers.ODT.Generic.Fallible import qualified Text.Pandoc.Readers.ODT.Generic.SetMap as SM import Text.Pandoc.Readers.ODT.Generic.Utils import Text.Pandoc.Readers.ODT.Generic.XMLConverter -import Text.Pandoc.Readers.ODT.Base import Text.Pandoc.Readers.ODT.Namespaces readStylesAt :: XML.Element -> Fallible Styles-readStylesAt e = runConverter' readAllStyles mempty e+readStylesAt e = runConverter readAllStyles mempty e -------------------------------------------------------------------------------- -- Reader for font declarations and font pitches@@ -99,41 +95,29 @@ type FontPitches = M.Map FontFaceName FontPitch -- To get there, the fonts have to be read and the pitches extracted.--- But the resulting map are only needed at one later place, so it should not be--- transported on the value level, especially as we already use a state arrow.--- So instead, the resulting map is lifted into the state of the reader.--- (An alternative might be ImplicitParams, but again, we already have a state.)+-- But the resulting map is only needed at one later place, so it should not be+-- transported on the value level. Instead, it is lifted into the state of the+-- reader. ----- So the main style readers will have the types-type StyleReader a b = XMLReader FontPitches a b--- and-type StyleReaderSafe a b = XMLReaderSafe FontPitches a b--- respectively.+-- So the main style readers will have the type+type StyleReader a = XMLConverter Namespace FontPitches a ----- But before we can work with these, we need to define the reader that reads--- the fonts:+-- But before we can work with this, we need to define the reader that reads+-- the fonts. It is polymorphic in the extra state, since the pitches are not+-- yet available while it runs. -- | A reader for font pitches-fontPitchReader :: XMLReader s a FontPitches-fontPitchReader = executeInSub NsOffice "font-face-decls" (- withEveryL NsStyle "font-face" (liftAsSuccess (- findAttr' NsStyle "name"- &&&- lookupDefaultingAttr NsStyle "font-pitch"- ))- >>?^ ( M.fromList . L.foldl' accumLegalPitches [] )- ) `ifFailedDo` returnV (Right M.empty)- where accumLegalPitches ls (Nothing,_) = ls- accumLegalPitches ls (Just n,p) = (n,p):ls----- | A wrapper around the font pitch reader that lifts the result into the--- state.-readFontPitches :: StyleReader a a-readFontPitches = producingExtraState () () fontPitchReader-+fontPitchReader :: XMLConverter Namespace s FontPitches+fontPitchReader =+ executeInSub NsOffice "font-face-decls" (do+ fonts <- withEveryL NsStyle "font-face" $ do+ name <- findAttr' NsStyle "name"+ pitch <- lookupDefaultingAttr NsStyle "font-pitch"+ return (name, pitch)+ return $ M.fromList [ (n, p) | (Just n, p) <- fonts ])+ <|> return M.empty --- | Looking up a pitch in the state of the arrow.+-- | Looking up a pitch in the extra state. -- -- The function does the following: -- * Look for the font pitch in an attribute.@@ -141,15 +125,12 @@ -- and use the pitch from there. -- * Return the result in a Maybe ---findPitch :: XMLReaderSafe FontPitches a (Maybe FontPitch)-findPitch = ( lookupAttr NsStyle "font-pitch"- `ifFailedDo` findAttr NsStyle "font-name"- >>? ( keepingTheValue getExtraState- >>% M.lookup- >>^ maybeToChoice- )- )- >>> choiceToMaybe+findPitch :: StyleReader (Maybe FontPitch)+findPitch = optional+ $ lookupAttr NsStyle "font-pitch"+ <|> (do fontName <- findAttr NsStyle "font-name"+ pitches <- getExtraState+ fromMaybeF $ M.lookup fontName pitches) -------------------------------------------------------------------------------- -- Definitions of main data@@ -201,10 +182,17 @@ ] -- | A named style-data Style = Style { styleFamily :: Maybe StyleFamily- , styleParentName :: Maybe StyleName- , listStyle :: Maybe StyleName- , styleProperties :: StyleProperties+data Style = Style { styleFamily :: Maybe StyleFamily+ , styleParentName :: Maybe StyleName+ , listStyle :: Maybe StyleName+ , styleProperties :: StyleProperties+ -- | The name shown to the user+ -- (@style:display-name@), if given.+ , styleDisplayName :: Maybe Text+ -- | Whether this is an automatic style,+ -- i.e. one that only records direct+ -- formatting and is not visible to the user.+ , styleAutomatic :: Bool } deriving ( Eq, Show ) @@ -416,84 +404,80 @@ -------------------------------------------------------------------------------- ---readAllStyles :: StyleReader a Styles-readAllStyles = ( readFontPitches- >>?! ( readAutomaticStyles- &&& readStyles ))- >>?%? chooseMax+readAllStyles :: StyleReader Styles+readAllStyles = do+ fontPitchReader >>= setExtraState+ autoStyles <- tryC readAutomaticStyles+ styles <- tryC readStyles+ fromFallible $ chooseMax autoStyles styles -- all top elements are always on the same hierarchy level ---readStyles :: StyleReader a Styles-readStyles = executeInSub NsOffice "styles" $ liftAsSuccess- $ liftA3 Styles- ( tryAll NsStyle "style" readStyle >>^ M.fromList )- ( tryAll NsText "list-style" readListStyle >>^ M.fromList )- ( tryAll NsStyle "default-style" readDefaultStyle >>^ M.fromList )+readStyles :: StyleReader Styles+readStyles = executeInSub NsOffice "styles" $+ Styles <$> (M.fromList <$> tryAll NsStyle "style" (readStyle False) )+ <*> (M.fromList <$> tryAll NsText "list-style" readListStyle )+ <*> (M.fromList <$> tryAll NsStyle "default-style" readDefaultStyle) ---readAutomaticStyles :: StyleReader a Styles-readAutomaticStyles = executeInSub NsOffice "automatic-styles" $ liftAsSuccess- $ liftA3 Styles- ( tryAll NsStyle "style" readStyle >>^ M.fromList )- ( tryAll NsText "list-style" readListStyle >>^ M.fromList )- ( returnV M.empty )+readAutomaticStyles :: StyleReader Styles+readAutomaticStyles = executeInSub NsOffice "automatic-styles" $+ Styles <$> (M.fromList <$> tryAll NsStyle "style" (readStyle True) )+ <*> (M.fromList <$> tryAll NsText "list-style" readListStyle )+ <*> pure M.empty ---readDefaultStyle :: StyleReader a (StyleFamily, StyleProperties)-readDefaultStyle = lookupAttr NsStyle "family"- >>?! keepingTheValue readStyleProperties+readDefaultStyle :: StyleReader (StyleFamily, StyleProperties)+readDefaultStyle = (,) <$> lookupAttr NsStyle "family"+ <*> readStyleProperties ----readStyle :: StyleReader a (StyleName,Style)-readStyle = findAttr NsStyle "name"- >>?! keepingTheValue- ( liftA4 Style- ( lookupAttr' NsStyle "family" )- ( findAttr' NsStyle "parent-style-name" )- ( findAttr' NsStyle "list-style-name" )- readStyleProperties- )+-- | Read a style; the argument says whether it is an automatic style.+readStyle :: Bool -> StyleReader (StyleName, Style)+readStyle automatic = do+ name <- findAttr NsStyle "name"+ style <- Style <$> lookupAttr' NsStyle "family"+ <*> findAttr' NsStyle "parent-style-name"+ <*> findAttr' NsStyle "list-style-name"+ <*> readStyleProperties+ <*> findAttr' NsStyle "display-name"+ <*> pure automatic+ return (name, style) ---readStyleProperties :: StyleReaderSafe a StyleProperties-readStyleProperties = liftA2 SProps- ( readTextProperties >>> choiceToMaybe )- ( readParaProperties >>> choiceToMaybe )+readStyleProperties :: StyleReader StyleProperties+readStyleProperties = SProps <$> optional readTextProperties+ <*> optional readParaProperties ---readTextProperties :: StyleReader a TextProperties+readTextProperties :: StyleReader TextProperties readTextProperties =- executeInSub NsStyle "text-properties" $ liftAsSuccess- ( liftA6 PropT- ( searchAttr NsXSL_FO "font-style" False isFontEmphasised )- ( searchAttr NsXSL_FO "font-weight" False isFontBold )- findPitch- ( getAttr NsStyle "text-position" )- readUnderlineMode- readStrikeThroughMode- )+ executeInSub NsStyle "text-properties" $+ PropT <$> searchAttr NsXSL_FO "font-style" False isFontEmphasised+ <*> searchAttr NsXSL_FO "font-weight" False isFontBold+ <*> findPitch+ <*> getAttr NsStyle "text-position"+ <*> readUnderlineMode+ <*> readStrikeThroughMode where isFontEmphasised = [("normal",False),("italic",True),("oblique",True)]+ -- As in CSS, `bold` is weight 700; lighter weights are not bold. isFontBold = ("normal",False):("bold",True)- :map ((,True) . tshow) ([100,200..900]::[Int])+ :map (\w -> (tshow w, w >= 700)) ([100,200..900]::[Int]) -readUnderlineMode :: StyleReaderSafe a (Maybe UnderlineMode)+readUnderlineMode :: StyleReader (Maybe UnderlineMode) readUnderlineMode = readLineMode "text-underline-mode" "text-underline-style" -readStrikeThroughMode :: StyleReaderSafe a (Maybe UnderlineMode)+readStrikeThroughMode :: StyleReader (Maybe UnderlineMode) readStrikeThroughMode = readLineMode "text-line-through-mode" "text-line-through-style" -readLineMode :: Text -> Text -> StyleReaderSafe a (Maybe UnderlineMode)-readLineMode modeAttr styleAttr = proc x -> do- isUL <- searchAttr NsStyle styleAttr False isLinePresent -< x- mode <- lookupAttr' NsStyle modeAttr -< x- if isUL- then case mode of- Just m -> returnA -< Just m- Nothing -> returnA -< Just UnderlineModeNormal- else returnA -< Nothing+readLineMode :: Text -> Text -> StyleReader (Maybe UnderlineMode)+readLineMode modeAttr styleAttr = do+ isUL <- searchAttr NsStyle styleAttr False isLinePresent+ mode <- lookupAttr' NsStyle modeAttr+ return $ if isUL+ then mode <|> Just UnderlineModeNormal+ else Nothing where isLinePresent = ("none",False) : map (,True) [ "dash" , "dot-dash" , "dot-dot-dash" , "dotted"@@ -501,20 +485,16 @@ ] ---readParaProperties :: StyleReader a ParaProperties+readParaProperties :: StyleReader ParaProperties readParaProperties =- executeInSub NsStyle "paragraph-properties" $ liftAsSuccess- ( liftA3 PropP- ( liftA2 readNumbering- ( isSet' NsText "number-lines" )- ( readAttr' NsText "line-number" )- )- ( liftA2 readIndentation- ( isSetWithDefault NsStyle "auto-text-indent" False )- ( getAttr NsXSL_FO "text-indent" )- )- ( getAttr NsXSL_FO "margin-left" )- )+ executeInSub NsStyle "paragraph-properties" $+ PropP <$> ( readNumbering+ <$> isSet' NsText "number-lines"+ <*> readAttr' NsText "line-number" )+ <*> ( readIndentation+ <$> isSetWithDefault NsStyle "auto-text-indent" False+ <*> getAttr NsXSL_FO "text-indent" )+ <*> getAttr NsXSL_FO "margin-left" where readNumbering (Just True) (Just n) = NumberingRestart n readNumbering (Just True) _ = NumberingKeep readNumbering _ _ = NumberingNone@@ -527,39 +507,37 @@ ---- ---readListStyle :: StyleReader a (StyleName, ListStyle)-readListStyle =- findAttr NsStyle "name"- >>?! keepingTheValue- ( liftA ListStyle- $ liftA3 SM.union3- ( readListLevelStyles NsText "list-level-style-number" LltNumbered )- ( readListLevelStyles NsText "list-level-style-bullet" LltBullet )- ( readListLevelStyles NsText "list-level-style-image" LltImage ) >>^ M.mapMaybe chooseMostSpecificListLevelStyle- )+readListStyle :: StyleReader (StyleName, ListStyle)+readListStyle = do+ name <- findAttr NsStyle "name"+ styles <- SM.union3+ <$> readListLevelStyles NsText "list-level-style-number" LltNumbered+ <*> readListLevelStyles NsText "list-level-style-bullet" LltBullet+ <*> readListLevelStyles NsText "list-level-style-image" LltImage+ return ( name+ , ListStyle $ M.mapMaybe chooseMostSpecificListLevelStyle styles )+ -- readListLevelStyles :: Namespace -> ElementName -> ListLevelType- -> StyleReaderSafe a (SM.SetMap Int ListLevelStyle)+ -> StyleReader (SM.SetMap Int ListLevelStyle) readListLevelStyles namespace elementName levelType =- tryAll namespace elementName (readListLevelStyle levelType)- >>^ SM.fromList+ SM.fromList <$> tryAll namespace elementName (readListLevelStyle levelType) ---readListLevelStyle :: ListLevelType -> StyleReader a (Int, ListLevelStyle)-readListLevelStyle levelType = readAttr NsText "level"- >>?! keepingTheValue- ( liftA5 toListLevelStyle- ( returnV levelType )- ( findAttr' NsStyle "num-prefix" )- ( findAttr' NsStyle "num-suffix" )- ( getAttr NsStyle "num-format" )- ( findAttrText' NsText "start-value" )- )+readListLevelStyle :: ListLevelType -> StyleReader (Int, ListLevelStyle)+readListLevelStyle levelType = do+ level <- readAttr NsText "level"+ style <- toListLevelStyle+ <$> findAttr' NsStyle "num-prefix"+ <*> findAttr' NsStyle "num-suffix"+ <*> getAttr NsStyle "num-format"+ <*> findAttr' NsText "start-value"+ return (level, style) where- toListLevelStyle _ p s LinfNone b = ListLevelStyle LltBullet p s LinfNone (startValue b)- toListLevelStyle _ p s f@(LinfString _) b = ListLevelStyle LltBullet p s f (startValue b)- toListLevelStyle t p s f b = ListLevelStyle t p s f (startValue b)+ toListLevelStyle p s LinfNone b = ListLevelStyle LltBullet p s LinfNone (startValue b)+ toListLevelStyle p s f@(LinfString _) b = ListLevelStyle LltBullet p s f (startValue b)+ toListLevelStyle p s f b = ListLevelStyle levelType p s f (startValue b) startValue mbx = fromMaybe 1 (mbx >>= safeRead) --@@ -589,6 +567,48 @@ lookupStyle :: StyleName -> Styles -> Maybe Style lookupStyle name Styles{..} = M.lookup name stylesByName +-- | Find the named (i.e., non-automatic) style that an element with the+-- given style name is an instance of. An automatic style (@P1@, @T3@, ...)+-- only records direct formatting, so it is followed up its chain of parents.+-- Returns the internal name of the style together with the style, which+-- may not be defined: automatic styles always are, so an undefined style+-- is a named one.+lookupNamedStyle :: StyleName -> Styles+ -> Maybe (StyleName, Maybe Style)+lookupNamedStyle name0 styles = go S.empty name0+ where+ go seen name+ | name `S.member` seen = Nothing -- cyclic parent references+ | otherwise =+ case lookupStyle name styles of+ Nothing -> Just (name, Nothing)+ Just style+ | styleAutomatic style+ -> styleParentName style >>= go (S.insert name seen)+ | otherwise -> Just (name, Just style)++-- | The name of a style as shown to the user: its @style:display-name@,+-- if any, or else its internal name with ODF's escapes (@_20_@ for a+-- space, etc.) decoded.+styleUIName :: StyleName -> Maybe Style -> Text+styleUIName name mbStyle =+ fromMaybe (decodeStyleName name) (mbStyle >>= styleDisplayName)++-- | Decode the hex escapes (e.g. @_20_@) that ODF uses for characters that+-- are not allowed in style names.+decodeStyleName :: StyleName -> Text+decodeStyleName t =+ case T.breakOn "_" t of+ (before, rest)+ | T.null rest -> before+ | (hex, rest') <- T.span isHexDigit (T.drop 1 rest)+ , not (T.null hex)+ , "_" `T.isPrefixOf` rest'+ , [(n, "")] <- readHex (T.unpack hex)+ , n <= 0x10FFFF+ -> before <> T.singleton (chr n) <> decodeStyleName (T.drop 1 rest')+ | otherwise -> before <> "_" <> decodeStyleName (T.drop 1 rest)+ -- lookupDefaultStyle' :: Styles -> StyleFamily -> StyleProperties lookupDefaultStyle' Styles{..} family = fromMaybe def@@ -602,17 +622,27 @@ -- | Returns a chain of parent of the current style. The direct parent will -- be the first element of the list, followed by its parent and so on. -- The current style is not in the list.+-- Note that @style:parent-style-name@ references can be cyclic, so we+-- keep track of the names already seen to avoid looping forever. parents :: Style -> Styles -> [Style]-parents style styles = L.unfoldr findNextParent style -- Ha!- where findNextParent Style{..}- = fmap duplicate $ (`lookupStyle` styles) =<< styleParentName+parents style styles = go S.empty style+ where+ go seen Style{..}+ | Just name <- styleParentName+ , not (name `S.member` seen)+ , Just parent <- lookupStyle name styles+ = parent : go (S.insert name seen) parent+ | otherwise+ = [] -- | Looks up the style family of the current style. Normally, every style -- should have one. But if not, all parents are searched.+-- Searching the parents recursively would repeatedly walk the same+-- chains, taking time exponential in its length; the chain is linear,+-- so we can just walk it once. getStyleFamily :: Style -> Styles -> Maybe StyleFamily-getStyleFamily style@Style{..} styles- = styleFamily- <|> F.asum (map (`getStyleFamily` styles) $ parents style styles)+getStyleFamily style styles+ = F.asum $ map styleFamily $ style : parents style styles -- | Each 'Style' has certain 'StyleProperties'. But sometimes not all property -- values are specified. Instead, a value might be inherited from a
@@ -0,0 +1,171 @@+{-# LANGUAGE OverloadedStrings #-}+{- |+ Module : Text.Pandoc.Readers.OOXML.Charts+ Copyright : © 2026 Nacho García+ License : GNU GPL, version 2 or above++ Maintainer : Nacho García <hello@nachogarcia.dev>+ Stability : alpha+ Portability : portable++Reading of DrawingML charts (in PPTX slides and XLSX sheets), from the+values cached in the chart part.+-}+module Text.Pandoc.Readers.OOXML.Charts+ ( Chart(..)+ , ChartSeries(..)+ , readChart+ , chartToBlocks+ ) where++import Codec.Archive.Zip (Archive, findEntryByPath, fromEntry)+import Data.List (find, nub, transpose)+import qualified Data.Map.Strict as M+import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)+import qualified Data.Text as T+import qualified Data.Text.Lazy.Encoding as TL+import Data.Text (Text)+import qualified Text.Pandoc.Builder as B+import Text.Pandoc.Definition+import Text.Pandoc.Readers.OOXML.Shared+import Text.Pandoc.XML.Light+import Text.Read (readMaybe)++-- | A chart's data, as cached in its chart part+data Chart = Chart+ { chartTitle :: Text -- ^ Empty if the chart has no title+ , chartTypes :: [Text] -- ^ Such as @bar@ or @line@, one per plot+ , chartSeries :: [ChartSeries]+ } deriving (Show)++-- | One series: its name, and its points as (category, value)+data ChartSeries = ChartSeries+ { seriesName :: Text+ , seriesPoints :: [(Text, Text)]+ } deriving (Show)++-- | Read a chart part (@c:chartSpace@)+readChart :: Archive -> FilePath -> Either Text Chart+readChart archive path = do+ entry <- maybe (Left $ "Entry not found: " <> T.pack path) Right $+ findEntryByPath path archive+ chartSpace <- parseXMLElement (TL.decodeUtf8 (fromEntry entry))+ chart <- maybe (Left "Missing c:chart") Right $ child "chart" chartSpace+ plotArea <- maybe (Left "Missing c:plotArea") Right $+ child "plotArea" chart+ let plots = filter (("Chart" `T.isSuffixOf`) . qName . elName)+ (elChildren plotArea)+ series = zipWith parseSeries [1..] (concatMap (children "ser") plots)+ title = maybe "" titleText (child "title" chart)+ if all (all (T.null . snd) . seriesPoints) series+ then Left "Chart has no cached values"+ else Right Chart+ { chartTitle = title+ , chartTypes = nub $ map (T.dropEnd 5 . qName . elName) plots+ , chartSeries = series+ }++-- | Text of a chart title: rich text, or a cached cell reference+titleText :: Element -> Text+titleText title =+ case child "tx" title of+ Just tx | Just rich <- child "rich" tx -> drawingMLText rich+ | otherwise -> T.unwords (cachedValues tx)+ Nothing -> ""++-- | Parse a series (@c:ser@). Its categories come from @c:cat@, or+-- @c:xVal@ in a scatter or bubble chart, and its values from @c:val@ or+-- @c:yVal@. A series without categories is numbered from 1, as in+-- PowerPoint.+parseSeries :: Int -> Element -> ChartSeries+parseSeries n ser =+ ChartSeries+ { seriesName = case child "tx" ser of+ Just tx -> T.unwords (cachedValues tx)+ Nothing -> "Series" <> T.pack (show n)+ , seriesPoints =+ [ (fromMaybe (T.pack (show (i + 1))) (M.lookup i categories), v)+ | i <- [0 .. count - 1]+ , let v = fromMaybe "" (M.lookup i values)+ ]+ }+ where+ points names = maybe (0, M.empty) cachedPoints $+ listToMaybe (mapMaybe (`child` ser) names)+ (catCount, categories) = points ["cat", "xVal"]+ (valCount, values) = points ["val", "yVal"]+ count = maximum [catCount, valCount, size values, size categories]+ size m = maybe 0 ((+ 1) . fst) (M.lookupMax m)++-- | The cached points of a data source (@c:cat@, @c:val@, ...): the+-- point count and the value of each point by index. In multi-level+-- categories only the innermost level is read.+cachedPoints :: Element -> (Int, M.Map Int Text)+cachedPoints source =+ case listToMaybe (mapMaybe cache (elChildren source)) of+ Just c ->+ ( fromMaybe 0 $ child "ptCount" c >>= findAttr (unqual "val") >>=+ readMaybe . T.unpack+ , M.fromList+ [ (i, maybe "" strContent (child "v" pt))+ | pt <- children "pt" c+ , Just i <- [findAttr (unqual "idx") pt >>= readMaybe . T.unpack]+ ]+ )+ Nothing -> (0, M.empty)+ where+ cache el = case qName (elName el) of+ name | name `elem` ["strLit", "numLit"] -> Just el+ | name `elem` ["strRef", "numRef"] ->+ find ((`elem` ["strCache", "numCache"]) . qName . elName)+ (elChildren el)+ | name == "multiLvlStrRef" ->+ child "multiLvlStrCache" el >>= child "lvl"+ | otherwise -> Nothing++-- | The cached values of a text source such as a series name (@c:tx@),+-- which is either a cell reference with its cache or a literal @c:v@+cachedValues :: Element -> [Text]+cachedValues tx =+ case child "v" tx of+ Just v -> [strContent v]+ Nothing -> M.elems (snd (cachedPoints tx))++-- | A chart as a table, in a @chart@ div with the chart types as its+-- @type@ attribute. Series that share their categories go in one table,+-- one column per series; otherwise each series gets its own table.+chartToBlocks :: Chart -> [Block]+chartToBlocks chart =+ [Div ("", ["chart"], [("type", T.unwords (chartTypes chart))])+ (map table groups)]+ where+ series = chartSeries chart+ categoriesOf = map fst . seriesPoints+ groups = case series of+ s:rest | all ((== categoriesOf s) . categoriesOf) rest -> [series]+ _ -> map (: []) series+ caption = if T.null (chartTitle chart)+ then Caption Nothing []+ else Caption Nothing [Plain (B.toList (B.text+ (chartTitle chart)))]+ table group =+ let cell t = Cell nullAttr AlignDefault (RowSpan 1) (ColSpan 1)+ [Plain (B.toList (B.text t))]+ headerRow = Row nullAttr $ cell "" : map (cell . seriesName) group+ categories = maybe [] categoriesOf (listToMaybe group)+ values = transpose (map (map snd . seriesPoints) group)+ bodyRows = zipWith (\category vs -> Row nullAttr+ (map cell (category : vs)))+ categories values+ in Table nullAttr caption+ (replicate (length group + 1) (AlignDefault, ColWidthDefault))+ (TableHead nullAttr [headerRow])+ [TableBody nullAttr 0 [] bodyRows]+ (TableFoot nullAttr [])++-- Chart elements are matched by local name, as the c prefix can vary.+child :: Text -> Element -> Maybe Element+child name = find ((== name) . qName . elName) . elChildren++children :: Text -> Element -> [Element]+children name = filter ((== name) . qName . elName) . elChildren
@@ -26,6 +26,9 @@ , findChildrenByName , findElementByName , findAttrByName+ -- * DrawingML text+ , drawingMLParagraphLines+ , drawingMLText ) where import qualified Data.Map as M@@ -93,3 +96,25 @@ findAttrByName ns pref name el = let ns' = ns <> elemToNameSpaces el in findAttr (elemName ns' pref name) el++-- | The text of a DrawingML paragraph (@a:p@), one item per line: line+-- breaks (@a:br@) split it. The text of runs (@a:r@) and fields+-- (@a:fld@) is concatenated as written, without adding spaces between+-- them, since a run boundary can fall anywhere, even inside a word.+drawingMLParagraphLines :: Element -> [Text]+drawingMLParagraphLines = T.splitOn "\n" . T.concat . map piece . elChildren+ where+ piece el = case qName (elName el) of+ "r" -> runText el+ "fld" -> runText el+ "br" -> "\n"+ _ -> ""+ runText = T.concat . map strContent .+ filter ((== "t") . qName . elName) . elChildren++-- | The text of all DrawingML paragraphs (@a:p@) in an element, with a+-- space between paragraphs and in place of line breaks.+drawingMLText :: Element -> Text+drawingMLText = T.unwords . filter (not . T.null) . map T.strip .+ concatMap drawingMLParagraphLines .+ filterElementsName ((== "p") . qName)
@@ -24,6 +24,7 @@ ) where import Control.Monad (void, guard)+import Data.Char (isAlphaNum, isDigit) import Data.Text (Text) import Text.Pandoc.Readers.Org.Parsing import Text.Pandoc.Definition as Pandoc@@ -65,7 +66,7 @@ pure name where latexEnvName :: Monad m => OrgParser m Text- latexEnvName = try $ mappend <$> many1Char alphaNum <*> option "" (textStr "*")+ latexEnvName = try $ mappend <$> takeWhile1P isAlphaNum <*> option "" (textStr "*") listCounterCookie :: Monad m => OrgParser m Int listCounterCookie = try $@@ -73,7 +74,7 @@ *> parseNum <* char ']' <* (skipSpaces <|> lookAhead eol)- where parseNum = (safeRead =<< many1Char digit)+ where parseNum = (safeRead =<< takeWhile1P isDigit) <|> snd <$> (lowerAlpha <|> upperAlpha) bulletListStart :: Monad m => OrgParser m Int@@ -93,7 +94,7 @@ ind <- length <$> many spaceChar fancy <- option False $ True <$ guardEnabled Ext_fancy_lists -- Ordered list markers allowed in org-mode- let styles = (many1Char digit $> (if fancy+ let styles = (takeWhile1P isDigit $> (if fancy then Decimal else DefaultStyle)) : if fancy
@@ -31,11 +31,11 @@ import Text.Pandoc.Class.PandocMonad (PandocMonad) import Text.Pandoc.Definition import Text.Pandoc.Options-import Text.Pandoc.Shared (compactify, compactifyDL, safeRead)+import Text.Pandoc.Shared (compactify, compactifyDL, safeRead, compactifyTable) import Control.Monad (foldM, guard, mzero, void) import Data.Bifunctor (bimap)-import Data.Char (isSpace)+import Data.Char (isAlphaNum, isDigit, isSpace) import Data.Default (Default) import Data.Functor (($>)) import qualified Data.List as L@@ -162,7 +162,8 @@ manyTill ((,) <$> key <*> value) newline where key :: Monad m => OrgParser m Text- key = try $ skipSpaces *> char ':' *> many1Char nonspaceChar+ key = try $ skipSpaces *> char ':' *>+ takeWhile1P (\c -> c /= ' ' && c /= '\t' && c /= '\n' && c /= '\r') value :: Monad m => OrgParser m Text value = skipSpaces *> manyTillChar anyChar endOfValue@@ -212,7 +213,7 @@ skipSpaces metaLineStart stringAnyCase "begin_"- many1Char (satisfy (not . isSpace))+ takeWhile1P (not . isSpace) admonitionBlock :: PandocMonad m => Text -> BlockAttributes -> Text -> OrgParser m (F Blocks)@@ -273,11 +274,15 @@ rawLine :: Monad m => OrgParser m Text rawLine = try $ ("" <$ blankline) <|> anyLine + -- Remove one comma from lines consisting of any number of commas+ -- followed by "*" or "#+", like Emacs' org-unescape-code-in-region. commaEscaped suff = case T.uncons suff of Just (',', cs)- | "*" <- T.take 1 cs -> cs- | "#+" <- T.take 2 cs -> cs- _ -> suff+ | isEscaped cs -> cs+ _ -> suff+ where+ isEscaped cs = let cs' = T.dropWhile (== ',') cs+ in "*" `T.isPrefixOf` cs' || "#+" `T.isPrefixOf` cs' -- | Read but ignore all remaining block headers. ignHeaders :: Monad m => OrgParser m ()@@ -425,7 +430,7 @@ -- | Reads a line number switch option. The line number switch can be used with -- example and source blocks. lineNumberSwitch :: Monad m => OrgParser m (Char, Maybe Text, SwitchPolarity)-lineNumberSwitch = genericSwitch 'n' (manyChar digit)+lineNumberSwitch = genericSwitch 'n' (takeWhileP isDigit) blockOption :: Monad m => OrgParser m (Text, Text) blockOption = try $ do@@ -437,7 +442,7 @@ orgParamValue = try $ skipSpaces *> notFollowedBy orgArgKey- *> ((char '"' *> manyChar (noneOf "\n\r\"") <* char '"') <|>+ *> ((char '"' *> takeWhileP (`notElem` ("\n\r\"" :: [Char])) <* char '"') <|> noneOf "\n\r" `many1TillChar` endOfValue) <* skipSpaces where@@ -579,7 +584,7 @@ include = try $ do metaLineStart <* stringAnyCase "include:" <* skipSpaces filename <- includeTarget- includeArgs <- many (try $ skipSpaces *> many1Char alphaNum)+ includeArgs <- many (try $ skipSpaces *> takeWhile1P isAlphaNum) params <- keyValues blocksParser <- case includeArgs of ("example" : _) -> return $ pure . B.codeBlock <$> parseRaw@@ -608,7 +613,7 @@ manyTill (noneOf "\n\r\t") (char '"') parseRaw :: PandocMonad m => OrgParser m Text- parseRaw = manyChar anyChar+ parseRaw = takeWhileP (const True) blockFilter :: [(Text, Text)] -> [Block] -> [Block] blockFilter params blks =@@ -701,8 +706,8 @@ pure $ B.simpleCaption . B.plain $ ils' let attr = (fromMaybe mempty identMb, [], blockAttrKeyValues blockAttrs)- pure $ B.tableWith attr capt cs th tb tf- _ -> tbl -- should not happen+ pure $ compactifyTable $ B.tableWith attr capt cs th tb tf+ _ -> compactifyTable <$> tbl -- should not happen else mempty -- | A normal org table@@ -718,7 +723,8 @@ let totalWidth = if any (isJust . columnRelWidth) colProps then Just . sum $ map (fromMaybe 1 . columnRelWidth) colProps else Nothing- in B.tableWith nullAttr (Caption Nothing mempty)+ in compactifyTable $+ B.tableWith nullAttr (Caption Nothing mempty) (map (convertColProp totalWidth) colProps) (TableHead nullAttr $ toHeaderRow heads) [TableBody nullAttr 0 [] $ map toRow lns]@@ -762,7 +768,7 @@ <$> (skipSpaces *> char '<' *> optionMaybe tableAlignFromChar)- <*> (optionMaybe (many1Char digit >>= safeRead)+ <*> (optionMaybe (takeWhile1P isDigit >>= safeRead) <* char '>' <* emptyOrgCell) @@ -782,8 +788,10 @@ rowsToTable :: [OrgTableRow] -> F OrgTable-rowsToTable = foldM rowToContent emptyTable+rowsToTable = fmap unreverseRows . foldM rowToContent emptyTable where emptyTable = OrgTable mempty mempty mempty+ -- Rows are accumulated in reverse order (see 'appendToBody').+ unreverseRows tbl = tbl{ orgTableRows = reverse (orgTableRows tbl) } normalizeTable :: OrgTable -> OrgTable normalizeTable (OrgTable colProps heads rows) =@@ -821,10 +829,9 @@ appendToBody :: F [Blocks] -> F OrgTable appendToBody frow = do newRow <- frow- let oldRows = orgTableRows tbl- -- NOTE: This is an inefficient O(n) operation. This should be changed- -- if performance ever becomes a problem.- return tbl{ orgTableRows = oldRows ++ [newRow] }+ -- Rows are prepended to avoid quadratic behavior on long tables;+ -- the final list is reversed in 'rowsToTable'.+ return tbl{ orgTableRows = newRow : orgTableRows tbl } --
@@ -24,7 +24,7 @@ -- | Read and handle space separated org-mode export settings. exportSettings :: PandocMonad m => OrgParser m ()-exportSettings = void $ sepBy skipSpaces exportSetting+exportSettings = void $ sepEndBy exportSetting skipSpaces -- | Setter function for export settings. type ExportSettingSetter a = a -> ExportSettings -> ExportSettings@@ -32,7 +32,7 @@ -- | Read and process a single org-mode export option. exportSetting :: PandocMonad m => OrgParser m () exportSetting = choice- [ booleanSetting "^" (\val es -> es { exportSubSuperscripts = val })+ [ subSupSetting "^" (\val es -> es { exportSubSuperscripts = val }) , booleanSetting "'" (\val es -> es { exportSmartQuotes = val }) , booleanSetting "*" (\val es -> es { exportEmphasizedText = val }) , booleanSetting "-" (\val es -> es { exportSpecialStrings = val })@@ -143,6 +143,22 @@ char '"' *> manyTillChar alphaNum (char '"') +-- | Parses either @t@, @{}@, or @nil@ into a 'SubSupOption' value.+subSupSetting :: Monad m+ => Text+ -> ExportSettingSetter SubSupOption+ -> OrgParser m ()+subSupSetting = genericExportSetting $ subSupBraced <|> subSupBoolean+ where+ subSupBraced = SubSupBraced <$ optionString "{}"++ subSupBoolean = try $ do+ exportBool <- elispBoolean+ return $+ if exportBool+ then SubSupAll+ else SubSupNone+ -- | Parses either @t@, @nil@, or @verbatim@ into a 'TeXExport' value. texSetting :: Monad m => Text@@ -166,7 +182,7 @@ -- | Read any setting string, but ignore it and emit a warning. ignoreAndWarn :: PandocMonad m => OrgParser m () ignoreAndWarn = try $ do- opt <- many1Char nonspaceChar+ opt <- takeWhile1P (\c -> c /= ' ' && c /= '\t' && c /= '\n' && c /= '\r') report (UnknownOrgExportOption opt) return ()
@@ -34,8 +34,9 @@ import Safe (lastMay) import Control.Monad (guard, mplus, mzero, unless, when, void) import Control.Monad.Trans (lift)-import Data.Char (isAlphaNum, isSpace)+import Data.Char (isAlpha, isAlphaNum, isSpace) import qualified Data.Map as M+import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as T @@ -78,8 +79,27 @@ -- | Parse a single Org-mode inline element inline :: PandocMonad m => OrgParser m (F Inlines)-inline =- choice [ whitespace+inline = do+ c <- lookAhead anyChar+ (if isStrEnd c then inlineMarkup else inlineWord c)+ <* (guard =<< newlinesCountWithinLimits)+ <?> "inline"+ where+ -- Fast path for a character that starts a plain word. Every+ -- alternative in 'inlineMarkup' other than those below begins with a+ -- character for which 'isStrEnd' holds, so none of them can match+ -- here; and 'str' always succeeds, so nothing after it is reachable.+ -- The remaining candidates are 'plainLink' (whose URI schemes all+ -- start with a letter), the org-ref form of 'cite' (which must start+ -- with "cite") and 'inlineCodeBlock' (which must start with "src_").+ inlineWord c = case c of+ 'c' -> cite <|> plainLink <|> str+ 's' -> plainLink <|> inlineCodeBlock <|> str+ _ | isAlpha c -> plainLink <|> str+ | otherwise -> str++ inlineMarkup = choice+ [ whitespace , linebreak , cite , footnote@@ -101,8 +121,7 @@ , smartQuotes , specialStrings , symbol- ] <* (guard =<< newlinesCountWithinLimits)- <?> "inline"+ ] -- | Read the rest of the input as inlines. inlines :: PandocMonad m => OrgParser m (F Inlines)@@ -112,7 +131,44 @@ specialChars :: [Char] specialChars = "\"$'()*+-,./:;<=>@[\\]^_{|}~" +-- | True for characters that terminate a plain 'str' run, i.e. the+-- 'specialChars' plus whitespace and newlines. Written as a @case@ so+-- that GHC compiles it to a jump table instead of a linear scan over+-- 'specialChars'; this predicate is applied to every input character.+isStrEnd :: Char -> Bool+isStrEnd c = case c of+ ' ' -> True+ '\n' -> True+ '\r' -> True+ '"' -> True+ '$' -> True+ '\'' -> True+ '(' -> True+ ')' -> True+ '*' -> True+ '+' -> True+ '-' -> True+ ',' -> True+ '.' -> True+ '/' -> True+ ':' -> True+ ';' -> True+ '<' -> True+ '=' -> True+ '>' -> True+ '@' -> True+ '[' -> True+ '\\' -> True+ ']' -> True+ '^' -> True+ '_' -> True+ '{' -> True+ '|' -> True+ '}' -> True+ '~' -> True+ _ -> False + whitespace :: PandocMonad m => OrgParser m (F Inlines) whitespace = pure B.space <$ skipMany1 spaceChar <* updateLastPreCharPos@@ -124,7 +180,7 @@ str :: PandocMonad m => OrgParser m (F Inlines) str = return . B.str <$>- ( many1Char (noneOf $ specialChars ++ "\n\r ") >>= updatePositions' )+ ( takeWhile1P (not . isStrEnd) >>= updatePositions' ) <* updateLastStrPos where updatePositions' str' = str' <$@@ -222,7 +278,7 @@ orgCiteKey :: PandocMonad m => OrgParser m Text orgCiteKey = do char '@'- T.pack <$> many1 (satisfy orgCiteKeyChar)+ takeWhile1P orgCiteKeyChar orgCiteKeyChar :: Char -> Bool orgCiteKeyChar c =@@ -409,7 +465,7 @@ inlineNote :: PandocMonad m => OrgParser m (F Inlines) inlineNote = try $ do string "[fn:"- ref <- manyChar alphaNum+ ref <- takeWhileP (\c -> isAlphaNum c || c == '-' || c == '_') char ':' note <- fmap B.para . trimInlinesF . mconcat <$> many1Till inline (char ']') unless (T.null ref) $@@ -501,7 +557,7 @@ internalLink :: Text -> Inlines -> F Inlines internalLink link title = do ids <- asksF orgStateAnchorIds- if link `elem` ids+ if link `Set.member` ids then return $ B.link ("#" <> link) "" title else let attr' = ("", ["spurious-link"] , [("target", link)]) in return $ B.spanWith attr' (B.emph title)@@ -547,7 +603,7 @@ orgInlineParamValue = try $ skipSpaces *> notFollowedBy (char ':')- *> many1Char (noneOf "\t\n\r ]")+ *> takeWhile1P (`notElem` ("\t\n\r ]" :: [Char])) <* skipSpaces @@ -801,21 +857,26 @@ -- | Read a sub- or superscript expression subOrSuperExpr :: PandocMonad m => OrgParser m (F Inlines)-subOrSuperExpr = try $- simpleSubOrSuperText <|>- (choice [ charsInBalanced '{' '}' (T.singleton <$> noneOf "\n\r")- , enclosing ('(', ')') <$> charsInBalanced '(' ')' (T.singleton <$> noneOf "\n\r")- ] >>= parseFromString (mconcat <$> many inline))- where enclosing (left, right) s = T.cons left $ T.snoc s right+subOrSuperExpr = try $ do+ subSupOption <- getExportSetting exportSubSuperscripts+ case subSupOption of+ SubSupNone -> mzero+ SubSupBraced -> bracedText+ SubSupAll -> simpleSubOrSuperText <|> bracedText <|> parenText+ where+ bracedText = charsInBalanced '{' '}' (T.singleton <$> noneOf "\n\r")+ >>= parseFromString (mconcat <$> many inline)+ parenText = (enclosing ('(', ')') <$>+ charsInBalanced '(' ')' (T.singleton <$> noneOf "\n\r"))+ >>= parseFromString (mconcat <$> many inline)+ enclosing (left, right) s = T.cons left $ T.snoc s right simpleSubOrSuperText :: PandocMonad m => OrgParser m (F Inlines)-simpleSubOrSuperText = try $ do- state <- getState- guard . exportSubSuperscripts . orgStateExportSettings $ state+simpleSubOrSuperText = try $ return . B.str <$> choice [ textStr "*" , mappend <$> option "" (T.singleton <$> oneOf "+-")- <*> many1Char alphaNum+ <*> takeWhile1P isAlphaNum ] inlineLaTeX :: PandocMonad m => OrgParser m (F Inlines)@@ -891,7 +952,7 @@ recursionDepth <- orgStateMacroDepth <$> getState guard $ recursionDepth < 15 string "{{{"- name <- manyChar alphaNum+ name <- takeWhileP isAlphaNum args <- ([] <$ string "}}}") <|> char '(' *> argument `sepBy` char ',' <* eoa expander <- lookupMacro name <$> getState@@ -922,7 +983,7 @@ guard =<< getExportSetting exportSpecialStrings choice [orgDash, orgEllipses, shyHyphen] where- shyHyphen = pure <$> (B.str "\173" <$ string "\\-") <* updatePositions '-'+ shyHyphen = pure <$> (B.str "\173" <$ try (string "\\-")) <* updatePositions '-' orgDash = pure <$> dash <* updatePositions '-' orgEllipses = pure <$> ellipses <* updatePositions '.'
@@ -30,6 +30,7 @@ import Text.Pandoc.URI (urlEncode) import Control.Monad (mzero, void)+import Data.Char (isAlphaNum, isDigit) import Data.List (intercalate, intersperse) import Data.Map (Map) import Data.Maybe (fromMaybe)@@ -61,13 +62,13 @@ keywordLine :: PandocMonad m => OrgParser m () keywordLine = try $ do- key <- T.toLower <$> metaKey+ key <- metaKey case Map.lookup key keywordHandlers of Nothing -> fail $ "Unknown keyword: " ++ T.unpack key Just hd -> hd metaKey :: Monad m => OrgParser m Text-metaKey = T.toLower <$> many1Char (noneOf ": \n\r")+metaKey = T.toLower <$> takeWhile1P (`notElem` (": \n\r" :: [Char])) <* char ':' <* skipSpaces @@ -175,7 +176,9 @@ -- | Parse a link type definition (like @wp https://en.wikipedia.org/wiki/@). addLinkFormatter :: Monad m => OrgParser m () addLinkFormatter = try $ do- linkType <- T.cons <$> letter <*> manyChar (alphaNum <|> oneOf "-_") <* skipSpaces+ linkType <- T.cons <$> letter+ <*> takeWhileP (\c -> isAlphaNum c || c == '-' || c == '_')+ <* skipSpaces formatter <- parseFormat updateState $ \s -> let fs = orgStateLinkFormatters s@@ -253,7 +256,7 @@ where todoKeyword :: Monad m => OrgParser m Text todoKeyword = do- keyword <- many1Char nonspaceChar+ keyword <- takeWhile1P (\c -> c /= ' ' && c /= '\t' && c /= '\n' && c /= '\r') let cleanKeyword = T.takeWhile (/= '(') keyword skipSpaces return cleanKeyword@@ -278,14 +281,15 @@ macroDefinition :: Monad m => OrgParser m (Text, [Text] -> Text) macroDefinition = try $ do- macroName <- many1Char nonspaceChar <* skipSpaces+ macroName <- takeWhile1P (\c -> c /= ' ' && c /= '\t' && c /= '\n' && c /= '\r')+ <* skipSpaces firstPart <- expansionPart (elemOrder, parts) <- unzip <$> many ((,) <$> placeholder <*> expansionPart) let expander = mconcat . alternate (firstPart:parts) . reorder elemOrder return (macroName, expander) where placeholder :: Monad m => OrgParser m Int- placeholder = try . fmap (fromMaybe 1 . safeRead) $ char '$' *> many1Char digit+ placeholder = try . fmap (fromMaybe 1 . safeRead) $ char '$' *> takeWhile1P isDigit expansionPart :: Monad m => OrgParser m Text expansionPart = try $ manyChar (notFollowedBy placeholder *> noneOf "\n\r")
@@ -35,6 +35,7 @@ , returnF , ExportSettings (..) , ArchivedTreesOption (..)+ , SubSupOption (..) , TeXExport (..) , optionsToParserState ) where@@ -91,7 +92,7 @@ -- | Org-mode parser state data OrgParserState = OrgParserState- { orgStateAnchorIds :: [Text]+ { orgStateAnchorIds :: Set.Set Text , orgStateEmphasisCharStack :: [Char] , orgStateEmphasisPreChars :: [Char] -- ^ Chars allowed to occur before -- emphasis; spaces and newlines are@@ -163,7 +164,7 @@ defaultOrgParserState :: OrgParserState defaultOrgParserState = OrgParserState- { orgStateAnchorIds = []+ { orgStateAnchorIds = Set.empty , orgStateEmphasisPreChars = "-\t ('\"{\x200B" , orgStateEmphasisPostChars = "-\t\n .,:!?;'\")}[\x200B" , orgStateEmphasisCharStack = []@@ -240,6 +241,14 @@ | ArchivedTreesNoExport -- ^ Exclude archived trees from exporting | ArchivedTreesHeadlineOnly -- ^ Export only the headline, discard the contents +-- | Options for the handling of TeX-like sub- and superscript syntax.+-- Represents allowed values of Emacs variable+-- @org-export-with-sub-superscripts@.+data SubSupOption+ = SubSupAll -- ^ Interpret all sub- and superscripts (@t@)+ | SubSupBraced -- ^ Interpret only expressions in braces (@{}@)+ | SubSupNone -- ^ Never interpret sub-/superscripts (@nil@)+ -- | Options for the handling of LaTeX environments and fragments. -- Represents allowed values of Emacs variable @org-export-with-latex@. data TeXExport@@ -261,7 +270,8 @@ , exportPreserveBreaks :: Bool -- ^ Whether to preserve linebreaks , exportSmartQuotes :: Bool -- ^ Parse quotes smartly , exportSpecialStrings :: Bool -- ^ Parse ellipses and dashes smartly- , exportSubSuperscripts :: Bool -- ^ TeX-like syntax for sub- and superscripts+ , exportSubSuperscripts :: SubSupOption+ -- ^ TeX-like syntax for sub- and superscripts , exportWithAuthor :: Bool -- ^ Include author in final meta-data , exportWithCreator :: Bool -- ^ Include creator in final meta-data , exportWithEmail :: Bool -- ^ Include email in final meta-data@@ -286,7 +296,7 @@ , exportPreserveBreaks = False , exportSmartQuotes = False , exportSpecialStrings = True- , exportSubSuperscripts = True+ , exportSubSuperscripts = SubSupAll , exportWithAuthor = True , exportWithCreator = True , exportWithEmail = True
@@ -37,6 +37,8 @@ , manyChar , many1Char , manyTillChar+ , takeWhileP+ , takeWhile1P , many1Till , many1TillChar , notFollowedBy'@@ -94,6 +96,7 @@ , try , sepBy , sepBy1+ , sepEndBy , sepEndBy1 , endBy1 , option@@ -106,6 +109,7 @@ , getPosition ) where +import qualified Data.Set as Set import Data.Text (Text) import Text.Pandoc.Readers.Org.ParserState @@ -223,7 +227,7 @@ orgAnchor :: Monad m => OrgParser m Text orgAnchor = try $ do string "<<"- anchorId <- many1Char (noneOf "\t\n\r<>\"' ")+ anchorId <- takeWhile1P (`notElem` ("\t\n\r<>\"' " :: [Char])) string ">>" skipSpaces recordAnchorId anchorId@@ -231,4 +235,4 @@ recordAnchorId :: Monad m => Text -> OrgParser m () recordAnchorId i = updateState $ \s ->- s{ orgStateAnchorIds = i : orgStateAnchorIds s }+ s{ orgStateAnchorIds = Set.insert i (orgStateAnchorIds s) }
@@ -32,7 +32,8 @@ `elem` imageExtensions isKnownProtocolUri = any (\x -> (x <> "://") `T.isPrefixOf` fp) protocols - imageExtensions = [ ".jpeg", ".jpg", ".png", ".gif", ".svg", ".webp", ".jxl", ".avif" ]+ imageExtensions = [ ".jpeg", ".jpg", ".png", ".gif", ".svg", ".webp", ".jxl", ".avif",+ ".pdf" ] protocols = [ "file", "http", "https" ] -- | Cleanup and canonicalize a string describing a link. Return @Nothing@ if
@@ -16,7 +16,7 @@ import Control.Monad (void) import Control.Monad.Except (throwError)-import Data.Char (isAsciiUpper, digitToInt)+import Data.Char (isAlphaNum, isAsciiUpper, isDigit, isLetter, digitToInt) import Data.Default (Default) import Text.Pandoc.Logging import Text.Pandoc.Options@@ -30,7 +30,7 @@ import qualified Text.Pandoc.Builder as B import qualified Data.Text as T import qualified Data.Text.Read as TR-import Text.Pandoc.Shared (stringify, textToIdentifier, tshow)+import Text.Pandoc.Shared (stringifyInlines, textToIdentifier, tshow) import Data.Set (Set) import Data.Functor (($>)) import Data.Maybe (listToMaybe, fromMaybe)@@ -210,7 +210,7 @@ lookAhead (char '<') case ctrl of 'B' -> B.strong <$> argument- 'C' -> B.code . stringify <$> argument+ 'C' -> B.code . stringifyInlines <$> argument 'F' -> B.spanWith (mempty, ["filename"], mempty) <$> argument 'I' -> B.emph <$> argument 'S' -> argument -- TODO map nbsps@@ -218,7 +218,7 @@ 'Z' -> argument $> mempty 'E' -> do- a <- stringify <$> argument+ a <- stringifyInlines <$> argument case entity a of -- per spec: -- Pod parsers, when faced with some unknown "E<identifier>" code,@@ -294,9 +294,9 @@ let name' = fromMaybe (defaultLinkName dest) name in case dest of LinkUrl _ href -> B.link href "" name'- LinkMan nm Nothing -> B.linkWith (mempty, mempty, [("manual", stringify nm)]) "" "" name'- LinkMan nm (Just sc) -> B.linkWith (mempty, mempty, [("manual", stringify nm), ("section", stringify sc)]) "" "" name'- LinkInternal sc -> B.link ("#" <> identifier (stringify sc)) "" name'+ LinkMan nm Nothing -> B.linkWith (mempty, mempty, [("manual", stringifyInlines nm)]) "" "" name'+ LinkMan nm (Just sc) -> B.linkWith (mempty, mempty, [("manual", stringifyInlines nm), ("section", stringifyInlines sc)]) "" "" name'+ LinkInternal sc -> B.link ("#" <> identifier (stringifyInlines sc)) "" name' linkName sp ex = optionMaybe $ try $ many (try format@@ -312,12 +312,12 @@ -- manual page reference, so what we are doing here is roughly equivalent -- even though it is nonsense url ex = do- scheme <- many1Char (letter <|> digit <|> char '_')+ scheme <- takeWhile1P (\c -> isLetter c || isDigit c || c == '_') colon <- T.singleton <$> char ':' <* notFollowedBy (char ':') rst <- many (format <|> B.str <$> many1Char (podCharLess ex)) return $ LinkUrl (B.str scheme <> B.str colon <> mconcat rst)- (scheme <> colon <> stringify rst)+ (scheme <> colon <> stringifyInlines (mconcat rst)) quotedSection sp close ex = do let mystr = B.str <$> many1Char (podCharLess ('\"':ex) <|> try (char '"' <* notFollowedBy close)) char '"'@@ -353,7 +353,7 @@ nonEmptyLine :: PandocMonad m => PodParser m T.Text nonEmptyLine = try $ do- pre <- manyChar spaceChar+ pre <- takeWhileP (\c -> c == ' ' || c == '\t') something <- T.singleton <$> nonspaceChar post <- anyLineNewline return $ pre <> something <> post@@ -368,7 +368,7 @@ optional blanklines return $ B.codeBlock $ mconcat $ start:lns where- startVerbatimLine = many1Char spaceChar <> nonEmptyLine+ startVerbatimLine = takeWhile1P (\c -> c == ' ' || c == '\t') <> nonEmptyLine -- =begin/=end/=for and data paragraphs -- The =begin/=end (and single-paragraph =for variant) markers in Pod are@@ -390,7 +390,7 @@ -- structure. It seems unlikely this would be encountered in the wild. regionIdentifier :: PandocMonad m => PodParser m T.Text-regionIdentifier = many1Char (alphaNum <|> oneOf "-_")+regionIdentifier = takeWhile1P (\c -> isAlphaNum c || c == '-' || c == '_') for :: PandocMonad m => PodParser m Blocks for = do
@@ -26,7 +26,9 @@ import qualified Data.Text.Lazy.Encoding as TL import Data.Text (Text) import System.FilePath (splitFileName)+import qualified System.FilePath.Posix as Posix import Text.Pandoc.Readers.OOXML.Shared+import Text.Pandoc.Shared (collapseFilePath) import Text.Pandoc.XML.Light import Text.Read (readMaybe) @@ -46,6 +48,9 @@ , slidePath :: FilePath , slideElement :: Element -- The parsed p:sld element , slideRels :: [(Text, Text)] -- Slide relationships+ , slideNotes :: Maybe Element -- The parsed p:notes element, if any+ , slideLayout :: Maybe Element -- The slide's p:sldLayout+ , slideMaster :: Maybe Element -- Its layout's p:sldMaster } deriving (Show) -- | Presentation-level information from presentation.xml@@ -208,7 +213,53 @@ slideRelsPath <- getPresentationRelsPath archive slidePath' slideRels' <- loadRelationships archive slideRelsPath - return $ PptxSlide sid slidePath' slideElem slideRels'+ -- Follow the slide's own notesSlide relationship: notesSlideN.xml need+ -- not belong to slideN.xml. A missing or unreadable notes part means+ -- the slide has no notes; it never fails the conversion.+ let notesElem = do+ notesTarget <- findRelTargetByType archive slideRelsPath "notesSlide"+ either (const Nothing) Just $+ loadXMLFromArchive archive (resolveTarget slidePath' notesTarget)++ -- The slide's layout and master, which the text of its placeholders+ -- inherits from; either may be missing.+ let related source typeName = do+ t <- findRelTargetByType archive (relsPathOf source) typeName+ let path = resolveTarget source t+ el <- either (const Nothing) Just $ loadXMLFromArchive archive path+ return (path, el)+ layout = related slidePath' "slideLayout"+ master = layout >>= \(layoutPath, _) -> related layoutPath "slideMaster"++ return $ PptxSlide sid slidePath' slideElem slideRels' notesElem+ (snd <$> layout) (snd <$> master)++-- | The path of a part's relationships+relsPathOf :: FilePath -> FilePath+relsPathOf path =+ let (dir, file) = splitFileName path+ in dir ++ "/_rels/" ++ file ++ ".rels"++-- | Find the target of the first relationship in a .rels part whose Type+-- ends with the given name, such as @notesSlide@.+findRelTargetByType :: Archive -> FilePath -> Text -> Maybe Text+findRelTargetByType archive relsPath typeName = do+ entry <- findEntryByPath relsPath archive+ relsElem <- either (const Nothing) Just $ parseXMLFromEntry entry+ rel <- find hasType (onlyElems $ elContent relsElem)+ findAttr (unqual "Target") rel+ where+ hasType el = maybe False (("/" <> typeName) `T.isSuffixOf`) $+ findAttr (unqual "Type") el++-- | Resolve a relationship target to an archive path. A target starting+-- with @/@ is relative to the package root; any other target is relative+-- to the folder of the part that holds the relationship.+resolveTarget :: FilePath -> Text -> FilePath+resolveTarget source target =+ case T.unpack target of+ '/':path -> path+ path -> collapseFilePath (Posix.takeDirectory source Posix.</> path) -- | Helper: Maybe a -> Either Text a maybeToEither :: Text -> Maybe a -> Either Text a
@@ -14,40 +14,68 @@ module Text.Pandoc.Readers.Pptx.Shapes ( PptxShape(..) , PptxParagraph(..)+ , TableCell(..) , BulletType(..) , parseShapes , parseShape+ , parseShapeWith+ , inheritedBullets , shapeToBlocks+ , paragraphsToBlocks , isTitlePlaceholder , extractDrawingMLText ) where import Codec.Archive.Zip (Archive, findEntryByPath, fromEntry) import qualified Data.ByteString.Lazy as B+import Data.Char (isSpace) import Data.List (find, groupBy)-import Data.Maybe (mapMaybe)+import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)+import qualified Data.Set as S import qualified Data.Text as T import Data.Text (Text) import Text.Read (readMaybe)+import qualified Text.Pandoc.Builder as B import Text.Pandoc.Class.PandocMonad (PandocMonad) import qualified Text.Pandoc.Class.PandocMonad as P import Text.Pandoc.Definition import Text.Pandoc.Readers.OOXML.Shared+import Text.Pandoc.Readers.OOXML.Charts import Text.Pandoc.Readers.Pptx.SmartArt+import Text.Pandoc.Shared (collapseFilePath)+import Text.Pandoc.Walk (walk) import Text.Pandoc.XML.Light+import Text.TeXMath (writeTeX)+import Text.TeXMath.Readers.OMML (readOMML)+import qualified System.FilePath.Posix as Posix -- | Paragraph with bullet/numbering information data PptxParagraph = PptxParagraph { paraLevel :: Int -- Bullet level (0, 1, 2...) , paraBullet :: BulletType- , paraText :: Text+ , paraText :: [Inline] } deriving (Show) +-- | A table cell (@a:tc@): its text, its row and column span, and whether+-- another cell's span covers it from the left (@hMerge@) or from above+-- (@vMerge@)+data TableCell = TableCell+ { tcText :: Text+ , tcRowSpan :: Int+ , tcColSpan :: Int+ , tcHMerge :: Bool+ , tcVMerge :: Bool+ } deriving (Show)++tcCovered :: TableCell -> Bool+tcCovered c = tcHMerge c || tcVMerge c+ -- | Bullet type data BulletType = NoBullet | Bullet -- Has bullet (character detected or implicit) | WingdingsBullet -- Detected via Wingdings symbol+ | Numbered ListAttributes -- Automatic numbering (a:buAutoNum) deriving (Show, Eq) -- | Shape types in PPTX slides@@ -58,11 +86,12 @@ , picTitle :: Text , picAlt :: Text }- | PptxTable [[Text]] -- Simple text cells for now+ | PptxTable [[TableCell]] -- Rows of cells | PptxDiagramRef { dgmDataRelId :: Text -- Relationship to data.xml , dgmLayoutRelId :: Text -- Relationship to layout.xml }+ | PptxChartRef Text Text -- Relationship to the chart, URI | PptxGraphic Text -- Placeholder for other graphics deriving (Show) @@ -76,12 +105,20 @@ -- | Parse individual shape element parseShape :: NameSpaces -> Element -> Maybe PptxShape-parseShape ns el+parseShape = parseShapeWith (const Nothing)++-- | Parse a shape, given the bullet its paragraphs inherit at each level+-- (0 for the first) when they don't set one themselves+parseShapeWith :: (Int -> Maybe BulletType) -> NameSpaces -> Element+ -> Maybe PptxShape+parseShapeWith inherited ns el -- Text box: <p:sp> with <p:txBody>+ -- A slide number placeholder only holds the number PowerPoint shows+ | isSlideNumberPlaceholder ns el = Nothing | isElem ns "p" "sp" el = case findChildByName ns "p" "txBody" el of Just txBody ->- let paras = parseParagraphs ns txBody+ let paras = parseParagraphs inherited ns txBody in if null paras then Nothing else Just $ PptxTextBox paras@@ -133,34 +170,53 @@ (Just dataRelId, Just layoutRelId) -> Just $ PptxDiagramRef dataRelId layoutRelId _ -> Just $ PptxGraphic "diagram-missing-rels"+ else if uri == chartUri+ then+ -- The c prefix is usually declared on c:chart itself+ case find ((== "chart") . qName . elName)+ (elChildren graphicData) of+ Just chartElem+ | Just relId <- findAttrByName+ (ns <> elemToNameSpaces chartElem) "r" "id"+ chartElem+ -> Just $ PptxChartRef relId uri+ _ -> Just $ PptxGraphic ("other: " <> uri) else- -- Other graphic (chart, etc.)+ -- Other graphic Just $ PptxGraphic ("other: " <> uri) -- Skip other shapes for now | otherwise = Nothing -- | Parse table rows (simple text extraction)-parseTableRows :: NameSpaces -> Element -> [[Text]]+parseTableRows :: NameSpaces -> Element -> [[TableCell]] parseTableRows ns tblElem = let trElems = findChildrenByName ns "a" "tr" tblElem in map (parseTableRow ns) trElems -parseTableRow :: NameSpaces -> Element -> [Text]+parseTableRow :: NameSpaces -> Element -> [TableCell] parseTableRow ns trElem = let tcElems = findChildrenByName ns "a" "tc" trElem- in map extractCellText tcElems+ in map parseCell tcElems where- extractCellText tcElem =- -- Get text from txBody/a:p/a:r/a:t- case findChildByName ns "a" "txBody" tcElem of- Just txBody -> extractDrawingMLText txBody- Nothing -> ""+ parseCell tcElem = TableCell+ { tcText = case findChildByName ns "a" "txBody" tcElem of+ -- Get text from txBody/a:p/a:r/a:t+ Just txBody -> extractDrawingMLText txBody+ Nothing -> ""+ , tcRowSpan = span' "rowSpan" tcElem+ , tcColSpan = span' "gridSpan" tcElem+ , tcHMerge = flag "hMerge" tcElem+ , tcVMerge = flag "vMerge" tcElem+ }+ span' name el = maybe 1 (max 1) $+ findAttr (unqual name) el >>= readMaybe . T.unpack+ flag name el = findAttr (unqual name) el `elem` [Just "1", Just "true"] -- | Convert shape to Pandoc blocks shapeToBlocks :: PandocMonad m => Archive -> [(Text, Text)] -> PptxShape -> m [Block]-shapeToBlocks _archive _rels (PptxTextBox paras) =- return $ paragraphsToBlocks paras+shapeToBlocks _archive rels (PptxTextBox paras) =+ return $ resolveLinks rels $ paragraphsToBlocks paras shapeToBlocks archive rels (PptxPicture relId title alt) = do -- Resolve relationship to get media path case lookup relId rels of@@ -178,13 +234,15 @@ return [Para [Image nullAttr altText (mediaPath, title)]] shapeToBlocks _archive _rels (PptxTable rows) =- -- Simple table representation for now- case rows of+ case spannedRows rows of [] -> return [] (headerRow:bodyRows) -> do- let makeCell text = Cell nullAttr AlignDefault (RowSpan 1) (ColSpan 1) [Plain [Str text]]- headerCells = map makeCell headerRow- bodyCells = map (map makeCell) bodyRows+ let makeCell (TableCell text rs cs _ _) =+ Cell nullAttr AlignDefault (RowSpan rs) (ColSpan cs)+ [Plain (B.toList (B.text text))]+ visible = map makeCell . filter (not . tcCovered)+ headerCells = visible headerRow+ bodyCells = map visible bodyRows caption = Caption Nothing [] colSpec = replicate (length headerRow) (AlignDefault, ColWidthDefault) headerRow' = Row nullAttr headerCells@@ -202,10 +260,46 @@ return [Para [Str $ "[Diagram parse error: " <> err <> "]"]] Right diagram -> return $ diagramToBlocks diagram+shapeToBlocks archive rels (PptxChartRef relId uri) =+ -- A chart whose part can't be read keeps the placeholder+ return $ either (const [Para [Str $ "[Graphic: other: " <> uri <> "]"]])+ chartToBlocks $ do+ target <- maybe (Left $ "Relationship not found: " <> relId) Right $+ lookup relId rels+ readChart archive $ case T.unpack target of+ '/':path -> path+ path -> collapseFilePath ("ppt/slides" Posix.</> path) shapeToBlocks _archive _rels (PptxGraphic text) = -- Placeholder for other graphics (charts, etc.) return [Para [Str $ "[Graphic: " <> text <> "]"]] +-- | Make the spans of a table's cells fit pandoc's table model. Each row+-- has a cell for every grid column, with the cells covered by a span+-- marked. As a cell can't span the header row and the body, the header+-- row's spans are kept to that row, and the body cells they covered+-- become empty cells of their own. Row spans are clipped to the rows+-- there are.+spannedRows :: [[TableCell]] -> [[TableCell]]+spannedRows [] = []+spannedRows (header:body) =+ map (\c -> c{ tcRowSpan = 1 }) header : go headerSpanCols (length body) body+ where+ headerSpanCols = S.fromList+ [ col' | (col, c) <- zip [0 :: Int ..] header+ , tcRowSpan c > 1, not (tcCovered c)+ , col' <- [col .. col + tcColSpan c - 1] ]+ -- cols: the columns still covered by a span from the header row;+ -- left: the number of rows from this one to the last+ go _ _ [] = []+ go cols left (row:rows) =+ let cells = zip [0 ..] row+ fix (col, c)+ | col `S.member` cols && tcVMerge c =+ TableCell "" 1 1 False False+ | otherwise = c{ tcRowSpan = min (tcRowSpan c) left }+ stillCovered col = maybe False tcVMerge (lookup col cells)+ in map fix cells : go (S.filter stillCovered cols) (left - 1) rows+ -- | Resolve media path (handle relative paths) resolveMediaPath :: Text -> Text resolveMediaPath target =@@ -223,17 +317,19 @@ Nothing -> Nothing -- | Parse paragraphs from text box-parseParagraphs :: NameSpaces -> Element -> [PptxParagraph]-parseParagraphs ns txBody =+parseParagraphs :: (Int -> Maybe BulletType) -> NameSpaces -> Element+ -> [PptxParagraph]+parseParagraphs inherited ns txBody = let pElems = findChildrenByName ns "a" "p" txBody- in map (parseParagraph ns) pElems+ in map (parseParagraph inherited ns) pElems -- | Parse individual paragraph-parseParagraph :: NameSpaces -> Element -> PptxParagraph-parseParagraph ns pElem =+parseParagraph :: (Int -> Maybe BulletType) -> NameSpaces -> Element+ -> PptxParagraph+parseParagraph inherited ns pElem = let level = parseBulletLevel ns pElem- bullet = detectBulletType ns pElem- text = extractParagraphText ns pElem+ bullet = detectBulletType (inherited level) ns pElem+ text = paragraphInlines pElem in PptxParagraph level bullet text -- | Parse bullet level from paragraph properties@@ -245,19 +341,85 @@ Just lvl -> lvl Nothing -> 0 -- Default to level 0 --- | Detect bullet type-detectBulletType :: NameSpaces -> Element -> BulletType-detectBulletType ns pElem =- -- Check for explicit <a:pPr><a:buChar>- case findChildByName ns "a" "pPr" pElem >>=- findChildByName ns "a" "buChar" of- Just _buCharElem -> Bullet- Nothing ->+-- | Detect bullet type: the paragraph's own, or else the one it inherits+detectBulletType :: Maybe BulletType -> NameSpaces -> Element -> BulletType+detectBulletType inherited ns pElem =+ case findChildByName ns "a" "pPr" pElem >>= explicitBullet of+ Just bullet -> bullet+ Nothing -- Check for Wingdings symbol (common in PowerPoint)- if hasWingdingsSymbol ns pElem- then WingdingsBullet- else NoBullet+ | hasWingdingsSymbol ns pElem -> WingdingsBullet+ | otherwise -> fromMaybe NoBullet inherited +-- | The bullet that paragraph properties (@a:pPr@ or @a:lvlNpPr@) set,+-- if they set one+explicitBullet :: Element -> Maybe BulletType+explicitBullet pPr = listToMaybe (mapMaybe bullet (elChildren pPr))+ where+ bullet el = case qName (elName el) of+ "buChar" -> Just Bullet+ "buBlip" -> Just Bullet+ "buNone" -> Just NoBullet+ "buAutoNum" -> Just (Numbered (numbering el))+ _ -> Nothing+ numbering el =+ let scheme = fromMaybe "arabicPeriod" (findAttr (unqual "type") el)+ start = fromMaybe 1 $+ findAttr (unqual "startAt") el >>= readMaybe . T.unpack+ style | "arabic" `T.isPrefixOf` scheme = Decimal+ | "alphaLc" `T.isPrefixOf` scheme = LowerAlpha+ | "alphaUc" `T.isPrefixOf` scheme = UpperAlpha+ | "romanLc" `T.isPrefixOf` scheme = LowerRoman+ | "romanUc" `T.isPrefixOf` scheme = UpperRoman+ | otherwise = DefaultStyle+ delim | "ParenBoth" `T.isSuffixOf` scheme = TwoParens+ | "ParenR" `T.isSuffixOf` scheme = OneParen+ | "Period" `T.isSuffixOf` scheme = Period+ | otherwise = DefaultDelim+ in (start, style, delim)++-- | The bullet a shape's paragraphs inherit at each level (0 for the+-- first), from the shape's own list style, then the matching placeholder+-- on the slide layout and on the slide master, then the master's text+-- style for that kind of placeholder (or for other text).+inheritedBullets :: Maybe Element -- ^ Slide layout+ -> Maybe Element -- ^ Slide master+ -> Element -- ^ Shape+ -> Int -> Maybe BulletType+inheritedBullets layout master shape level =+ listToMaybe $ mapMaybe (>>= explicitBullet) $+ [lstLevel shape] +++ case placeholder shape of+ Just (phType, phIdx) ->+ [ layout >>= findPlaceholder phType phIdx >>= lstLevel+ , master >>= findPlaceholder (masterType phType) Nothing >>= lstLevel+ , textStyle (styleFor phType) ]+ Nothing -> [textStyle "otherStyle"]+ where+ lvlName = "lvl" <> T.pack (show (level + 1)) <> "pPr"+ child name el = find ((== name) . qName . elName) (elChildren el)+ lstLevel sp = child "txBody" sp >>= child "lstStyle" >>= child lvlName+ textStyle name = master >>= child "txStyles" >>= child name >>=+ child lvlName+ placeholder sp = do+ ph <- child "nvSpPr" sp >>= child "nvPr" >>= child "ph"+ return ( fromMaybe "obj" (findAttr (unqual "type") ph)+ , findAttr (unqual "idx") ph )+ -- A placeholder is matched by index if it has one, else by type+ findPlaceholder phType phIdx root = do+ tree <- child "cSld" root >>= child "spTree"+ let phs = [ (sp, p) | sp <- elChildren tree+ , Just p <- [placeholder sp] ]+ byIdx = [ sp | Just i <- [phIdx], (sp, (_, j)) <- phs, j == Just i ]+ byType = [ sp | (sp, (t, _)) <- phs, masterType t == phType ||+ t == phType ]+ listToMaybe (byIdx ++ byType)+ masterType t = if t `elem` ["obj", "body"] then "body" else t+ styleFor t+ | t `elem` ["title", "ctrTitle"] = "titleStyle"+ | t `elem` ["obj", "body", "subTitle"] = "bodyStyle"+ | otherwise = "otherStyle"+ -- | Check if paragraph starts with Wingdings symbol hasWingdingsSymbol :: NameSpaces -> Element -> Bool hasWingdingsSymbol ns pElem =@@ -271,51 +433,163 @@ Nothing -> False in any checkRun runs --- | Extract text from paragraph-extractParagraphText :: NameSpaces -> Element -> Text-extractParagraphText _ns pElem =- -- Find all <a:t> elements and concatenate- let textElems = filterElementsName (\qn -> qName qn == "t") pElem- texts = map strContent textElems- in T.unwords $ filter (not . T.null) texts+-- | Inlines of a paragraph: the text of its runs and fields with their+-- bold, italic, superscript and subscript, its line breaks, its+-- hyperlinks, and its equations (@a14:m@) as math. A hyperlink's target+-- is a relationship id at this point, which 'resolveLinks' replaces.+paragraphInlines :: Element -> [Inline]+paragraphInlines pElem =+ B.toList $ foldMap linked $ groupBy (\a b -> fst a == fst b) $+ concatMap piece (elChildren pElem)+ where+ piece el = case qName (elName el) of+ "r" -> [(runLink el, Left (runFormat el, runText el))]+ "fld" -> [(runLink el, Left (runFormat el, runText el))]+ "br" -> [(Nothing, Right B.linebreak)]+ "m" -> [(Nothing, Right (foldMap mathInlines (elChildren el)))]+ _ -> []+ linked grp =+ let inls = foldMap formatted $ groupBy sameFormat (map snd grp)+ in case grp of+ (Just relId, _) : _ ->+ B.linkWith ("", [], [(linkRelAttr, relId)]) "" "" inls+ _ -> inls+ sameFormat (Left (f, _)) (Left (g, _)) = f == g+ sameFormat _ _ = False+ -- Runs with the same format are joined before it is applied, so+ -- that adjacent bold runs make one strong span.+ formatted pieces@(Left (fmt, _) : _) =+ applyFormat fmt (T.concat [t | Left (_, t) <- pieces])+ formatted pieces = mconcat [ils | Right ils <- pieces] --- | Extract text from DrawingML element (finds all <a:t> descendants)+-- | The format of a run: bold, italic, and its baseline shift (positive+-- for superscript, negative for subscript)+type RunFormat = (Bool, Bool, Int)++runFormat :: Element -> RunFormat+runFormat run =+ case find ((== "rPr") . qName . elName) (elChildren run) of+ Nothing -> (False, False, 0)+ Just rPr ->+ ( flag "b" rPr, flag "i" rPr+ , fromMaybe 0 $+ findAttr (unqual "baseline") rPr >>= readMaybe . T.unpack )+ where+ flag name el = findAttr (unqual name) el `elem` [Just "1", Just "true"]++runText :: Element -> Text+runText = T.concat . map strContent .+ filter ((== "t") . qName . elName) . elChildren++-- | Text with a run format. Spaces at the edges stay outside the+-- formatting, as Markdown emphasis can't start or end with a space.+applyFormat :: RunFormat -> Text -> B.Inlines+applyFormat (bold, italic, baseline) txt+ | not (bold || italic || baseline /= 0) || T.null core = B.text txt+ | otherwise = edge leading <> wrap (B.text core) <> edge trailing+ where+ isBlank = (`elem` (" \t\r\n" :: String))+ core = T.dropAround isBlank txt+ leading = T.takeWhile isBlank txt+ trailing = T.takeWhileEnd isBlank txt+ edge t = if T.null t then mempty else B.space+ wrap = (if bold then B.strong else id) .+ (if italic then B.emph else id) .+ (if baseline > 0 then B.superscript+ else if baseline < 0 then B.subscript else id)++-- | The relationship id of a run's hyperlink (@a:hlinkClick@). Links+-- that act within the presentation, such as a jump to another slide,+-- carry an @action@ and are not kept.+runLink :: Element -> Maybe Text+runLink run = do+ rPr <- find ((== "rPr") . qName . elName) (elChildren run)+ link <- find ((== "hlinkClick") . qName . elName) (elChildren rPr)+ case findAttr (unqual "action") link of+ Just _ -> Nothing+ Nothing -> fmap attrVal $ find isRelId (elAttribs link)+ where+ isRelId a = qName (attrKey a) == "id" && qURI (attrKey a) == Just relNs+ relNs =+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships"++-- | The attribute that marks a hyperlink whose target is still a+-- relationship id+linkRelAttr :: Text+linkRelAttr = "pptx-relationship"++-- | Replace the relationship id of each hyperlink read by+-- 'paragraphInlines' with the relationship's target. A link whose+-- relationship is missing is replaced by its text.+resolveLinks :: [(Text, Text)] -> [Block] -> [Block]+resolveLinks rels = walk resolve+ where+ resolve (Link (_, _, [(key, relId)]) inls _)+ | key == linkRelAttr =+ case lookup relId rels of+ Just target -> Link nullAttr inls (target, "")+ Nothing -> Span nullAttr inls+ resolve x = x++-- | An equation (@m:oMathPara@ or @m:oMath@) as math. If texmath can't+-- read it, its text is kept.+mathInlines :: Element -> B.Inlines+mathInlines el =+ case readOMML (showElement el) of+ Right exps+ | qName (elName el) == "oMathPara" -> B.displayMath (writeTeX exps)+ | otherwise -> B.math (writeTeX exps)+ Left _ -> B.text $ T.concat $ map strContent $+ filterElementsName ((== "t") . qName) el++-- | Extract text from DrawingML element (all its paragraphs) extractDrawingMLText :: Element -> Text-extractDrawingMLText el =- let textElems = filterElementsName (\qn -> qName qn == "t") el- texts = map strContent textElems- in T.unwords $ filter (not . T.null) texts+extractDrawingMLText = drawingMLText --- | Convert paragraphs to blocks, grouping bullets into lists+-- | Convert paragraphs to blocks, grouping bulleted and numbered+-- paragraphs into lists. Paragraphs at a deeper level than a list item+-- go inside that item, as a nested list or as further paragraphs. paragraphsToBlocks :: [PptxParagraph] -> [Block]-paragraphsToBlocks paras =- -- If we have multiple paragraphs with bullets, group them- let hasBullets = any (\p -> paraBullet p /= NoBullet) paras- in if hasBullets- then groupBulletParagraphs paras- else map (\p -> Para [Str $ paraText p]) paras---- | Group bullet paragraphs into lists-groupBulletParagraphs :: [PptxParagraph] -> [Block]-groupBulletParagraphs paras =- let grouped = groupBy sameBulletLevel paras- in concatMap groupToBlock grouped+paragraphsToBlocks [] = []+paragraphsToBlocks (p:ps)+ | paraBullet p == NoBullet = Para (paraText p) : paragraphsToBlocks ps+ -- PowerPoint shows no bullet or number on an empty paragraph+ | isEmpty p = paragraphsToBlocks ps+ | otherwise =+ let (items, rest) = listItems p (p:ps)+ in listBlock (paraBullet p) items : paragraphsToBlocks rest where- sameBulletLevel p1 p2 =- (paraBullet p1 /= NoBullet) &&- (paraBullet p2 /= NoBullet) &&- (paraLevel p1 == paraLevel p2)+ -- The items of a list that starts with the paragraph first, with the+ -- deeper paragraphs that follow each item+ listItems first (q:qs)+ | isEmpty q, paraBullet q /= NoBullet = listItems first qs+ | paraLevel q == paraLevel first, sameKind q first =+ let (inner, rest) = span ((> paraLevel first) . paraLevel) qs+ (items, rest') = listItems first rest+ in ((Plain (paraText q) : paragraphsToBlocks inner) : items, rest')+ listItems _ qs = ([], qs)+ sameKind a b = case (paraBullet a, paraBullet b) of+ (Numbered _, Numbered _) -> True+ (Numbered _, _) -> False+ (_, Numbered _) -> False+ (x, y) -> (x == NoBullet) == (y == NoBullet)+ listBlock (Numbered attrs) items = OrderedList attrs items+ listBlock _ items = BulletList items+ isEmpty = all blank . paraText+ blank il = case il of+ Space -> True+ SoftBreak -> True+ LineBreak -> True+ Str t -> T.all isSpace t+ _ -> False - groupToBlock :: [PptxParagraph] -> [Block]- groupToBlock [] = []- groupToBlock ps@(p:_)- | paraBullet p /= NoBullet =- -- Bullet list- let items = map (\para -> [Plain [Str $ paraText para]]) ps- in [BulletList items]- | otherwise =- -- Plain paragraph- map (\para -> Para [Str $ paraText para]) ps+-- | Check if shape is a slide number placeholder+isSlideNumberPlaceholder :: NameSpaces -> Element -> Bool+isSlideNumberPlaceholder ns el =+ maybe False ((== Just "sldNum") . findAttr (unqual "type")) $+ findChildByName ns "p" "nvSpPr" el >>=+ findChildByName ns "p" "nvPr" >>=+ findChildByName ns "p" "ph" -- | Check if shape is title placeholder (also used in Slides module) isTitlePlaceholder :: NameSpaces -> Element -> Bool@@ -328,3 +602,7 @@ Just phType -> phType == "title" || phType == "ctrTitle" Nothing -> False Nothing -> False++-- | The graphic data URI of a chart+chartUri :: Text+chartUri = "http://schemas.openxmlformats.org/drawingml/2006/chart"
@@ -15,16 +15,20 @@ ) where import Codec.Archive.Zip (Archive)+import Control.Applicative ((<|>)) import Data.List (find)-import Data.Maybe (mapMaybe)+import qualified Data.Map as M+import Data.Maybe (fromMaybe, mapMaybe) import qualified Data.Text as T import Data.Text (Text)+import qualified Text.Pandoc.Builder as B import Text.Pandoc.Class.PandocMonad (PandocMonad) import Text.Pandoc.Definition import Text.Pandoc.Options (ReaderOptions) import Text.Pandoc.Readers.OOXML.Shared import Text.Pandoc.Readers.Pptx.Parse import Text.Pandoc.Readers.Pptx.Shapes+import Text.Pandoc.Shared (stringify) import Text.Pandoc.XML.Light -- | Convert Pptx intermediate representation to Pandoc AST@@ -42,7 +46,7 @@ slideToBlocks :: PandocMonad m => Archive -> PptxSlide -> m [Block] slideToBlocks archive slide = do let SlideId n = slideId slide- slideElem = slideElement slide+ slideElem = resolveAlternateContent (slideElement slide) rels = slideRels slide ns = elemToNameSpaces slideElem @@ -54,20 +58,61 @@ headerText = if T.null title then "Slide " <> T.pack (show n) else title- header = Header 2 (slideIdent, [], []) [Str headerText]+ -- A slide hidden from the slide show (show="0") is still read,+ -- with a "hidden" class on its header.+ classes = ["hidden" | findAttr (unqual "show") slideElem == Just "0"]+ header = Header 2 (slideIdent, classes, [])+ (B.toList (B.text headerText)) -- Parse shapes and convert to blocks case findChildByName ns "p" "cSld" slideElem >>= findChildByName ns "p" "spTree" of- Nothing -> return [header]+ Nothing -> return $ header : notesBlocks Just spTree -> do -- Filter out title placeholder shapes before parsing- let allShapeElems = onlyElems $ elContent spTree+ let allShapeElems = ungroupShapes ns (onlyElems $ elContent spTree) nonTitleShapeElems = filter (not . isTitlePlaceholder ns) allShapeElems- shapes = mapMaybe (parseShape ns) nonTitleShapeElems+ inherited = inheritedBullets (slideLayout slide) (slideMaster slide)+ shapes = mapMaybe (\el -> parseShapeWith (inherited el) ns el)+ nonTitleShapeElems shapeBlocks <- concat <$> mapM (shapeToBlocks archive rels) shapes- return $ header : shapeBlocks+ return $ header : shapeBlocks ++ notesBlocks+ where+ notesBlocks = maybe [] notesToBlocks (slideNotes slide) +-- | Convert a notes slide (@p:notes@) to a @notes@ div, the form the pptx+-- writer turns into speaker notes. The notes text is in the shape holding+-- the body placeholder; the slide image and slide number placeholders are+-- ignored. Notes without text yield nothing.+notesToBlocks :: Element -> [Block]+notesToBlocks notesElem =+ case filter isBody shapeElems >>= textBoxBlocks of+ blocks | T.null (T.strip (stringify blocks)) -> []+ | otherwise -> [Div ("", ["notes"], []) blocks]+ where+ ns = elemToNameSpaces notesElem+ shapeElems = maybe [] elChildren $+ findChildByName ns "p" "cSld" notesElem >>=+ findChildByName ns "p" "spTree"+ isBody el =+ case findChildByName ns "p" "nvSpPr" el >>=+ findChildByName ns "p" "nvPr" >>=+ findChildByName ns "p" "ph" of+ Just ph -> findAttr (unqual "type") ph == Just "body"+ Nothing -> False+ textBoxBlocks el = case parseShape ns el of+ Just (PptxTextBox paras) -> paragraphsToBlocks paras+ _ -> []++-- | Replace each group shape (@p:grpSp@, possibly nested) by the shapes+-- it contains, keeping document order.+ungroupShapes :: NameSpaces -> [Element] -> [Element]+ungroupShapes ns = concatMap ungroup+ where+ ungroup el+ | isElem ns "p" "grpSp" el = ungroupShapes ns (elChildren el)+ | otherwise = [el]+ -- | Extract title from title placeholder extractSlideTitle :: NameSpaces -> Element -> Text extractSlideTitle ns slideElem =@@ -81,3 +126,38 @@ in maybe "" extractDrawingMLText titleShape -- isTitlePlaceholder is imported from Shapes module++-- | Replace each markup-compatibility block (@mc:AlternateContent@) by+-- the content of its first @mc:Choice@ whose required namespaces the+-- reader understands, or else by its @mc:Fallback@. PowerPoint and the+-- pptx writer put the shapes holding equations (@a14:m@) in such blocks.+resolveAlternateContent :: Element -> Element+resolveAlternateContent = resolve mempty+ where+ resolve ns el =+ let ns' = elemToNameSpaces el <> ns+ in el{ elContent = concatMap (content ns') (elContent el) }+ content ns (Elem el)+ | isMce "AlternateContent" el =+ let ns' = elemToNameSpaces el <> ns+ branches = elChildren el+ chosen = find (understood ns') branches <|>+ find (isMce "Fallback") branches+ in case chosen of+ Just branch ->+ concatMap (content (elemToNameSpaces branch <> ns'))+ (elContent branch)+ Nothing -> []+ | otherwise = [Elem (resolve ns el)]+ content _ c = [c]+ understood ns branch =+ isMce "Choice" branch &&+ all (\prefix -> M.lookup prefix (elemToNameSpaces branch <> ns)+ `elem` map Just understoodNamespaces)+ (T.words $ fromMaybe "" $ findAttr (unqual "Requires") branch)+ isMce name el =+ qName (elName el) == name &&+ qURI (elName el) ==+ Just "http://schemas.openxmlformats.org/markup-compatibility/2006"+ understoodNamespaces =+ [ "http://schemas.microsoft.com/office/drawing/2010/main" ] -- a14
@@ -22,6 +22,7 @@ import qualified Data.Text as T import qualified Data.Text.Lazy.Encoding as TL import Data.Text (Text)+import qualified Text.Pandoc.Builder as B import Text.Pandoc.Definition import Text.Pandoc.Readers.OOXML.Shared import Text.Pandoc.XML.Light@@ -136,8 +137,7 @@ -- Extract text from dgm:t element (which contains a:p/a:r/a:t) let text = case findChildByName ns "dgm" "t" ptElem of Just tElem ->- -- Recursively get ALL text content from all descendants- getAllText tElem+ drawingMLText tElem Nothing -> "" -- Only return nodes with actual text@@ -200,19 +200,12 @@ nodeToBlocks :: (Text, [Text]) -> [Block] nodeToBlocks (nodeText, childTexts) = if null childTexts- then [Para [Strong [Str nodeText]]]- else [ Para [Strong [Str nodeText]]- , BulletList [[Plain [Str child]] | child <- childTexts]+ then [Para [Strong (inlines nodeText)]]+ else [ Para [Strong (inlines nodeText)]+ , BulletList [[Plain (inlines child)] | child <- childTexts] ]---- | Recursively extract all text from an element and its descendants-getAllText :: Element -> Text-getAllText el =- let textFromContent (Text cdata) = cdData cdata- textFromContent (Elem e) = getAllText e- textFromContent _ = ""- texts = map textFromContent (elContent el)- in T.unwords $ filter (not . T.null) texts+ where+ inlines = B.toList . B.text -- Helper functions maybeToEither :: Text -> Maybe a -> Either Text a
@@ -18,10 +18,11 @@ import Control.Monad (forM_, guard, liftM, mplus, mzero, when, unless, void) import Control.Monad.Except (throwError) import Control.Monad.Identity (Identity (..))-import Data.Char (isHexDigit, isSpace, toUpper, isAlphaNum, generalCategory,+import Data.Char (isDigit, isHexDigit, isSpace, toUpper, isAlphaNum,+ generalCategory, GeneralCategory(OpenPunctuation, InitialQuote, FinalQuote, DashPunctuation, OtherSymbol))-import Data.List (deleteFirstsBy, elemIndex, partition, sort, transpose)+import Data.List (elemIndex, partition, sort, transpose) import qualified Data.Map as M import Data.Maybe (fromMaybe, maybeToList, isJust, isNothing, catMaybes) import Data.Sequence (ViewR (..), viewr)@@ -75,8 +76,35 @@ underlineChars = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" -- treat these as potentially non-text when parsing inline:-specialChars :: [Char]-specialChars = "\\`|*_<>$:/[]{}()-.\"'\8216\8217\8220\8221"+-- (equivalent to the character class "\\`|*_<>$:/[]{}()-.\"'\8216\8217\8220\8221")+isSpecialChar :: Char -> Bool+isSpecialChar c =+ case c of+ '\\' -> True+ '`' -> True+ '|' -> True+ '*' -> True+ '_' -> True+ '<' -> True+ '>' -> True+ '$' -> True+ ':' -> True+ '/' -> True+ '[' -> True+ ']' -> True+ '{' -> True+ '}' -> True+ '(' -> True+ ')' -> True+ '-' -> True+ '.' -> True+ '"' -> True+ '\'' -> True+ '\8216' -> True+ '\8217' -> True+ '\8220' -> True+ '\8221' -> True+ _ -> False -- -- parsing documents@@ -121,8 +149,10 @@ metaFromDefList ds meta = adjustAuthors $ foldr f meta ds where f (k,v) = case v of- [[Plain ils]] -> setMeta (T.toLower (stringify k)) $ MetaInlines ils- _ -> setMeta (T.toLower (stringify k)) $ mconcat $ map fromList v+ [[Plain ils]] -> setMeta (T.toLower (stringifyInlines k)) $+ MetaInlines ils+ _ -> setMeta (T.toLower (stringifyInlines k)) $+ mconcat $ map fromList v adjustAuthors (Meta metamap) = Meta $ M.adjust splitAuthors "author" $ M.mapKeys (\k -> if k == "authors"@@ -163,9 +193,15 @@ let (blocks', meta') = if standalone then titleTransform (blocks, meta) else (blocks, meta)- let reversedNotes = stateNotes state- updateState $ \s -> s { stateNotes = reverse reversedNotes }- doc <- walkM resolveReferences =<<+ let notes = reverse $ stateNotes state+ -- Named notes are never removed, so we can look them up in a Map;+ -- auto-numbered notes are consumed in order of occurrence, so we+ -- keep them in a list (in stateNotes):+ let isAutoNote (r, _) = r == "*" || r == "#"+ let namedNotes = M.fromList $ reverse $ filter (not . isAutoNote) notes+ -- reverse, so that the first occurrence of a label wins+ updateState $ \s -> s { stateNotes = filter isAutoNote notes }+ doc <- walkM (resolveReferences namedNotes) =<< walkM resolveBlockSubstitutions (Pandoc meta' (blocks' ++ refBlock)) reportLogMessages@@ -187,22 +223,27 @@ bls -> return $ Div nullAttr bls resolveBlockSubstitutions x = return x -resolveReferences :: PandocMonad m => Inline -> RSTParser m Inline-resolveReferences = resolveReferences' Set.empty+resolveReferences :: PandocMonad m+ => M.Map Text Text -- ^ named notes+ -> Inline -> RSTParser m Inline+resolveReferences namedNotes = resolveReferences' namedNotes Set.empty -resolveReferences' :: PandocMonad m => Set.Set Key -> Inline -> RSTParser m Inline-resolveReferences' seen x@(Link _ ils (s,_))+resolveReferences' :: PandocMonad m+ => M.Map Text Text -> Set.Set Key -> Inline+ -> RSTParser m Inline+resolveReferences' namedNotes seen x@(Link _ ils (s,_)) | Just ref <- T.stripPrefix "##REF##" s = do let isAnonKey (Key (T.uncons -> Just ('_',_))) = True isAnonKey _ = False state <- getState let keyTable = stateKeys state- let anonKeys = sort $ filter isAnonKey $ M.keys keyTable key <- if ref == "_" -- anonymous key then- case anonKeys of- [] -> mzero -- TODO log?- (k:_) -> return k+ -- anonymous keys are named _0000, _0001, ... so the+ -- next one to use is the smallest key >= "_":+ case M.lookupGE (Key "_") keyTable of+ Just (k, _) | isAnonKey k -> return k+ _ -> mzero -- TODO log? else return $ toKey ref if key `Set.member` seen then do@@ -214,27 +255,31 @@ ((src,tit), attr) <- lookupKey [] key when (isAnonKey key) $ updateState $ \st -> st{ stateKeys = M.delete key keyTable }- resolveReferences' (Set.insert key seen) (Link attr ils (src, tit))+ resolveReferences' namedNotes (Set.insert key seen)+ (Link attr ils (src, tit)) | Just ref <- T.stripPrefix "##NOTE##" s = do state <- getState- let notes = stateNotes state- case lookup ref notes of+ let autoNotes = stateNotes state+ let mbnote = if ref == "*" || ref == "#" -- auto-numbered+ -- consume the note, so the next auto-numbered+ -- note doesn't get the same contents:+ then case break ((== ref) . fst) autoNotes of+ (xs, (_, raw) : ys) -> Just (raw, xs ++ ys)+ _ -> Nothing+ else (\raw -> (raw, autoNotes)) <$>+ M.lookup ref namedNotes+ case mbnote of Nothing -> do pos <- getPosition logMessage $ ReferenceNotFound ref pos return x- Just raw -> do+ Just (raw, newnotes) -> do -- We temporarily empty the note list while parsing the note, -- so that we don't get infinite loops with notes inside notes... -- Note references inside other notes are allowed in reST, but -- not yet in this implementation. updateState $ \st -> st{ stateNotes = [] } contents <- parseFromString' parseBlocks raw- let newnotes = if ref == "*" || ref == "#" -- auto-numbered- -- delete the note so the next auto-numbered note- -- doesn't get the same contents:- then deleteFirstsBy (==) notes [(ref,raw)]- else notes updateState $ \st -> st{ stateNotes = newnotes } return $ Note (B.toList contents) | Just ref <- T.stripPrefix "##SUBST##" s = do@@ -255,9 +300,9 @@ [Para [t]] -> return t [Para xs] -> return $ Span nullAttr xs bls -> return $ Span nullAttr $ blocksToInlines bls- resolveReferences' (Set.insert key seen) resolved+ resolveReferences' namedNotes (Set.insert key seen) resolved | otherwise = return x-resolveReferences' _ x = return x+resolveReferences' _ _ x = return x parseCitation :: PandocMonad m => (Text, Text) -> RSTParser m (Inlines, [Blocks])@@ -304,7 +349,8 @@ indent <- length <$> many (char ' ') guard $ indent >= minIndent char ':'- name <- many1TillChar (noneOf "\n") (char ':')+ name <- takeWhile1P (\c -> c /= '\n' && c /= ':')+ char ':' (void (lookAhead newline)) <|> skipMany1 spaceChar first <- anyLine rest <- option "" $ try $ do lookAhead (count indent (char ' ') >> spaceChar)@@ -347,6 +393,7 @@ bodyElements <- do raw <- option "" indentedBlock parseFromString' parseBlocks $ (rawfirst <> raw) <> "\n\n"+ guard (not (null bodyElements)) optional blanklines pure (B.code opts, [bodyElements]) @@ -433,7 +480,7 @@ Nothing -> (headerTable ++ [DoubleHeader c], length headerTable + 1) setState (state { stateHeaderTable = headerTable' }) attr@(ident,_,_) <- registerHeader nullAttr txt- let key = toKey (stringify txt)+ let key = toKey (stringifyInlines txt) updateState $ \s -> s { stateKeys = M.insert key (("#" <> ident,""), nullAttr) $ stateKeys s } return $ B.headerWith attr level txt@@ -465,7 +512,7 @@ Nothing -> (headerTable ++ [SingleHeader c], length headerTable + 1) setState (state { stateHeaderTable = headerTable' }) attr@(ident,_,_) <- registerHeader nullAttr txt- let key = toKey (stringify txt)+ let key = toKey (stringifyInlines txt) updateState $ \s -> s { stateKeys = M.insert key (("#" <> ident,""), nullAttr) $ stateKeys s } return $ B.headerWith attr level txt@@ -473,7 +520,14 @@ singleHeader' :: PandocMonad m => RSTParser m (Inlines, Char) singleHeader' = try $ do notFollowedBy' whitespace- lookAhead $ anyLine >> oneOf underlineChars+ -- check that the next line is a full underline before committing+ -- to parsing the header text (otherwise we'd parse the first line+ -- of many paragraphs twice):+ lookAhead $ do+ anyLine+ c <- oneOf underlineChars+ skipMany (char c)+ blankline txt <- trimInlines . mconcat <$> many1 (notFollowedBy blankline >> inline) pos <- getPosition let len = sourceColumn pos - 1@@ -501,25 +555,22 @@ -- code blocks -- --- read a line indented by a given string-indentedLine :: (HasReaderOptions st, Monad m)- => Int -> ParsecT Sources st m Text-indentedLine indents = try $ do- lookAhead spaceChar- gobbleAtMostSpaces indents- anyLine- -- one or more indented lines, possibly separated by blank lines.--- any amount of indentation will work.+-- any amount of indentation will work; the minimum indentation+-- is stripped from all lines (#12071). indentedBlock :: (HasReaderOptions st, Monad m) => ParsecT Sources st m Text indentedBlock = try $ do- indents <- length <$> lookAhead (many1 spaceChar)+ lookAhead spaceChar lns <- many1 $ try $ do b <- option "" blanklines- l <- indentedLine indents- return (b <> l)+ lookAhead spaceChar+ n <- gobbleAtMostSpaces maxBound+ l <- anyLine+ return (b, n, l) optional blanklines- return $ T.unlines lns+ let minIndent = minimum [n | (_, n, _) <- lns]+ return $ T.unlines+ [b <> T.replicate (n - minIndent) " " <> l | (b, n, l) <- lns] quotedBlock :: Monad m => ParsecT Sources st m Text quotedBlock = try $ do@@ -1044,22 +1095,15 @@ _ -> replicate numOfCols ColWidthDefault let toRow = Row nullAttr . map B.simpleCell toHeaderRow l = [toRow l | not (null l)]- return $ B.table (B.simpleCaption $ B.plain title)+ return $ compactifyTable+ $ B.table (B.simpleCaption $ B.plain title) (zip (replicate numOfCols AlignDefault) widths) (TableHead nullAttr $ toHeaderRow headerRow) [TableBody nullAttr 0 [] $ map toRow bodyRows] (TableFoot nullAttr []) -singleParaToPlain :: Blocks -> Blocks-singleParaToPlain bs =- case B.toList bs of- [Para ils] -> B.fromList [Plain ils]- _ -> bs- parseCell :: PandocMonad m => Text -> RSTParser m Blocks-parseCell t = singleParaToPlain- <$> parseFromString' parseBlocks (trim t <> "\n\n")-+parseCell t = parseFromString' parseBlocks (trim t <> "\n\n") -- TODO: -- - Only supports :format: fields with a single format for :raw: roles,@@ -1176,8 +1220,8 @@ then stripTrailingNewlines else id attribs = (ident, classes', kvs)- classes' = lang- : ["numberLines" | isJust (lookup "number-lines" fields)]+ classes' = [ lang | not (T.null lang) ]+ ++ ["numberLines" | isJust (lookup "number-lines" fields)] ++ classes kvs = [(k,v) | (k,v) <- fields, k /= "number-lines", k /= "class", k /= "id", k /= "name"]@@ -1235,7 +1279,7 @@ noteMarker :: Monad m => RSTParser m Text noteMarker = do char '['- res <- many1Char digit+ res <- takeWhile1P isDigit <|> try (char '#' >> liftM ("#" <>) simpleReferenceName) <|> countChar 1 (oneOf "#*")@@ -1275,9 +1319,9 @@ targetURI = do skipSpaces optional $ try $ newline >> notFollowedBy blankline- contents <- trim <$>- many1Char (satisfy (/='\n')- <|> try (newline >> many1 spaceChar >> noneOf " \t\n"))+ contents <- trim . mconcat <$>+ many1 (takeWhile1P (/='\n')+ <|> T.singleton <$> try (newline >> many1 spaceChar >> noneOf " \t\n")) blanklines return $ stripBackticks contents where@@ -1514,7 +1558,8 @@ gridTableWith (Identity <$> parseBlocks) table :: PandocMonad m => RSTParser m Blocks-table = gridTable <|> simpleTable False <|> simpleTable True <?> "table"+table = compactifyTable <$>+ (gridTable <|> simpleTable False <|> simpleTable True <?> "table") -- -- inline@@ -1523,6 +1568,7 @@ inline :: PandocMonad m => RSTParser m Inlines inline = (note -- can start with whitespace, so try before ws+ <|> quotedSymbol <|> do notAfterString >>= guard (link <|> inlineAnchor <|> strong <|> emph) <|> code@@ -1552,17 +1598,35 @@ unless (canPrecedeOpener c) updateLastStrPos return $ B.str $ T.singleton c +-- Characters that may precede an inline markup start-string: the ASCII+-- characters - : / ' " < ( [ { and similar non-ASCII punctuation. canPrecedeOpener :: Char -> Bool canPrecedeOpener c =+ c == ':' || c == '/' || c == '\'' || c == '"' || c == '<' || generalCategory c `elem` [OpenPunctuation, InitialQuote, FinalQuote, DashPunctuation, OtherSymbol] symbol :: Monad m => RSTParser m Inlines symbol = do- c <- oneOf specialChars+ c <- satisfy isSpecialChar unless (canPrecedeOpener c) updateLastStrPos return $ B.str $ T.singleton c +-- A start-string between an opening character and the matching closing+-- character, as in (*) or "*", is not inline markup.+quotedSymbol :: Monad m => RSTParser m Inlines+quotedSymbol = try $ do+ opener <- oneOf "([{<\"'\8216\8220"+ case lookup opener openersClosers of+ Nothing -> mzero+ Just closer ->+ B.text . (\c -> T.pack [opener, c, closer])+ <$> try (oneOf "*`|" <* char closer)+ where+ openersClosers = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>'),+ ('"', '"'), ('\'', '\''),+ ('\8216', '\8217'), ('\8220', '\8221')]+ -- parses inline code, between codeStart and codeEnd code :: Monad m => RSTParser m Inlines code = try $ do@@ -1720,10 +1784,11 @@ str :: Monad m => RSTParser m Inlines str = do- let strChar = noneOf ("\t\n " ++ specialChars)- result <- many1Char strChar+ result <- takeWhile1P isStrChar updateLastStrPos return $ B.str result+ where+ isStrChar c = c /= '\t' && c /= '\n' && c /= ' ' && not (isSpecialChar c) -- an endline character that can be treated as a space, not a structural break endline :: Monad m => RSTParser m Inlines@@ -1741,8 +1806,29 @@ -- link :: PandocMonad m => RSTParser m Inlines-link = choice [explicitLink, referenceLink, autoLink] <?> "link"+link = do+ linkPossible+ choice [explicitLink, referenceLink, autoLink] <?> "link" +-- Fail fast if no link parser can succeed here: each of them+-- requires one of the characters @`[_:\@@ before the next+-- whitespace ('`' for explicit links and quoted reference names,+-- '[' for citation names, '_' for reference links, ':' after the+-- scheme of a URI, '@' in an email address). Checking this on the+-- raw input is much cheaper than running each link parser over the+-- next word only to have it fail.+linkPossible :: Monad m => RSTParser m ()+linkPossible = do+ Sources inps <- getInput+ case inps of+ [] -> mzero+ (_, t) : rest -> do+ let (w, t') = T.break (\c -> c == ' ' || c == '\t' || c == '\n') t+ guard $ (T.null t' && not (null rest))+ -- word may continue in next chunk: inconclusive+ || T.any (\c -> c == '`' || c == '[' || c == '_' ||+ c == ':' || c == '@') w+ explicitLink :: PandocMonad m => RSTParser m Inlines explicitLink = try $ do char '`'@@ -1763,7 +1849,7 @@ let label'' = if label' == mempty then B.str src else label'- let key = toKey $ stringify label'+ let key = toKey $ stringifyInlines label' unless (key == Key mempty) $ do updateState $ \s -> s{ stateKeys = M.insert key ((src',""), nullAttr) $ stateKeys s }
@@ -34,7 +34,7 @@ import Text.Pandoc.Parsing import Text.Pandoc.Logging (LogMessage(UnsupportedCodePage)) import Text.Pandoc.Shared (tshow)-import Data.Char (isAlphaNum, chr, isAscii, isLetter, isSpace, ord)+import Data.Char (chr, isAscii, isLetter, isSpace, ord) import qualified Data.ByteString.Lazy as BL import Data.Maybe (mapMaybe, fromMaybe) import Safe (lastMay, initSafe, headDef)@@ -82,6 +82,12 @@ -- paragraph numbering is in -- effect; every paragraph in -- scope is its own list item+ , sPendingSurrogate :: Maybe Word16+ -- A high surrogate from a \u+ -- control word, held back+ -- until we know whether the+ -- low surrogate completing it+ -- follows } deriving (Show) instance Default RTFState where@@ -99,6 +105,7 @@ , sEatChars = 0 , sListText = False , sPnActive = False+ , sPendingSurrogate = Nothing } type FontTable = IntMap.IntMap FontFamily@@ -254,7 +261,8 @@ <|> (ControlSymbol <$> anyChar) ) controlWord = do name <- letterSequence- param <- parameter <* optional delimChar+ param <- parameter+ optional (char ' ') case name of "bin" -> do let n = fromMaybe 0 param@@ -286,11 +294,11 @@ x <- hexDigit y <- hexDigit return $ hexToWord (T.pack [x,y])- letterSequence = T.pack <$> many1 (satisfy (\c -> isAscii c && isLetter c))+ letterSequence = takeWhile1P (\c -> isAscii c && isLetter c) unformattedText = do- ts <- filter (\c -> c /= '\r' && c /= '\n') <$>- ( many1 (satisfy (\c -> not (isSpecial c) || c == '\r' || c == '\n')))- return $! UnformattedText $! T.pack ts+ ts <- T.filter (\c -> c /= '\r' && c /= '\n') <$>+ takeWhile1P (\c -> not (isSpecial c) || c == '\r' || c == '\n')+ return $! UnformattedText ts grouped = do char '{' skipMany nl@@ -312,9 +320,6 @@ isSpecial '\n' = True isSpecial _ = False -delimChar :: PandocMonad m => RTFParser m Char-delimChar = satisfy (\c -> not (isAlphaNum c || isSpecial c))- modifyGroup :: PandocMonad m => (Properties -> Properties) -> RTFParser m ()@@ -385,12 +390,46 @@ addText :: PandocMonad m => Text -> RTFParser m () addText t = do+ -- A high surrogate held back by 'addUnicodeCodeUnit' can only be completed+ -- by the next \u control word, so any text in between means it was+ -- unpaired. Empty text does not count: consuming a \u's fallback+ -- characters ends in adding the empty remainder of them, and that happens+ -- between the two halves of a surrogate pair.+ unless (T.null t) $+ updateState $ \s -> s{ sPendingSurrogate = Nothing } gs <- sGroupStack <$> getState let !props = case gs of (x:_) -> x _ -> def updateState (\s -> s{ sTextContent = (props, t) : sTextContent s }) +-- | Add the character denoted by the argument of a @\\u@ control word, which+-- is a single UTF-16 code unit. A character outside the Basic Multilingual+-- Plane is written as a surrogate pair, that is, as two consecutive @\\u@+-- words, each followed by its own fallback characters, so a high surrogate is+-- held back until we know whether the low surrogate completing it follows.+-- An unpaired surrogate denotes no character and cannot be represented in+-- 'Text', so it is dropped.+addUnicodeCodeUnit :: PandocMonad m => Word16 -> RTFParser m ()+addUnicodeCodeUnit unit = do+ mbHigh <- sPendingSurrogate <$> getState+ updateState $ \s -> s{ sPendingSurrogate = Nothing }+ case mbHigh of+ Just high | isLowSurrogate unit -> -- a complete pair+ addText $ T.singleton $ chr $+ 0x10000 + (fromIntegral high - 0xD800) * 0x400+ + (fromIntegral unit - 0xDC00)+ _ | isHighSurrogate unit -> -- wait for the low surrogate+ updateState $ \s -> s{ sPendingSurrogate = Just unit }+ | isLowSurrogate unit -> pure () -- unpaired, so dropped+ | otherwise -> addText $ T.singleton $ chr $ fromIntegral unit++isHighSurrogate :: Word16 -> Bool+isHighSurrogate unit = unit >= 0xD800 && unit <= 0xDBFF++isLowSurrogate :: Word16 -> Bool+isLowSurrogate unit = unit >= 0xDC00 && unit <= 0xDFFF+ inGroup :: PandocMonad m => RTFParser m a -> RTFParser m a inGroup p = do updateState $ \st ->@@ -660,9 +699,7 @@ -- "RTF control words generally accept signed 16-bit numbers as -- arguments. For this reason, Unicode values greater than 32767 -- must be expressed as negative numbers."- let codepoint :: Word16- codepoint = fromIntegral i- addText (T.singleton (chr $ fromIntegral codepoint))+ addUnicodeCodeUnit (fromIntegral i) ControlWord "caps" mbp -> bs <$ modifyGroup (\g -> g{ gCaps = boolParam mbp }) ControlWord "deleted" mbp -> bs <$
@@ -34,7 +34,7 @@ import Control.Monad.Except (throwError) import Text.Pandoc.Class.PandocMonad (getResourcePath, readFileFromDirs, PandocMonad(..), report)-import Data.Char (isLower, toLower, toUpper, isAlphaNum)+import Data.Char (isLetter, isLower, toLower, toUpper, isAlphaNum) import Data.Default (Default) import qualified Data.Map as M import Data.List (intercalate)@@ -202,7 +202,7 @@ guard $ sourceColumn pos == 1 || afterConditional st char '.' <|> char '\'' skipMany spacetab- macroName <- manyChar (satisfy isAlphaNum)+ macroName <- takeWhileP isAlphaNum case macroName of "nop" -> return mempty "ie" -> lexConditional "ie"@@ -286,7 +286,7 @@ tableOption :: PandocMonad m => RoffLexer m TableOption tableOption = do- k <- many1Char letter+ k <- takeWhile1P isLetter v <- option "" $ try $ do skipMany spacetab char '('@@ -366,7 +366,7 @@ expression :: PandocMonad m => RoffLexer m (Maybe Bool) expression = do raw <- charsInBalanced '(' ')' (T.singleton <$> (satisfy (/= '\n')))- <|> many1Char nonspaceChar+ <|> takeWhile1P (\c -> c /= ' ' && c /= '\t' && c /= '\n' && c /= '\r') returnValue $ case raw of "1" -> Just True@@ -530,7 +530,8 @@ regularText :: PandocMonad m => RoffLexer m [LinePart] regularText = do- s <- many1Char $ noneOf "\n\r\t \\\""+ s <- takeWhile1P (\c -> not (c == '\n' || c == '\r' || c == '\t' ||+ c == ' ' || c == '\\' || c == '"')) return [RoffStr s] quoteChar :: PandocMonad m => RoffLexer m [LinePart]
@@ -13,7 +13,7 @@ ( PandocMonad(..), report, PandocMonad(..), report ) import Control.Monad ( mzero, mplus, mzero, mplus )-import Data.Char (chr, isAscii, isAlphaNum)+import Data.Char (chr, isAscii, isAlphaNum, isDigit) import qualified Data.Map as M import qualified Data.Text as T import Text.Pandoc.Logging (LogMessage(..))@@ -123,7 +123,7 @@ case c of ' ' -> return $ emit " " -- mandoc_char(7) says this should be a nonbreaking space '"' -> mempty <$ skipMany (satisfy (/='\n')) -- line comment- '#' -> mempty <$ manyTill anyChar newline+ '#' -> mempty <$ (skipMany (satisfy (/='\n')) <* newline) '%' -> return mempty -- optional hyphenation '&' -> return mempty -- nonprintable zero-width ')' -> return mempty -- nonprintable zero-width@@ -205,18 +205,18 @@ signedNumber :: (PandocMonad m, RoffLikeLexer x) => Lexer m x T.Text signedNumber = try $ do sign <- option "" ("-" <$ char '-' <|> "" <$ char '+')- ds <- many1Char digit+ ds <- takeWhile1P isDigit return (sign <> ds) -- Parses: [..] or (.. escapeArg :: (PandocMonad m, RoffLikeLexer x) => Lexer m x T.Text escapeArg = choice [ char '[' *> optional expandString *>- manyTillChar (noneOf ['\n',']']) (char ']')+ takeWhileP (\c -> c /= '\n' && c /= ']') <* char ']' , char '(' *> optional expandString *> countChar 2 (satisfy (/='\n')) ] -- Parses: '..' quoteArg :: (PandocMonad m, RoffLikeLexer x) => Lexer m x T.Text-quoteArg = char '\'' *> manyTillChar (noneOf ['\n','\'']) (char '\'')+quoteArg = char '\'' *> takeWhile1P (\c -> c /= '\n' && c /= '\'') <* char '\''
@@ -28,7 +28,7 @@ import Text.Pandoc.Options import Text.Pandoc.Parsing hiding (enclosed) import Text.Pandoc.Readers.HTML (htmlTag, isCommentTag)-import Text.Pandoc.Shared (tshow)+import Text.Pandoc.Shared (tshow, compactifyTable) import Text.Pandoc.XML (fromEntities) -- | Read twiki from an input string and return a Pandoc document.@@ -217,7 +217,8 @@ return $ buildTable mempty rows $ fromMaybe (align rows, columns rows) thead where buildTable caption rows (aligns, heads)- = B.table (B.simpleCaption $ B.plain caption)+ = compactifyTable $ B.table+ (B.simpleCaption $ B.plain caption) aligns (TableHead nullAttr $ toHeaderRow heads) [TableBody nullAttr 0 [] $ map toRow rows]@@ -418,7 +419,8 @@ => TWParser m a -> TWParser m Text nestedString end = innerSpace <|> countChar 1 nonspaceChar where- innerSpace = try $ many1Char spaceChar <* notFollowedBy end+ innerSpace = try $ takeWhile1P (\c -> c == ' ' || c == '\t')+ <* notFollowedBy end boldCode :: PandocMonad m => TWParser m B.Inlines boldCode = try $ B.strong . B.code . fromEntities <$> enclosed (string "==") nestedString@@ -449,14 +451,15 @@ | otherwise = isAlphaNum c str :: PandocMonad m => TWParser m B.Inlines-str = B.str <$> (many1Char alphaNum <|> characterReference)+str = B.str <$> (takeWhile1P isAlphaNum <|> characterReference) nop :: PandocMonad m => TWParser m B.Inlines nop = try $ (void exclamation <|> void nopTag) >> followContent where exclamation = char '!' nopTag = stringAnyCase "<nop>"- followContent = B.str . fromEntities <$> many1Char nonspaceChar+ followContent = B.str . fromEntities <$>+ takeWhile1P (\c -> c /= ' ' && c /= '\t' && c /= '\n' && c /= '\r') symbol :: PandocMonad m => TWParser m B.Inlines symbol = B.str <$> countChar 1 nonspaceChar
@@ -56,7 +56,7 @@ import Text.Pandoc.Parsing import Text.Pandoc.Readers.HTML (htmlTag, isBlockTag, isInlineTag) import Text.Pandoc.Readers.LaTeX (rawLaTeXBlock, rawLaTeXInline)-import Text.Pandoc.Shared (trim, tshow)+import Text.Pandoc.Shared (trim, tshow, compactifyTable) import Text.Read (readMaybe) -- | Parse a Textile text and return a Pandoc document.@@ -452,7 +452,8 @@ transpose $ map (map (snd . fst)) (headers:rows) let toRow = Row nullAttr . map B.simpleCell toHeaderRow l = [toRow l | not (null l)]- return $ B.table (B.simpleCaption $ B.plain caption)+ return $ compactifyTable+ $ B.table (B.simpleCaption $ B.plain caption) (zip aligns (replicate nbOfCols ColWidthDefault)) (TableHead nullAttr $ toHeaderRow $ map snd headers) [TableBody nullAttr 0 [] $ map (toRow . map snd) rows]@@ -592,6 +593,76 @@ wordBoundaries :: [Char] wordBoundaries = markupChars <> stringBreakers +-- | True for the characters in 'wordBoundaries'. Written as a @case@ so+-- that GHC compiles it to a jump table; 'wordChunk' applies this to every+-- single character of the input (at three separate sites), so a linear+-- scan over the 39-element list here is a significant cost. Must be kept+-- in sync with 'markupChars' and 'stringBreakers'.+isWordBoundary :: Char -> Bool+isWordBoundary c = case c of+ '\t' -> True+ '\n' -> True+ '\r' -> True+ ' ' -> True+ '!' -> True+ '"' -> True+ '#' -> True+ '%' -> True+ '&' -> True+ '\'' -> True+ '(' -> True+ ')' -> True+ '*' -> True+ '+' -> True+ ',' -> True+ '-' -> True+ '.' -> True+ ':' -> True+ ';' -> True+ '<' -> True+ '=' -> True+ '>' -> True+ '?' -> True+ '@' -> True+ '[' -> True+ '\\' -> True+ ']' -> True+ '^' -> True+ '_' -> True+ '|' -> True+ '~' -> True+ '\171' -> True+ '\187' -> True+ '\8216' -> True+ '\8217' -> True+ '\8218' -> True+ '\8220' -> True+ '\8221' -> True+ '\8222' -> True+ _ -> False++-- | True for the characters in 'markupChars'. As with 'isWordBoundary',+-- written as a @case@ to get a jump table. Must be kept in sync with+-- 'markupChars'.+isMarkupChar :: Char -> Bool+isMarkupChar c = case c of+ '#' -> True+ '%' -> True+ '&' -> True+ '*' -> True+ '+' -> True+ '-' -> True+ '=' -> True+ '@' -> True+ '[' -> True+ '\\' -> True+ ']' -> True+ '^' -> True+ '_' -> True+ '|' -> True+ '~' -> True+ _ -> False+ -- | Parse a hyphened sequence of words hyphenedWords :: PandocMonad m => TextileParser m Text hyphenedWords = do@@ -601,10 +672,10 @@ wordChunk :: PandocMonad m => TextileParser m Text wordChunk = try $ do- hd <- noneOf wordBoundaries- tl <- many ( noneOf wordBoundaries <|>- try (notFollowedBy' note *> oneOf markupChars- <* lookAhead (noneOf wordBoundaries) ) )+ hd <- satisfy (not . isWordBoundary)+ tl <- many ( satisfy (not . isWordBoundary) <|>+ try (notFollowedBy' note *> satisfy isMarkupChar+ <* lookAhead (satisfy (not . isWordBoundary)) ) ) return $ T.pack $ hd:tl -- | Any string@@ -679,7 +750,7 @@ let attr = case lookup "style" kvs of Just stls -> (ident, cls, pickStylesToKVs ["width", "height"] stls) Nothing -> (ident, cls, kvs)- src <- T.pack <$> many1 (noneOf " \t\n\r!(")+ src <- takeWhile1P (`notElem` (" \t\n\r!(" :: [Char])) alt <- fmap T.pack $ option "" $ try $ char '(' *> manyTill anyChar (char ')') char '!' let img = B.imageWith attr src alt (B.str alt)
@@ -614,7 +614,7 @@ wikiLinkText :: PandocMonad m => Text -> Text -> Text -> TikiWikiParser m (Text, Text, Text) wikiLinkText start middle end = do string (T.unpack start)- url <- T.pack <$> many1 (noneOf $ T.unpack middle ++ "\n")+ url <- takeWhile1P (`notElem` (T.unpack middle ++ "\n")) seg1 <- option url linkContent seg2 <- option "" linkContent string (T.unpack end)@@ -626,7 +626,7 @@ where linkContent = do char '|'- T.pack <$> many (noneOf $ T.unpack middle)+ takeWhileP (`notElem` T.unpack middle) externalLink :: PandocMonad m => TikiWikiParser m B.Inlines externalLink = makeLink "[" "]|" "]"
@@ -18,6 +18,7 @@ import Control.Monad (guard, void, when) import Control.Monad.Except (catchError, throwError) import Control.Monad.Reader (Reader, asks, runReader)+import Data.Char (isAlphaNum) import Data.Default import Data.List (intercalate, transpose) import Data.List.NonEmpty (nonEmpty)@@ -33,7 +34,7 @@ import Text.Pandoc.Definition import Text.Pandoc.Options import Text.Pandoc.Parsing hiding (space, spaces, uri)-import Text.Pandoc.Shared (compactify, compactifyDL)+import Text.Pandoc.Shared (compactify, compactifyDL, compactifyTable) import Text.Pandoc.URI (escapeURI) type T2T = ParsecT Sources ParserState (Reader T2TMeta)@@ -132,7 +133,7 @@ setting :: T2T (Keyword, Value) setting = do string "%!"- keyword <- ignoreSpacesCap (many1Char alphaNum)+ keyword <- ignoreSpacesCap (takeWhile1P isAlphaNum) char ':' value <- ignoreSpacesCap (manyTillChar anyChar newline) return (keyword, value)@@ -274,7 +275,8 @@ let headerPadded = if null tableHeader then mempty else pad size tableHeader let toRow = Row nullAttr . map B.simpleCell toHeaderRow l = [toRow l | not (null l)]- return $ B.table B.emptyCaption+ return $ compactifyTable+ $ B.table B.emptyCaption (zip aligns (replicate ncolumns ColWidthDefault)) (TableHead nullAttr $ toHeaderRow headerPadded) [TableBody nullAttr 0 [] $ map toRow rowsPadded]@@ -415,7 +417,7 @@ -> (Text -> a) -- Special Case to handle ****** -> T2T Inlines inlineMarkup p f c special = try $ do- start <- many1Char (char c)+ start <- takeWhile1P (== c) let l = T.length start guard (l >= 2) when (l == 2) (void $ notFollowedBy space)@@ -426,7 +428,7 @@ case body of Just middle -> do lastChar <- anyChar- end <- many1Char (char c)+ end <- takeWhile1P (== c) let parser inp = parseFromString' (mconcat <$> many p) inp let start' = case T.drop 2 start of "" -> mempty@@ -518,7 +520,7 @@ t2tURI = do start <- try ((<>) <$> proto <*> urlLogin) <|> guess domain <- many1Char chars- sep <- manyChar (char '/')+ sep <- takeWhileP (== '/') form' <- option mempty (T.cons <$> char '?' <*> many1Char form) anchor' <- option mempty (T.cons <$> char '#' <*> manyChar anchor) return (start <> domain <> sep <> form' <> anchor')@@ -528,7 +530,7 @@ guess = (<>) <$> (((<>) <$> stringAnyCase "www" <*> option mempty (T.singleton <$> oneOf "23")) <|> stringAnyCase "ftp") <*> (T.singleton <$> char '.') login = alphaNum <|> oneOf "_.-"- pass = manyChar (noneOf " @")+ pass = takeWhileP (\x -> x /= ' ' && x /= '@') chars = alphaNum <|> oneOf "%._/~:,=$@&+-" anchor = alphaNum <|> oneOf "%._0" form = chars <|> oneOf ";*"@@ -548,6 +550,27 @@ specialChars :: [Char] specialChars = "%*-_/|:+;" +-- | True for characters that terminate a run of plain text, i.e. the+-- 'specialChars' plus newlines and spaces. Written as a @case@ so that+-- GHC compiles it to a jump table; this is applied to every single+-- character of the input, so a linear scan over the list here is a+-- significant cost. Must be kept in sync with 'specialChars'.+isStrEnd :: Char -> Bool+isStrEnd c = case c of+ '\n' -> True+ '\r' -> True+ ' ' -> True+ '%' -> True+ '*' -> True+ '+' -> True+ '-' -> True+ '/' -> True+ ':' -> True+ ';' -> True+ '_' -> True+ '|' -> True+ _ -> False+ tab :: T2T Char tab = char '\t' @@ -572,7 +595,7 @@ return B.softbreak str :: T2T Inlines-str = try $ B.str <$> many1Char (noneOf $ specialChars ++ "\n\r ")+str = try $ B.str <$> takeWhile1P (not . isStrEnd) whitespace :: T2T Inlines whitespace = try $ B.space <$ spaceChar
@@ -1,6 +1,7 @@ {-# LANGUAGE RankNTypes #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE LambdaCase #-}@@ -30,14 +31,14 @@ import Typst ( parseTypst, evaluateTypst ) import Text.Pandoc.Error (PandocError(..)) import Text.Pandoc.Translations (Term(References), translateTerm)-import Text.Pandoc.Shared (tshow, blocksToInlines)+import Text.Pandoc.Shared (tshow, blocksToInlines, compactifyTable) import Text.Pandoc.Parsing (registerHeader, reportLogMessages) import Control.Monad.Except (throwError) import Control.Monad (MonadPlus (mplus), void, guard, foldM) import Control.Monad.Trans (lift) import qualified Data.Foldable as F import qualified Data.Map as M-import Data.Maybe (catMaybes, fromMaybe, isJust)+import Data.Maybe (catMaybes, fromMaybe, isJust, listToMaybe) import Data.Sequence (Seq) import qualified Data.Sequence as Seq import qualified Data.Set as Set@@ -96,7 +97,7 @@ ignored ("unknown block element " <> tname <> " at " <> tshow pos) pure mempty- Just handler -> handler pos mbident fields+ Just (BlockHandler handler) -> handler pos mbident fields _ -> pure mempty pInline :: PandocMonad m => P m B.Inlines@@ -110,54 +111,130 @@ , tname /= "math.equation" -> B.math . writeTeX <$> pMathMany (Seq.singleton res) Elt name@(Identifier tname) pos fields -> do- labs <- sLabels <$> getState- labelTarget <- (do result <- getField "target" fields- case result of- VLabel t | t `elem` labs -> pure True- _ -> pure False)- <|> pure False- if tname == "ref" && not labelTarget- then do- -- @foo is a citation unless it links to a lab in the doc:- let targetToKey (Identifier "target") = Identifier "key"- targetToKey k = k- case M.lookup "cite" inlineHandlers of- Nothing -> do- ignored ("unknown inline element " <> tname <>- " at " <> tshow pos)- pure mempty- Just handler -> handler pos Nothing (M.mapKeys targetToKey fields)+ let runInlineHandler =+ case M.lookup name inlineHandlers of+ Nothing -> do+ ignored ("unknown inline element " <> tname <>+ " at " <> tshow pos)+ pure mempty+ Just (InlineHandler handler) -> handler pos Nothing fields+ if tname /= "ref"+ then runInlineHandler else do- case M.lookup name inlineHandlers of- Nothing -> do- ignored ("unknown inline element " <> tname <>- " at " <> tshow pos)- pure mempty- Just handler -> handler pos Nothing fields+ labs <- sLabels <$> getState+ labelTarget <- (do result <- getField "target" fields+ case result of+ VLabel t | t `Set.member` labs -> pure True+ _ -> pure False)+ <|> pure False+ if labelTarget+ then runInlineHandler+ else do+ -- @foo is a citation unless it links to a lab in the doc:+ let targetToKey (Identifier "target") = Identifier "key"+ targetToKey k = k+ case M.lookup "cite" inlineHandlers of+ Nothing -> do+ ignored ("unknown inline element " <> tname <>+ " at " <> tshow pos)+ pure mempty+ Just (InlineHandler handler) ->+ handler pos Nothing (M.mapKeys targetToKey fields) --- Pull block elements out of inline elements, e.g.--- Elt "smallcaps" [ Elt "heading" [..] ] ->--- Elt "heading" [ Elt "smallcaps" [..]]. See #11017.+-- Ensure that inline elements contain only inline content: split+-- them at paragraph breaks, and pull block children out, applying the+-- element to a child's own contents (#11017, #11881). A pandoc inline+-- cannot span paragraphs, so this is the closest structural rendering;+-- e.g. Elt "emph" [Txt "hi", parbreak, Txt "there"] becomes+-- Elt "emph" [Txt "hi"], parbreak, Elt "emph" [Txt "there"]. fixNesting :: Content -> Content-fixNesting el@(Elt name pos fields)- | Just (VContent elts) <- M.lookup "body" fields- = let elts' = fmap fixNesting elts- fields' = M.insert "body" (VContent elts') fields- in if isBlock el- then Elt name pos fields'- else case getField "body" fields' of- Just ([el'@(Elt name' pos' fields'')] :: Seq Content)- | isBlock el'- , not (isInline el')- , "body" `M.member` fields''- -> Elt name' pos' $- M.insert "body" (VContent- (Seq.singleton- (Elt name pos fields'')))- fields'- _ -> Elt name pos fields'+fixNesting el@(Elt name _ _)+ | Identifier tname <- name+ , "math." `T.isPrefixOf` tname = el -- math has its own grammar+fixNesting (Elt name pos fields) = Elt name pos (M.map fixVal fields) fixNesting x = x +fixVal :: Val -> Val+fixVal (VContent cs) = VContent (fixSeq cs)+fixVal (VArray vs) = VArray (fmap fixVal vs)+fixVal (VTermItem t d) = VTermItem (fixSeq t) (fixSeq d)+fixVal v = v++fixSeq :: Seq Content -> Seq Content+fixSeq = foldMap expand . fmap fixNesting++-- Split an inline element whose body contains block content.+expand :: Content -> Seq Content+expand el@(Elt name pos fields)+ | isSplittable name+ , Just (field, VContent body) <- contentField fields+ , F.any isStrictlyBlock body+ = splitInlineBody name pos fields field body+ | otherwise = Seq.singleton el+expand x = Seq.singleton x++-- Whether the element's body is parsed with pInlines, and thus cannot+-- contain block content: anything without a block handler, except+-- footnote (body parsed as blocks), the block-body table elements, and+-- math elements.+isSplittable :: Identifier -> Bool+isSplittable name@(Identifier tname) =+ name `Set.notMember` blockKeys+ && name `Set.notMember` blockBodyElements+ && not ("math." `T.isPrefixOf` tname)++-- Elements without block handlers whose content is nonetheless parsed+-- as block content.+blockBodyElements :: Set.Set Identifier+blockBodyElements = Set.fromList+ [ "footnote", "grid.cell", "table.cell", "grid.header"+ , "table.header", "grid.footer", "table.footer" ]++-- Strictly block content, not consumable by 'pInline'.+isStrictlyBlock :: Content -> Bool+isStrictlyBlock c = isBlock c && not (isInline c)++-- The element's content field. Only body and text: other content+-- fields, such as ref's supplement, are parameters rather than bodies.+contentField :: M.Map Identifier Val -> Maybe (Identifier, Val)+contentField fields =+ listToMaybe+ [ kv | kv@(k, VContent _) <- M.toAscList fields+ , k == Identifier "body" || k == Identifier "text" ]++-- Split the element's body at block content: inline runs are wrapped+-- back in the element, a parbreak separates paragraphs, and a block+-- child gets the element applied to its own body, if it has one.+splitInlineBody+ :: Identifier -> Maybe SourcePos -> M.Map Identifier Val+ -> Identifier -> Seq Content -> Seq Content+splitInlineBody name pos fields field =+ Seq.fromList . go [] . F.toList+ where+ wrap cs = Elt name pos (M.insert field (VContent (Seq.fromList cs)) fields)++ go run [] = flush run+ go run (c : cs)+ | isStrictlyBlock c = flush run ++ splitOff c ++ go [] cs+ | otherwise = go (c : run) cs++ flush run = [ wrap (reverse run) | not (null run) ]++ splitOff c+ | isParbreak c = [Elt "parbreak" pos mempty]+ | otherwise = case c of+ Elt bname bpos bfields+ | Just (VContent inner) <- M.lookup (Identifier "body") bfields ->+ [ Elt bname bpos+ ( M.insert (Identifier "body")+ (VContent (expand (wrap (F.toList inner))))+ bfields ) ]+ _ -> [c]++isParbreak :: Content -> Bool+isParbreak (Elt "parbreak" _ _) = True+isParbreak _ = False+ pPandoc :: PandocMonad m => P m B.Pandoc pPandoc = do Elt "document" _ fields <- pTok isDocument@@ -231,28 +308,35 @@ isInline Txt{} = True blockKeys :: Set.Set Identifier-blockKeys = Set.fromList $ M.keys- (blockHandlers :: M.Map Identifier- (Maybe SourcePos -> Maybe Text ->- M.Map Identifier Val -> P PandocPure B.Blocks))+blockKeys = Set.fromList $ M.keys blockHandlers inlineKeys :: Set.Set Identifier-inlineKeys = Set.fromList $ M.keys- (inlineHandlers :: M.Map Identifier- (Maybe SourcePos -> Maybe Text ->- M.Map Identifier Val -> P PandocPure B.Inlines))+inlineKeys = Set.fromList $ M.keys inlineHandlers -blockHandlers :: PandocMonad m =>- M.Map Identifier- (Maybe SourcePos -> Maybe Text ->- M.Map Identifier Val -> P m B.Blocks)+-- The handler maps are wrapped in newtypes with polymorphic fields so+-- that the maps themselves are monomorphic. This guarantees that they+-- are constant applicative forms, constructed only once; a+-- @PandocMonad m => M.Map ...@ would be a function taking a typeclass+-- dictionary, liable to be rebuilt at each lookup.++newtype BlockHandler = BlockHandler+ (forall m. PandocMonad m+ => Maybe SourcePos -> Maybe Text -> M.Map Identifier Val+ -> P m B.Blocks)++newtype InlineHandler = InlineHandler+ (forall m. PandocMonad m+ => Maybe SourcePos -> Maybe Text -> M.Map Identifier Val+ -> P m B.Inlines)++blockHandlers :: M.Map Identifier BlockHandler blockHandlers = M.fromList- [("text", \_ _ fields -> do+ [("text", BlockHandler $ \_ _ fields -> do body <- getField "body" fields -- sometimes text elements include para breaks notFollowedBy $ void $ pWithContents pInlines body pWithContents pBlocks body)- ,("title", \_ _ fields -> do+ ,("title", BlockHandler $ \_ _ fields -> do body <- getField "body" fields case body of VContent cs -> do@@ -260,16 +344,16 @@ updateState $ \s -> s{ sMeta = B.setMeta "title" ils (sMeta s) } pure mempty _ -> pure mempty)- ,("box", \_ _ fields -> do+ ,("box", BlockHandler $ \_ _ fields -> do body <- getField "body" fields B.divWith ("", ["box"], []) <$> pWithContents pBlocks body)- ,("heading", \_ mbident fields -> do+ ,("heading", BlockHandler $ \_ mbident fields -> do body <- getField "body" fields lev <- getField "level" fields <|> pure 1 ils <- pWithContents pInlines body attr <- registerHeader (fromMaybe "" mbident,[],[]) ils pure $ B.headerWith attr lev ils)- ,("quote", \_ _ fields -> do+ ,("quote", BlockHandler $ \_ _ fields -> do getField "block" fields >>= guard body <- getField "body" fields >>= pWithContents pBlocks attribution' <- getField "attribution" fields@@ -278,11 +362,11 @@ else (\x -> B.para ("\x2014\xa0" <> x)) <$> (pWithContents pInlines attribution') pure $ B.blockQuote $ body <> attribution)- ,("list", \_ _ fields -> do+ ,("list", BlockHandler $ \_ _ fields -> do children <- V.toList <$> getField "children" fields B.bulletList <$> mapM (pWithContents pBlocks) children)- ,("list.item", \_ _ fields -> getField "body" fields >>= pWithContents pBlocks)- ,("enum", \_ _ fields -> do+ ,("list.item", BlockHandler $ \_ _ fields -> getField "body" fields >>= pWithContents pBlocks)+ ,("enum", BlockHandler $ \_ _ fields -> do children <- V.toList <$> getField "children" fields mbstart <- getField "start" fields start <- case mbstart of@@ -311,8 +395,8 @@ _ -> (B.DefaultStyle, B.DefaultDelim) let listAttr = (start, sty, delim) B.orderedListWith listAttr <$> mapM (pWithContents pBlocks) children)- ,("enum.item", \_ _ fields -> getField "body" fields >>= pWithContents pBlocks)- ,("terms", \_ _ fields -> do+ ,("enum.item", BlockHandler $ \_ _ fields -> getField "body" fields >>= pWithContents pBlocks)+ ,("terms", BlockHandler $ \_ _ fields -> do children <- V.toList <$> getField "children" fields B.definitionList <$> mapM@@ -324,38 +408,41 @@ _ -> pure (mempty, []) ) children)- ,("terms.item", \_ _ fields -> getField "body" fields >>= pWithContents pBlocks)- ,("raw", \_ mbident fields -> do+ ,("terms.item", BlockHandler $ \_ _ fields -> getField "body" fields >>= pWithContents pBlocks)+ ,("raw", BlockHandler $ \_ mbident fields -> do txt <- T.filter (/= '\r') <$> getField "text" fields mblang <- getField "lang" fields let attr = (fromMaybe "" mbident, maybe [] (\l -> [l]) mblang, []) pure $ B.codeBlockWith attr txt)- ,("parbreak", \_ _ _ -> pure mempty)- ,("block", \_ mbident fields ->+ ,("parbreak", BlockHandler $ \_ _ _ -> pure mempty)+ ,("par", BlockHandler $ \_ mbident fields -> do+ maybe B.para (\ident -> B.divWith (ident, [], []) . B.para) mbident+ <$> (getField "body" fields >>= pWithContents pInlines))+ ,("block", BlockHandler $ \_ mbident fields -> maybe id (\ident -> B.divWith (ident, [], [])) mbident <$> (getField "body" fields >>= pWithContents pBlocks))- ,("place", \_ _ fields -> do+ ,("place", BlockHandler $ \_ _ fields -> do ignored "parameters of place" getField "body" fields >>= pWithContents pBlocks)- ,("columns", \_ _ fields -> do+ ,("columns", BlockHandler $ \_ _ fields -> do (cnt :: Integer) <- getField "count" fields B.divWith ("", ["columns-flow"], [("count", T.pack (show cnt))]) <$> (getField "body" fields >>= pWithContents pBlocks))- ,("rect", \_ _ fields ->+ ,("rect", BlockHandler $ \_ _ fields -> B.divWith ("", ["rect"], []) <$> (getField "body" fields >>= pWithContents pBlocks))- ,("circle", \_ _ fields ->+ ,("circle", BlockHandler $ \_ _ fields -> B.divWith ("", ["circle"], []) <$> (getField "body" fields >>= pWithContents pBlocks))- ,("ellipse", \_ _ fields ->+ ,("ellipse", BlockHandler $ \_ _ fields -> B.divWith ("", ["ellipse"], []) <$> (getField "body" fields >>= pWithContents pBlocks))- ,("polygon", \_ _ fields ->+ ,("polygon", BlockHandler $ \_ _ fields -> B.divWith ("", ["polygon"], []) <$> (getField "body" fields >>= pWithContents pBlocks))- ,("square", \_ _ fields ->+ ,("square", BlockHandler $ \_ _ fields -> B.divWith ("", ["square"], []) <$> (getField "body" fields >>= pWithContents pBlocks))- ,("align", \_ _ fields -> do+ ,("align", BlockHandler $ \_ _ fields -> do alignment <- getField "alignment" fields B.divWith ("", [], [("align", repr alignment)]) <$> (getField "body" fields >>= pWithContents pBlocks))- ,("stack", \_ _ fields -> do+ ,("stack", BlockHandler $ \_ _ fields -> do (dir :: Direction) <- getField "dir" fields `mplus` pure Ltr rawchildren <- getField "children" fields children <-@@ -370,9 +457,9 @@ B.divWith ("", [], [("stack", repr (VDirection dir))]) $ mconcat $ map (B.divWith ("", [], [])) children)- ,("grid", \_ mbident fields -> parseTable mbident fields)- ,("table", \_ mbident fields -> parseTable mbident fields)- ,("figure", \_ mbident fields -> do+ ,("grid", BlockHandler $ \_ mbident fields -> parseTable mbident fields)+ ,("table", BlockHandler $ \_ mbident fields -> parseTable mbident fields)+ ,("figure", BlockHandler $ \_ mbident fields -> do body <- getField "body" fields >>= pWithContents pBlocks (mbCaption :: Maybe (Seq Content)) <- getField "caption" fields (caption :: B.Blocks) <- maybe mempty (pWithContents pBlocks) mbCaption@@ -382,14 +469,14 @@ (B.Table attr (B.Caption Nothing (B.toList caption)) colspecs thead tbodies tfoot) _ -> B.figureWith (fromMaybe "" mbident, [], []) (B.Caption Nothing (B.toList caption)) body)- ,("line", \_ _ fields ->+ ,("line", BlockHandler $ \_ _ fields -> case ( M.lookup "start" fields >> M.lookup "end" fields >> M.lookup "angle" fields ) of Nothing -> pure B.horizontalRule _ -> pure mempty)- ,("divider", \_ _ _fields -> pure B.horizontalRule)- ,("numbering", \_ _ fields -> do+ ,("divider", BlockHandler $ \_ _ _fields -> pure B.horizontalRule)+ ,("numbering", BlockHandler $ \_ _ fields -> do numStyle <- getField "numbering" fields (nums :: V.Vector Integer) <- getField "numbers" fields let toText v = fromMaybe "" $ fromVal v@@ -402,12 +489,12 @@ Failure _ -> "?" _ -> "?" pure $ B.plain . B.text . mconcat . map toNum $ V.toList nums)- ,("footnote.entry", \_ _ fields ->+ ,("footnote.entry", BlockHandler $ \_ _ fields -> getField "body" fields >>= pWithContents pBlocks)- ,("pad", \_ _ fields -> -- ignore paddingy+ ,("pad", BlockHandler $ \_ _ fields -> -- ignore paddingy getField "body" fields >>= pWithContents pBlocks)- ,("pagebreak", \_ _ _ -> pure $ B.divWith ("", ["page-break"], [("wrapper", "1")]) B.horizontalRule)- ,("bibliography", \_ _ fields -> do+ ,("pagebreak", BlockHandler $ \_ _ _ -> pure $ B.divWith ("", ["page-break"], [("wrapper", "1")]) B.horizontalRule)+ ,("bibliography", BlockHandler $ \_ _ fields -> do let getSources v = case v of VString t -> MetaString t VArray xs -> MetaList $ map getSources $ V.toList xs@@ -426,7 +513,7 @@ _ -> Just . B.text <$> lift (translateTerm References) let hdr = maybe mempty (B.header 1) mbTitle pure $ hdr <> B.divWith ("refs", [], []) mempty)- ,("rotate", \_ _ fields -> do+ ,("rotate", BlockHandler $ \_ _ fields -> do body <- getField "body" fields >>= pWithContents pBlocks let kvs = case M.lookup "angle" fields of Just (VAngle ang) -> [("angle", T.pack $ show ang)]@@ -434,11 +521,9 @@ pure $ B.divWith ("", ["rotate"], kvs) body) ] -inlineHandlers :: PandocMonad m =>- M.Map Identifier (Maybe SourcePos -> Maybe Text ->- M.Map Identifier Val -> P m B.Inlines)+inlineHandlers :: M.Map Identifier InlineHandler inlineHandlers = M.fromList- [("ref", \_ _ fields -> do+ [("ref", InlineHandler $ \_ _ fields -> do VLabel target <- getField "target" fields supplement' <- getField "supplement" fields supplement <- case supplement' of@@ -449,17 +534,17 @@ pure $ B.text ("[" <> target <> "]") _ -> pure mempty pure $ B.linkWith ("", ["ref"], []) ("#" <> target) "" supplement)- ,("linebreak", \_ _ _ -> pure B.linebreak)- ,("text", \_ _ fields -> do+ ,("linebreak", InlineHandler $ \_ _ _ -> pure B.linebreak)+ ,("text", InlineHandler $ \_ _ fields -> do body <- getField "body" fields (mbweight :: Maybe Text) <- getField "weight" fields case mbweight of Just "bold" -> B.strong <$> pWithContents pInlines body _ -> pWithContents pInlines body)- ,("raw", \_ _ fields -> B.code . T.filter (/= '\r') <$> getField "text" fields)- ,("footnote", \_ _ fields ->+ ,("raw", InlineHandler $ \_ _ fields -> B.code . T.filter (/= '\r') <$> getField "text" fields)+ ,("footnote", InlineHandler $ \_ _ fields -> B.note <$> (getField "body" fields >>= pWithContents pBlocks))- ,("cite", \_ _ fields -> do+ ,("cite", InlineHandler $ \_ _ fields -> do VLabel key <- getField "key" fields (form :: Text) <- getField "form" fields <|> pure "normal" let citation =@@ -468,44 +553,50 @@ B.citationPrefix = mempty, B.citationSuffix = mempty, B.citationMode = case form of+ "prose" -> B.AuthorInText "year" -> B.SuppressAuthor+ -- "author" and "full" have no pandoc+ -- equivalent; fall back to normal _ -> B.NormalCitation, B.citationNoteNum = 0, B.citationHash = 0 } pure $ B.cite [citation] (B.text $ "[" <> key <> "]"))- ,("lower", \_ _ fields -> do+ ,("lower", InlineHandler $ \_ _ fields -> do body <- getField "text" fields walk (modString T.toLower) <$> pWithContents pInlines body)- ,("upper", \_ _ fields -> do+ ,("upper", InlineHandler $ \_ _ fields -> do body <- getField "text" fields walk (modString T.toUpper) <$> pWithContents pInlines body)- ,("emph", \_ _ fields -> do+ ,("emph", InlineHandler $ \_ _ fields -> do body <- getField "body" fields B.emph <$> pWithContents pInlines body)- ,("strong", \_ _ fields -> do+ ,("strong", InlineHandler $ \_ _ fields -> do body <- getField "body" fields B.strong <$> pWithContents pInlines body)- ,("sub", \_ _ fields -> do+ ,("sub", InlineHandler $ \_ _ fields -> do body <- getField "body" fields B.subscript <$> pWithContents pInlines body)- ,("super", \_ _ fields -> do+ ,("super", InlineHandler $ \_ _ fields -> do body <- getField "body" fields B.superscript <$> pWithContents pInlines body)- ,("strike", \_ _ fields -> do+ ,("strike", InlineHandler $ \_ _ fields -> do body <- getField "body" fields B.strikeout <$> pWithContents pInlines body)- ,("smallcaps", \_ _ fields -> do+ ,("smallcaps", InlineHandler $ \_ _ fields -> do body <- getField "body" fields B.smallcaps <$> pWithContents pInlines body)- ,("underline", \_ _ fields -> do+ ,("underline", InlineHandler $ \_ _ fields -> do body <- getField "body" fields B.underline <$> pWithContents pInlines body)- ,("quote", \_ _ fields -> do+ ,("highlight", InlineHandler $ \_ _ fields -> do+ body <- getField "body" fields+ B.spanWith ("", ["mark"], []) <$> pWithContents pInlines body)+ ,("quote", InlineHandler $ \_ _ fields -> do (getField "block" fields <|> pure False) >>= guard . not- body <- getInlineBody fields >>= pWithContents pInlines+ body <- getField "body" fields >>= pWithContents pInlines pure $ B.doubleQuoted $ B.trimInlines body)- ,("link", \_ _ fields -> do+ ,("link", InlineHandler $ \_ _ fields -> do dest <- getField "dest" fields src <- case dest of VString t -> pure t@@ -530,7 +621,7 @@ pWithContents (B.fromList . blocksToInlines . B.toList <$> pBlocks) body pure $ B.link src "" description)- ,("image", \mbpos _ fields -> do+ ,("image", InlineHandler $ \mbpos _ fields -> do path <- getField "source" fields <|> getField "path" fields alt <- (B.text <$> getField "alt" fields) `mplus` pure mempty let basedir = maybe "." (takeDirectory . sourceName) mbpos@@ -550,10 +641,10 @@ ++ maybe [] (\x -> [("height", x)]) mbheight ) pure $ B.imageWith attr path' "" alt)- ,("box", \_ _ fields -> do+ ,("box", InlineHandler $ \_ _ fields -> do body <- getField "body" fields B.spanWith ("", ["box"], []) <$> pWithContents pInlines body)- ,("h", \_ _ fields -> do+ ,("h", InlineHandler $ \_ _ fields -> do amount <- getField "amount" fields `mplus` pure (LExact 1 LEm) let em = case amount of LExact x LEm -> toRational x@@ -561,21 +652,21 @@ LExact x LPt -> toRational x / 12 _ -> 1 / 3 -- guess! pure $ B.text $ getSpaceChars em)- ,("place", \_ _ fields -> do+ ,("place", InlineHandler $ \_ _ fields -> do ignored "parameters of place" getField "body" fields >>= pWithContents pInlines)- ,("align", \_ _ fields -> do+ ,("align", InlineHandler $ \_ _ fields -> do alignment <- getField "alignment" fields B.spanWith ("", [], [("align", repr alignment)]) <$> (getField "body" fields >>= pWithContents pInlines))- ,("sys.version", \_ _ _ -> pure $ B.text "typst-hs")- ,("math.equation", \_ _ fields -> do+ ,("sys.version", InlineHandler $ \_ _ _ -> pure $ B.text "typst-hs")+ ,("math.equation", InlineHandler $ \_ _ fields -> do body <- getField "body" fields display <- getField "block" fields (if display then B.displayMath else B.math) . writeTeX <$> pMathMany body)- ,("pad", \_ _ fields -> -- ignore paddingy+ ,("pad", InlineHandler $ \_ _ fields -> -- ignore paddingy getField "body" fields >>= pWithContents pInlines)- ,("rotate", \_ _ fields -> do+ ,("rotate", InlineHandler $ \_ _ fields -> do body <- getField "body" fields >>= pWithContents pInlines let kvs = case M.lookup "angle" fields of Just (VAngle ang) -> [("angle", T.pack $ show ang)]@@ -583,19 +674,6 @@ pure $ B.spanWith ("", ["rotate"], kvs) body) ] -getInlineBody :: PandocMonad m => M.Map Identifier Val -> P m (Seq Content)-getInlineBody fields =- parbreaksToLinebreaks <$> getField "body" fields--parbreaksToLinebreaks :: Seq Content -> Seq Content-parbreaksToLinebreaks =- fmap go . Seq.dropWhileL isParbreak . Seq.dropWhileR isParbreak- where- go (Elt "parbreak" pos _) = Elt "linebreak" pos mempty- go x = x- isParbreak (Elt "parbreak" _ _) = True- isParbreak _ = False- pPara :: PandocMonad m => P m B.Blocks pPara = do ils <- B.trimInlines . collapseAdjacentCites . mconcat <$> many1 pInline@@ -634,20 +712,22 @@ Cite (cs1 ++ cs2) (ils1 <> ils2) : xs go (Cite cs1 ils1) (Space : Cite cs2 ils2 : xs) = Cite (cs1 ++ cs2) (ils1 <> ils2) : xs+ go (Cite cs1 ils1) (SoftBreak : Cite cs2 ils2 : xs) =+ Cite (cs1 ++ cs2) (ils1 <> ils2) : xs go x xs = x:xs modString :: (Text -> Text) -> B.Inline -> B.Inline modString f (B.Str t) = B.Str (f t) modString _ x = x -findLabels :: Seq.Seq Content -> [Text]-findLabels = foldr go []+findLabels :: Seq.Seq Content -> Set.Set Text+findLabels = F.foldl' go Set.empty where- go (Txt{}) = id- go (Lab t) = (t :)- go (Elt{ eltFields = fs }) = \ts -> foldr go' ts fs- go' (VContent cs) = (findLabels cs ++)- go' _ = id+ go acc Txt{} = acc+ go acc (Lab t) = Set.insert t acc+ go acc (Elt{ eltFields = fs }) = F.foldl' go' acc fs+ go' acc (VContent cs) = F.foldl' go acc cs+ go' acc _ = acc parseTable :: PandocMonad m => Maybe Text -> M.Map Identifier Val -> P m B.Blocks@@ -657,26 +737,22 @@ let toWidth (VFraction f) = Just (floor $ 1000 * f) toWidth _ = Nothing let normalizeWidths xs =+ -- keep column widths all-or-nothing for the writers: a+ -- column the source leaves to the layout engine makes the+ -- whole table content-sized let givenwidths = catMaybes xs- (totgivenwidth :: Int) = sum givenwidths- avgwidth = totgivenwidth `div` length givenwidths- totwidth = avgwidth * length xs- in if null givenwidths- then replicate (length xs) B.ColWidthDefault- else+ totwidth = sum givenwidths :: Int+ in if all isJust xs && totwidth > 0+ then map- ( \case- Just x ->- B.ColWidth (fromIntegral x / fromIntegral totwidth)- Nothing ->- B.ColWidth- (fromIntegral avgwidth / fromIntegral totwidth)- )- xs+ (\x -> B.ColWidth (fromIntegral x / fromIntegral totwidth))+ givenwidths+ else replicate (length xs) B.ColWidthDefault widths <- case columns of VInteger x -> pure $ replicate (fromIntegral x) B.ColWidthDefault VArray x -> pure $ normalizeWidths $ map toWidth (V.toList x) VNone -> pure [B.ColWidthDefault]+ VAuto -> pure [B.ColWidthDefault] _ -> fail $ "Could not determine number of columns: " <> show columns let numcols = length widths align <- getField "align" fields@@ -760,14 +836,13 @@ let headRows = getRows THeader tableData let bodyRows = getRows TBody tableData let footRows = getRows TFooter tableData- pure $- B.tableWith- (fromMaybe "" mbident, [], [])- (B.Caption mempty mempty)- colspecs- (B.TableHead B.nullAttr headRows)- [B.TableBody B.nullAttr 0 [] bodyRows]- (B.TableFoot B.nullAttr footRows)+ pure $ compactifyTable $ B.tableWith+ (fromMaybe "" mbident, [], [])+ (B.Caption mempty mempty)+ colspecs+ (B.TableHead B.nullAttr headRows)+ [B.TableBody B.nullAttr 0 [] bodyRows]+ (B.TableFoot B.nullAttr footRows) data TableSection = THeader | TBody | TFooter deriving (Show, Ord, Eq)
@@ -36,7 +36,7 @@ import Text.Pandoc.Definition data PState = PState- { sLabels :: [Text]+ { sLabels :: Set Text , sMeta :: Meta , sOptions :: ReaderOptions , sIdentifiers :: Set Text@@ -57,7 +57,7 @@ defaultPState :: PState defaultPState = PState- { sLabels = []+ { sLabels = Set.empty , sMeta = mempty , sOptions = def , sIdentifiers = Set.empty
@@ -1,5 +1,4 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-} {- | Module : Text.Pandoc.Readers.Vimwiki Copyright : Copyright (C) 2017-2020 Yuchen Pei@@ -66,15 +65,17 @@ import Text.Pandoc.Parsing (ParserState, ParsecT, blanklines, emailAddress, many1Till, orderedListMarker, readWithM, registerHeader, spaceChar, stateMeta,- stateOptions, uri, manyTillChar, manyChar, textStr,- many1Char, countChar, many1TillChar,+ stateOptions, uri, manyTillChar, textStr,+ countChar, many1TillChar,+ takeWhileP, takeWhile1P, alphaNum, anyChar, char, newline, noneOf, oneOf, space, spaces, string, choice, eof, lookAhead, many1, many, manyTill, notFollowedBy, skipMany1, try, option, updateState, getState, (<|>)) import Text.Pandoc.Sources (ToSources(..), Sources)-import Text.Pandoc.Shared (splitTextBy, stringify, stripFirstAndLast, tshow)+import Text.Pandoc.Shared (splitTextBy, stringifyInlines, stripFirstAndLast,+ tshow) import Text.Pandoc.URI (isURI) readVimwiki :: (PandocMonad m, ToSources a)@@ -99,6 +100,35 @@ spaceChars :: [Char] spaceChars = " \t\n" +-- | True for characters that terminate a run of plain text, i.e. the+-- 'spaceChars' plus the 'specialChars'. Written as a @case@ so that GHC+-- compiles it to a jump table; this is applied to every single character+-- of the input, so a linear scan over the list here is a significant+-- cost. Must be kept in sync with 'spaceChars' and 'specialChars'.+isStrEnd :: Char -> Bool+isStrEnd c = case c of+ '\t' -> True+ '\n' -> True+ ' ' -> True+ '#' -> True+ '$' -> True+ '%' -> True+ '*' -> True+ ',' -> True+ '-' -> True+ ':' -> True+ '=' -> True+ '[' -> True+ ']' -> True+ '^' -> True+ '_' -> True+ '`' -> True+ '{' -> True+ '|' -> True+ '}' -> True+ '~' -> True+ _ -> False+ -- main parser parseVimwiki :: PandocMonad m => VwParser m Pandoc@@ -200,7 +230,7 @@ definitionTerm :: PandocMonad m => VwParser m Inlines definitionTerm = try $ do x <- definitionTerm1 <|> definitionTerm2- guard (stringify x /= "")+ guard (stringifyInlines x /= "") return x definitionTerm1 :: PandocMonad m => VwParser m Inlines@@ -223,7 +253,7 @@ preformatted :: PandocMonad m => VwParser m Blocks preformatted = try $ do many spaceChar >> string "{{{"- attrText <- manyChar (noneOf "\n")+ attrText <- takeWhileP (/= '\n') lookAhead newline contents <- manyTillChar anyChar (try (char '\n' >> many spaceChar >> string "}}}" >> many spaceChar >> newline))@@ -233,8 +263,11 @@ makeAttr :: Text -> Attr makeAttr s =- let xs = splitTextBy (`elem` (" \t" :: String)) s in- ("", syntax xs, mapMaybe nameValue xs)+ let xs = splitTextBy (`elem` (" \t" :: String)) s+ kvs = mapMaybe nameValue xs+ cls = syntax xs ++ maybe [] T.words (lookup "class" kvs)+ ident = fromMaybe "" $ lookup "id" kvs+ in (ident, cls, [(k,v) | (k,v) <- kvs, k /= "class" && k /= "id"]) syntax :: [Text] -> [Text] syntax (s:_) | not $ T.isInfixOf "=" s = [s]@@ -338,7 +371,7 @@ listTodoMarker :: PandocMonad m => VwParser m Inlines listTodoMarker = try $ do x <- (many spaceChar >> char '[')- *> oneOf " .oOX"+ *> oneOf " .oOX-" <* (char ']' >> spaceChar) return $ makeListMarkerSpan x @@ -350,6 +383,7 @@ 'o' -> "done2" 'O' -> "done3" 'X' -> "done4"+ '-' -> "rejected" _ -> "" in B.spanWith ("", [cl], []) mempty@@ -479,7 +513,7 @@ inlineML = choice $ whitespace endlineML:inlineList str :: PandocMonad m => VwParser m Inlines-str = B.str <$> many1Char (noneOf $ spaceChars ++ specialChars)+str = B.str <$> takeWhile1P (not . isStrEnd) whitespace :: PandocMonad m => VwParser m () -> VwParser m Inlines whitespace endline = B.space <$ (skipMany1 spaceChar <|>@@ -507,7 +541,7 @@ return $ B.spanWith (makeId contents, [], []) mempty <> B.strong contents makeId :: Inlines -> Text-makeId i = T.concat (stringify <$> toList i)+makeId i = stringifyInlines i emph :: PandocMonad m => VwParser m Inlines emph = try $ do
@@ -25,6 +25,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Lazy (fromStrict)+import qualified Data.Text.Read as TR import Data.Version (Version, makeVersion) import Text.Pandoc.Builder import Text.Pandoc.Class.PandocMonad@@ -36,7 +37,6 @@ import Text.Pandoc.XML (lookupEntity) import Text.Pandoc.XML.Light import Text.Pandoc.XMLFormat-import Text.Read (readMaybe) -- TODO: use xmlPath state to give better context when an error occurs @@ -45,7 +45,6 @@ data XMLReaderState = XMLReaderState { xmlApiVersion :: Version, xmlMeta :: Meta,- xmlContent :: [Content], xmlPath :: [Text] } deriving (Show)@@ -55,7 +54,6 @@ XMLReaderState { xmlApiVersion = pandocVersion, xmlMeta = mempty,- xmlContent = [], xmlPath = ["root"] } @@ -65,7 +63,7 @@ tree <- either (throwError . PandocXMLError "") return $ parseXMLContents (fromStrict . sourcesToText $ sources)- (bs, st') <- flip runStateT (def {xmlContent = tree}) $ mapM parseBlock tree+ (bs, st') <- flip runStateT def $ mapM parseBlock tree let blockList = toList $ concatMany bs return $ Pandoc (xmlMeta st') blockList @@ -117,7 +115,7 @@ "Header" -> (headerWith attr level) <$> getInlines (elContent e) where level = textToInt (attrValue atNameLevel e) 1- attr = filterAttrAttributes [atNameLevel] $ attrFromElement e+ attr = attrFromElementExcept [atNameLevel] e "HorizontalRule" -> return horizontalRule "BlockQuote" -> do contents <- getBlocks e@@ -192,25 +190,39 @@ _ -> Nothing strContentRecursive :: Element -> Text-strContentRecursive =- strContent- . (\e' -> e' {elContent = map elementToStr $ elContent e'})--elementToStr :: Content -> Content-elementToStr (Elem e') = Text $ CData CDataText (strContentRecursive e') Nothing-elementToStr x = x+strContentRecursive e = case elContent e of+ [Text (CData _ s _)] -> s -- the common case: no nested elements+ cs -> T.concat $ map contentToStr cs+ where+ contentToStr (Text (CData _ s _)) = s+ contentToStr (Elem e') = strContentRecursive e'+ contentToStr (CRef _) = mempty textToInt :: Text -> Int -> Int-textToInt t deflt =- let safe_to_int :: Text -> Maybe Int- safe_to_int s = readMaybe $ T.unpack s- in case (safe_to_int t) of- Nothing -> deflt- Just (n) -> n+textToInt t deflt = case TR.signed TR.decimal (T.strip t) of+ Right (n, rest) | T.null rest -> n+ _ -> deflt +-- | Split text into 'Str', 'Space' and 'SoftBreak' inlines. This is+-- 'Text.Pandoc.Builder.text', but it builds a list instead of a 'Seq'.+textToInlines :: Text -> [Inline]+textToInlines t+ | T.null t = []+ | isSpaceChar (T.head t) =+ case T.span isSpaceChar t of+ (spaces, rest) ->+ (if T.any isNewlineChar spaces then SoftBreak else Space)+ : textToInlines rest+ | otherwise =+ case T.break isSpaceChar t of+ (word, rest) -> Str word : textToInlines rest+ where+ isSpaceChar c = c == ' ' || c == '\n' || c == '\r' || c == '\t'+ isNewlineChar c = c == '\n' || c == '\r'+ parseInline :: (PandocMonad m) => Content -> XMLReader m Inlines parseInline (Text (CData _ s _)) =- return $ text s+ return $ fromList $ textToInlines s parseInline (CRef ref) = return $ maybe (text $ T.toUpper ref) text $@@ -245,12 +257,12 @@ where url = attrValue atNameLinkUrl e title = attrValue atNameTitle e- attr = filterAttrAttributes [atNameLinkUrl, atNameTitle] $ attrFromElement e+ attr = attrFromElementExcept [atNameLinkUrl, atNameTitle] e "Image" -> innerInlines $ imageWith attr url title where url = attrValue atNameImageUrl e title = attrValue atNameTitle e- attr = filterAttrAttributes [atNameImageUrl, atNameTitle] $ attrFromElement e+ attr = attrFromElementExcept [atNameImageUrl, atNameTitle] e "RawInline" -> do let format = (attrValue atNameFormat e) return $ rawInline format $ strContentRecursive e@@ -308,6 +320,8 @@ "AlignCenter" -> AlignCenter _ -> AlignDefault +-- NB. 'reads' is used rather than 'Data.Text.Read.double', which is+-- not correctly rounded and so would not round-trip column widths. getColWidth :: Text -> ColWidth getColWidth txt = case reads (T.unpack txt) of [(value, "")] -> if value == 0.0 then ColWidthDefault else ColWidth value@@ -322,7 +336,7 @@ getTableBody :: (PandocMonad m) => Element -> XMLReader m (Maybe TableBody) getTableBody body_el = do- let attr = filterAttrAttributes [atNameRowHeadColumns] $ attrFromElement body_el+ let attr = attrFromElementExcept [atNameRowHeadColumns] body_el bh = childrenNamed tgNameBodyHeader body_el bb = childrenNamed tgNameBodyBody body_el headcols = textToInt (attrValue atNameRowHeadColumns body_el) 0@@ -355,7 +369,7 @@ let alignment = alignmentFromText $ attrValue atNameAlignment c rowspan = RowSpan $ textToInt (attrValue atNameRowspan c) 1 colspan = ColSpan $ textToInt (attrValue atNameColspan c) 1- attr = filterAttrAttributes [atNameAlignment, atNameRowspan, atNameColspan] $ attrFromElement c+ attr = attrFromElementExcept [atNameAlignment, atNameRowspan, atNameColspan] c blocks <- getBlocks c return $ Cell attr alignment rowspan colspan (toList blocks) @@ -461,22 +475,26 @@ type PandocAttr = (Text, [Text], [(Text, Text)]) -filterAttributes :: S.Set Text -> [(Text, Text)] -> [(Text, Text)]-filterAttributes to_be_removed a = filter keep_attr a- where- keep_attr (k, _) = not (k `S.member` to_be_removed)--filterAttrAttributes :: [Text] -> PandocAttr -> PandocAttr-filterAttrAttributes to_be_removed (idn, classes, a) = (idn, classes, filtered)- where- filtered = filterAttributes (S.fromList to_be_removed) a- attrFromElement :: Element -> PandocAttr-attrFromElement e = filterAttrAttributes ["id", "class"] (idn, classes, attributes)+attrFromElement = attrFromElementExcept []++-- | Build a pandoc 'Attr' from an element's XML attributes, skipping+-- the attributes whose (decoded) names are listed in the first argument+-- in addition to @id@ and @class@, which become the identifier and the+-- classes of the 'Attr'.+attrFromElementExcept :: [Text] -> Element -> PandocAttr+attrFromElementExcept skip e = go (elAttribs e) "" "" [] where- idn = attrValue "id" e- classes = T.words $ attrValue "class" e- attributes = map (\a -> (qName $ attrKey a, attrVal a)) $ elAttribs e+ go [] idn classes kvs = (idn, T.words classes, reverse kvs)+ go (a : as) idn classes kvs =+ case qName (attrKey a) of+ "id" -> go as (attrVal a) classes kvs+ "class" -> go as idn (attrVal a) kvs+ name ->+ let name' = decodeAttrName name+ in if name' `elem` skip+ then go as idn classes kvs+ else go as idn classes ((name', attrVal a) : kvs) addMeta :: (PandocMonad m) => (ToMetaValue a) => Text -> a -> XMLReader m () addMeta field val = modify (setMeta field val)
@@ -32,6 +32,7 @@ data CellValue = TextValue Text | NumberValue Double+ | BoolValue Bool | EmptyValue deriving (Show, Eq)
@@ -22,16 +22,22 @@ ) where import Codec.Archive.Zip (Archive, Entry, findEntryByPath, fromEntry)+import Control.Monad (join)+import Data.Char (toLower) import Data.List (find) import qualified Data.Map.Strict as M import Data.Maybe (mapMaybe, fromMaybe) import qualified Data.Text as T import qualified Data.Text.Lazy.Encoding as TL import Data.Text (Text)+import Data.Time.Calendar (Day, addDays, fromGregorian, showGregorian) import qualified Data.Vector as V import System.FilePath (splitFileName)+import qualified System.FilePath.Posix as Posix+import Text.Pandoc.Readers.OOXML.Charts (Chart, readChart) import Text.Pandoc.Readers.OOXML.Shared import Text.Pandoc.Readers.Xlsx.Cells+import Text.Pandoc.Shared (collapseFilePath) import Text.Pandoc.XML.Light import Text.Read (readMaybe) @@ -51,8 +57,19 @@ -- | Style information data Styles = Styles { styleFonts :: V.Vector FontInfo+ , styleCellFonts :: V.Vector Int+ -- ^ For each cell format (@cellXfs@), the index of its font+ , styleDateFormats :: V.Vector (Maybe DateFormat)+ -- ^ For each cell format (@cellXfs@), what it shows of a date } deriving (Show) +-- | What a date or time number format shows+data DateFormat = DateFormat+ { dateShowsDate :: Bool+ , dateShowsTime :: Bool+ , dateShowsSeconds :: Bool+ } deriving (Show)+ -- | Complete XLSX document data Xlsx = Xlsx { xlsxWorkbook :: XlsxWorkbook@@ -64,6 +81,7 @@ -- | Workbook information data XlsxWorkbook = XlsxWorkbook { workbookSheetNames :: [(SheetId, Text, Text)] -- (id, name, relId)+ , workbookDate1904 :: Bool -- ^ Dates count from 1904 rather than 1900 } deriving (Show) -- | Individual worksheet@@ -71,6 +89,9 @@ { sheetId :: SheetId , sheetName :: Text , sheetCells :: M.Map CellRef XlsxCell+ , sheetMerges :: [(CellRef, CellRef)]+ -- ^ Merged ranges, as their top-left and bottom-right cells+ , sheetCharts :: [Chart] -- ^ In the order they are drawn } deriving (Show) -- | Parse XLSX archive@@ -88,21 +109,20 @@ -- Parse shared strings (look for sharedStrings relationship) sharedStrings <- case findRelWithTarget workbookRels "sharedStrings" of Just (_, target) -> do- let path = "xl/" ++ T.unpack target- el <- loadXMLFromArchive archive path+ el <- loadXMLFromArchive archive (resolveTarget workbookPath target) parseSharedStrings el Nothing -> Right V.empty -- Parse styles styles <- case findRelWithTarget workbookRels "styles" of Just (_, target) -> do- let path = "xl/" ++ T.unpack target- el <- loadXMLFromArchive archive path+ el <- loadXMLFromArchive archive (resolveTarget workbookPath target) parseStyles el- Nothing -> Right $ Styles V.empty+ Nothing -> Right $ Styles V.empty V.empty V.empty -- Parse worksheets- sheets <- mapM (\sheetInfo -> parseSheet archive workbookRels sharedStrings styles sheetInfo)+ sheets <- mapM (parseSheet archive workbookPath workbookRels sharedStrings+ styles (workbookDate1904 workbook)) (workbookSheetNames workbook) return $ Xlsx workbook sheets sharedStrings styles@@ -119,7 +139,8 @@ Nothing -> Left "No workbook.xml relationship found" Just rel -> do target <- maybeToEither "Missing Target" $ findAttr (unqual "Target") rel- return $ T.unpack target+ -- The root relationships belong to the package itself.+ return $ resolveTarget "" target where isOfficeDocRel el = case (findAttr (unqual "Type") el, findAttr (unqual "Target") el) of@@ -139,8 +160,12 @@ let sheetElems = filter (\e -> qName (elName e) == "sheet") (onlyElems $ elContent sheets) sheetRefs <- mapM (parseSheetRef ns) (zip [1..] sheetElems) - return $ XlsxWorkbook sheetRefs+ let date1904 = maybe False (`elem` ["1", "true"]) $+ find (\e -> qName (elName e) == "workbookPr") (elChildren wbElem)+ >>= findAttr (unqual "date1904") + return $ XlsxWorkbook sheetRefs date1904+ parseSheetRef :: NameSpaces -> (Int, Element) -> Either Text (SheetId, Text, Text) parseSheetRef ns (idx, sheetElem) = do let name = fromMaybe ("Sheet" <> T.pack (show idx)) $@@ -153,14 +178,22 @@ parseSharedStrings :: Element -> Either Text SharedStrings parseSharedStrings sstElem = do let siElems = filter (\e -> qName (elName e) == "si") (onlyElems $ elContent sstElem)- strings = map extractString siElems+ strings = map stringItemText siElems return $ V.fromList strings- where- extractString siElem =- case find (\e -> qName (elName e) == "t") (onlyElems $ elContent siElem) of- Just tElem -> strContent tElem- Nothing -> getAllText siElem +-- | Text of a string item (@si@ in the shared strings, @is@ in an inline+-- string cell): its @t@, or the @t@ of its rich-text runs (@r@)+-- concatenated as written. Phonetic runs (@rPh@) are skipped.+stringItemText :: Element -> Text+stringItemText el = T.concat+ [ strContent t+ | child <- elChildren el+ , t <- case qName (elName child) of+ "t" -> [child]+ "r" -> filter (\e -> qName (elName e) == "t") (elChildren child)+ _ -> []+ ]+ -- | Parse styles (fonts only for MVP) parseStyles :: Element -> Either Text Styles parseStyles stylesElem = do@@ -169,8 +202,108 @@ fontElems = maybe [] (\fe -> filter (\e -> qName (elName e) == "font") (onlyElems $ elContent fe)) fontsElem fonts = V.fromList $ map (parseFont mempty) fontElems - return $ Styles fonts+ -- A cell's style index (@s@) refers to a cell format in <cellXfs>,+ -- which refers to its font by @fontId@ and its number format by+ -- @numFmtId@.+ children name el = filter (\e -> qName (elName e) == name) (elChildren el)+ cellFormats =+ [ xf+ | cellXfs <- children "cellXfs" stylesElem+ , xf <- children "xf" cellXfs+ ]+ cellFonts = V.fromList+ [ fromMaybe 0 $ findAttr (unqual "fontId") xf >>= readMaybe . T.unpack+ | xf <- cellFormats+ ] + -- Number formats: custom ones from <numFmts>, by id+ customFormats = M.fromList+ [ (fmtId, code)+ | numFmts <- children "numFmts" stylesElem+ , numFmt <- children "numFmt" numFmts+ , Just fmtId <- [findAttr (unqual "numFmtId") numFmt >>=+ readMaybe . T.unpack]+ , Just code <- [findAttr (unqual "formatCode") numFmt]+ ]+ xfDateFormat xf = do+ fmtId <- findAttr (unqual "numFmtId") xf >>= readMaybe . T.unpack+ maybe (builtinDateFormat fmtId) customDateFormat+ (M.lookup fmtId customFormats)+ dateFormats = V.fromList $ map xfDateFormat cellFormats++ return $ Styles fonts cellFonts dateFormats++-- | The built-in number formats that show dates or times. Format 46,+-- @[h]:mm:ss@, shows a duration rather than a time of day.+builtinDateFormat :: Int -> Maybe DateFormat+builtinDateFormat fmtId+ | fmtId >= 14 && fmtId <= 17 = Just $ DateFormat True False False+ | fmtId == 18 || fmtId == 20 = Just $ DateFormat False True False+ | fmtId == 19 || fmtId == 21 || fmtId == 45 || fmtId == 47+ = Just $ DateFormat False True True+ | fmtId == 22 = Just $ DateFormat True True False+ | otherwise = Nothing++-- | Whether a custom format code shows a date or a time: whether its+-- first section has a date or time token (y, m, d, h, s) outside quoted+-- text, escaped characters and brackets. A bracketed elapsed-time token+-- such as @[h]@ makes it a duration, which isn't treated as a date.+customDateFormat :: Text -> Maybe DateFormat+customDateFormat code+ | elapsed = Nothing+ | showsDate || showsTime = Just $ DateFormat showsDate showsTime hasS+ | otherwise = Nothing+ where+ (tokens, brackets) = scan (T.unpack (T.takeWhile (/= ';') code)) [] []+ has c = c `elem` tokens+ hasS = has 's'+ showsTime = has 'h' || hasS+ showsDate = has 'y' || has 'd' || (has 'm' && not showsTime)+ elapsed = any (\b -> not (null b) && all (`elem` ("hms" :: String)) b)+ brackets+ -- Collect the letters outside quotes, escapes and brackets, and the+ -- contents of brackets.+ scan [] ts bs = (ts, bs)+ scan ('"':cs) ts bs = scan (drop 1 (dropWhile (/= '"') cs)) ts bs+ scan ('\\':_:cs) ts bs = scan cs ts bs+ scan ('_':_:cs) ts bs = scan cs ts bs+ scan ('*':_:cs) ts bs = scan cs ts bs+ scan ('[':cs) ts bs =+ let (inside, rest) = break (== ']') cs+ in scan (drop 1 rest) ts (map toLower inside : bs)+ scan (c:cs) ts bs = scan cs (toLower c : ts) bs++-- | Render a date serial number under a date or time format, as an ISO+-- 8601 date (@2023-11-01@), time (@14:30@ or @14:30:05@), or both joined+-- by a space. Nothing if the number isn't a date the format can show:+-- negative, or before the first day of the date system for a format+-- that shows the date.+formatDate :: Bool -> DateFormat -> Double -> Maybe Text+formatDate date1904 fmt serial+ | serial < 0 = Nothing+ | otherwise = do+ dateText <- if dateShowsDate fmt+ then (\d -> [showGregorian d]) <$> day+ else Just []+ let timeText = [ showTime | dateShowsTime fmt ]+ return $ T.pack $ unwords (dateText ++ timeText)+ where+ seconds = round (serial * 86400) :: Integer+ (days, secs) = seconds `divMod` 86400+ day :: Maybe Day+ day+ | date1904 = Just $ addDays days (fromGregorian 1904 1 1)+ -- 1900 system: day 1 is 1900-01-01, and day 60 is the 29 February+ -- 1900 that Excel counts but that didn't exist.+ | days < 1 || days == 60 = Nothing+ | days < 60 = Just $ addDays days (fromGregorian 1899 12 31)+ | otherwise = Just $ addDays days (fromGregorian 1899 12 30)+ (h, rest) = secs `divMod` 3600+ (m, s) = rest `divMod` 60+ pad n = (if n < 10 then ('0' :) else id) (show n)+ showTime = pad h ++ ":" ++ pad m +++ (if dateShowsSeconds fmt then ":" ++ pad s else "")+ parseFont :: NameSpaces -> Element -> FontInfo parseFont _ns fontElem = FontInfo@@ -180,33 +313,86 @@ } -- | Parse individual worksheet-parseSheet :: Archive -> [(Text, Text)] -> SharedStrings -> Styles -> (SheetId, Text, Text) -> Either Text XlsxSheet-parseSheet archive rels sharedStrings styles (sid, name, relId) = do+parseSheet :: Archive -> FilePath -> [(Text, Text)] -> SharedStrings -> Styles+ -> Bool -> (SheetId, Text, Text) -> Either Text XlsxSheet+parseSheet archive wbPath rels sharedStrings styles date1904+ (sid, name, relId) = do target <- maybeToEither ("Sheet relationship not found: " <> relId) $ lookup relId rels - let sheetPath = "xl/" ++ T.unpack target+ let sheetPath = resolveTarget wbPath target sheetElem <- loadXMLFromArchive archive sheetPath - cells <- parseSheetCells sheetElem sharedStrings styles+ cells <- parseSheetCells sheetElem sharedStrings styles date1904 - return $ XlsxSheet sid name cells+ let merges =+ [ (from, to)+ | mergeCells <- filter (\e -> qName (elName e) == "mergeCells")+ (elChildren sheetElem)+ , mergeCell <- filter (\e -> qName (elName e) == "mergeCell")+ (elChildren mergeCells)+ , Just ref <- [findAttr (unqual "ref") mergeCell]+ , (fromText, toText) <- [T.breakOn ":" ref]+ , Right from <- [parseCellRef fromText]+ , Right to <- [parseCellRef (T.drop 1 toText)]+ ] + return $ XlsxSheet sid name cells merges+ (sheetChartsOf archive sheetPath)++-- | The charts on a worksheet or chart sheet: those its drawing refers+-- to, in document order. A drawing or chart that can't be read is left+-- out.+sheetChartsOf :: Archive -> FilePath -> [Chart]+sheetChartsOf archive sheetPath = fromMaybe [] $ do+ drawingTarget <- findRelTargetByType archive (relsPathFor sheetPath)+ "drawing"+ let drawingPath = resolveTarget sheetPath drawingTarget+ drawing <- either (const Nothing) Just $+ loadXMLFromArchive archive drawingPath+ drawingRels <- either (const Nothing) Just $+ loadRelationships archive (relsPathFor drawingPath)+ return+ [ chart+ | chartElem <- filterElementsName ((== "chart") . qName) drawing+ , Just relId <- [lookupRelId chartElem]+ , Just chartTarget <- [lookup relId drawingRels]+ , Right chart <- [readChart archive+ (resolveTarget drawingPath chartTarget)]+ ]+ where+ lookupRelId el = attrVal <$> find isRelId (elAttribs el)+ isRelId a = qName (attrKey a) == "id" && qURI (attrKey a) == Just+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships"++-- | Find the target of the first relationship in a .rels part whose Type+-- ends with the given name, such as @drawing@+findRelTargetByType :: Archive -> FilePath -> Text -> Maybe Text+findRelTargetByType archive relsPath typeName = do+ entry <- findEntryByPath relsPath archive+ relsElem <- either (const Nothing) Just $ parseXMLFromEntry entry+ rel <- find hasType (onlyElems $ elContent relsElem)+ findAttr (unqual "Target") rel+ where+ hasType el = maybe False (("/" <> typeName) `T.isSuffixOf`) $+ findAttr (unqual "Type") el+ -- | Parse sheet cells-parseSheetCells :: Element -> SharedStrings -> Styles -> Either Text (M.Map CellRef XlsxCell)-parseSheetCells sheetElem sharedStrings styles = do+parseSheetCells :: Element -> SharedStrings -> Styles -> Bool+ -> Either Text (M.Map CellRef XlsxCell)+parseSheetCells sheetElem sharedStrings styles date1904 = do -- Find sheetData by local name case find (\e -> qName (elName e) == "sheetData") (onlyElems $ elContent sheetElem) of Nothing -> return M.empty Just sheetData -> do let rowElems = filter (\e -> qName (elName e) == "row") (onlyElems $ elContent sheetData) cellElems = concatMap (\r -> filter (\e -> qName (elName e) == "c") (onlyElems $ elContent r)) rowElems- cells = mapMaybe (parseCell sharedStrings styles) cellElems+ cells = mapMaybe (parseCell sharedStrings styles date1904) cellElems return $ M.fromList [(cellRef c, c) | c <- cells] -- | Parse individual cell-parseCell :: SharedStrings -> Styles -> Element -> Maybe XlsxCell-parseCell sharedStrings styles cElem = do+parseCell :: SharedStrings -> Styles -> Bool -> Element -> Maybe XlsxCell+parseCell sharedStrings styles date1904 cElem = do -- Get cell reference refText <- findAttr (unqual "r") cElem cellRefParsed <- either (const Nothing) Just $ parseCellRef refText@@ -227,20 +413,32 @@ Just idx | idx >= 0 && idx < V.length sharedStrings -> TextValue (sharedStrings V.! idx) _ -> EmptyValue+ else if cellType == "inlineStr"+ then+ -- Inline string, held in the cell itself rather than in <v>+ maybe EmptyValue (TextValue . stringItemText) $+ find (\e -> qName (elName e) == "is") (elChildren cElem) else if T.null vText then EmptyValue+ else if cellType == "b"+ then BoolValue (vText == "1") else -- Number case readMaybe (T.unpack vText) of- Just n -> NumberValue n+ Just n+ | Just txt <- dateFormat >>= \f -> formatDate date1904 f n+ -> TextValue txt+ | otherwise -> NumberValue n Nothing -> TextValue vText+ dateFormat = join $ styleIdx >>= (styleDateFormats styles V.!?) -- Get formatting from style- let (bold, italic) = case styleIdx of- Just idx | idx >= 0 && idx < V.length (styleFonts styles) ->- let font = styleFonts styles V.! idx- in (fontBold font, fontItalic font)- _ -> (False, False)+ let font = do+ idx <- styleIdx+ fontIdx <- styleCellFonts styles V.!? idx+ styleFonts styles V.!? fontIdx+ (bold, italic) = maybe (False, False)+ (\f -> (fontBold f, fontItalic f)) font return $ XlsxCell cellRefParsed value bold italic @@ -275,6 +473,15 @@ let (dir, file) = splitFileName path in dir ++ "/_rels/" ++ file ++ ".rels" +-- | Resolve a relationship target to an archive path. A target starting+-- with @/@ is relative to the package root; any other target is relative+-- to the folder of the part that holds the relationship.+resolveTarget :: FilePath -> Text -> FilePath+resolveTarget source target =+ case T.unpack target of+ '/':path -> path+ path -> collapseFilePath (Posix.takeDirectory source Posix.</> path)+ findRelWithTarget :: [(Text, Text)] -> Text -> Maybe (Text, Text) findRelWithTarget rels targetName = find (\(_, target) -> targetName `T.isInfixOf` target) rels@@ -282,14 +489,6 @@ maybeToEither :: Text -> Maybe a -> Either Text a maybeToEither err Nothing = Left err maybeToEither _ (Just x) = Right x--getAllText :: Element -> Text-getAllText el =- let textFromContent (Text cdata) = cdData cdata- textFromContent (Elem e) = getAllText e- textFromContent _ = ""- texts = map textFromContent (elContent el)- in T.unwords $ filter (not . T.null) texts addContext :: Either Text a -> Text -> Either Text a addContext (Right x) _ = Right x
@@ -15,11 +15,14 @@ ) where import qualified Data.Map.Strict as M+import qualified Data.Set as S import qualified Data.Text as T import Data.List (sort, dropWhileEnd)+import Numeric (floatToDigits) import Data.Char (isSpace) import Text.Pandoc.Definition import Text.Pandoc.Options (ReaderOptions)+import Text.Pandoc.Readers.OOXML.Charts (chartToBlocks) import Text.Pandoc.Readers.Xlsx.Parse import Text.Pandoc.Readers.Xlsx.Cells import qualified Text.Pandoc.Builder as B@@ -43,7 +46,7 @@ tableBlock = case cellsToTable sheet of Just tbl -> [tbl] Nothing -> [] -- Empty sheet- in header : tableBlock+ in header : tableBlock ++ concatMap chartToBlocks (sheetCharts sheet) -- | Convert cells to Pandoc Table cellsToTable :: XlsxSheet -> Maybe Block@@ -59,7 +62,7 @@ maxRow = maximum $ map cellRefRow refs -- Build dense grid- grid = [ [ M.lookup (CellRef col row) cells+ grid = [ [ (CellRef col row, M.lookup (CellRef col row) cells) | col <- [minCol..maxCol] ] | row <- [minRow..maxRow]@@ -71,19 +74,42 @@ [] -> ([], []) -- Filter out trailing empty rows (rows with only whitespace)- filteredBodyRows = dropWhileEnd isEmptyRow bodyRows+ filteredBodyRows = dropWhileEnd (isEmptyRow . map snd) bodyRows+ lastRow = minRow + length filteredBodyRows - makeCell mcell = case mcell of- Just cell ->- let inlines = cellToInlines cell- in Cell nullAttr AlignDefault (RowSpan 1) (ColSpan 1) [Plain inlines]- Nothing ->- Cell nullAttr AlignDefault (RowSpan 1) (ColSpan 1) [Plain []]+ -- A merged range becomes one cell spanning its rows and+ -- columns, clipped to the table and kept within the header row+ -- or within the body, as a cell can't span both.+ spans = M.fromList+ [ (from, (rows, cols))+ | (from, to) <- sheetMerges sheet+ , cellRefCol from >= minCol, cellRefCol from <= maxCol+ , cellRefRow from >= minRow, cellRefRow from <= lastRow+ , let rows = if cellRefRow from == minRow+ then 1+ else min (cellRefRow to) lastRow+ - cellRefRow from + 1+ cols = min (cellRefCol to) maxCol - cellRefCol from + 1+ , rows > 1 || cols > 1+ ]+ covered = S.fromList+ [ CellRef col row+ | (CellRef c r, (rows, cols)) <- M.toList spans+ , row <- [r .. r + rows - 1], col <- [c .. c + cols - 1]+ , (col, row) /= (c, r)+ ] + makeCell (ref, mcell) =+ let (rows, cols) = M.findWithDefault (1, 1) ref spans+ in Cell nullAttr AlignDefault (RowSpan rows) (ColSpan cols)+ [Plain (maybe [] cellToInlines mcell)]+ makeRow = Row nullAttr . map makeCell .+ filter (\(ref, _) -> not (ref `S.member` covered))+ numCols = length headerRow colSpec = replicate numCols (AlignDefault, ColWidthDefault)- thead = TableHead nullAttr [Row nullAttr $ map makeCell headerRow]- tbody = [TableBody nullAttr 0 [] $ map (Row nullAttr . map makeCell) filteredBodyRows]+ thead = TableHead nullAttr [makeRow headerRow]+ tbody = [TableBody nullAttr 0 [] $ map makeRow filteredBodyRows] tfoot = TableFoot nullAttr [] in Just $ Table nullAttr (Caption Nothing []) colSpec thead tbody tfoot@@ -97,16 +123,62 @@ EmptyValue -> True TextValue t -> T.all isSpace t NumberValue _ -> False+ BoolValue _ -> False -- | Convert cell to Pandoc inlines cellToInlines :: XlsxCell -> [Inline] cellToInlines cell = let base = case cellValue cell of TextValue t -> B.toList $ B.text t- NumberValue n -> [Str $ T.pack $ show n]+ NumberValue n -> [Str $ formatGeneral n]+ BoolValue b -> [Str $ if b then "TRUE" else "FALSE"] EmptyValue -> [] applyBold inls = if cellBold cell then [Strong inls] else inls applyItalic inls = if cellItalic cell then [Emph inls] else inls in applyItalic $ applyBold base++-- | Render a number as Excel's General format does, without rounding+-- it to fit a column: at most 15 significant digits (Excel's precision)+-- and no trailing zeros. Numbers with 12 or more digits before the+-- decimal point use scientific notation (@1.23456789012E+11@), as General+-- does, keeping all their digits; so do numbers below 1e-9.+formatGeneral :: Double -> T.Text+formatGeneral x+ | isNaN x || isInfinite x = T.pack (show x)+ | x == 0 = "0"+ | otherwise = T.pack $ sign ++ body+ where+ sign = if x < 0 then "-" else ""+ (ds, e) = roundDigits 15 (floatToDigits 10 (abs x))+ digits = concatMap show ds+ -- abs x == 0.d1d2d3... * 10^e+ body+ | e > 11 || e < -8 = scientific+ | e <= 0 = "0." ++ replicate (negate e) '0' ++ digits+ | length ds <= e = digits ++ replicate (e - length ds) '0'+ | otherwise = take e digits ++ "." ++ drop e digits+ scientific =+ let mantissa = case digits of+ d:rest@(_:_) -> d : '.' : rest+ _ -> digits+ ex = e - 1+ in mantissa ++ "E" ++ (if ex < 0 then "-" else "+") +++ (let a = show (abs ex) in if length a < 2 then '0' : a else a)++-- | Round a digit list (as from 'floatToDigits') to at most n digits,+-- dropping trailing zeros.+roundDigits :: Int -> ([Int], Int) -> ([Int], Int)+roundDigits n (ds, e) =+ case splitAt n ds of+ (_, []) -> (ds, e)+ (keep, d:_) ->+ case if d >= 5 then carry (reverse keep) else Just keep of+ Just ds' -> (dropWhileEnd (== 0) ds', e)+ Nothing -> ([1], e + 1)+ where+ -- Add one to a reversed digit list; Nothing if it overflows (all 9s).+ carry [] = Nothing+ carry (9:rest) = (++ [0]) <$> carry rest+ carry (k:rest) = Just (reverse (k + 1 : rest))
@@ -16,16 +16,16 @@ the HTML using data URIs. -} module Text.Pandoc.SelfContained ( makeDataURI, makeSelfContained ) where-import Codec.Compression.GZip as Gzip+import qualified Codec.Compression.Zlib.Internal as Zlib import Control.Applicative ((<|>)) import Data.ByteString (ByteString) import Data.ByteString.Base64 (encode) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy as L import qualified Data.Text as T-import Data.Char (isAlphaNum, isAscii)+import Data.Char (toLower) import Crypto.Hash (hashWith, SHA1(SHA1))-import Network.URI (escapeURIString)+import Network.URI (escapeURIString, isUnreserved) import System.FilePath (takeDirectory, takeExtension, (</>)) import Text.HTML.TagSoup import Text.Pandoc.Class.PandocMonad (PandocMonad (..), fetchItem,@@ -44,19 +44,16 @@ import qualified Data.Map as M import Control.Monad.State -isOk :: Char -> Bool-isOk c = isAscii c && isAlphaNum c- makeDataURI :: (MimeType, ByteString) -> T.Text makeDataURI (mime, raw) = if textual- then "data:" <> mime' <> "," <> T.pack (escapeURIString isOk (toString raw))+ then "data:" <> mime' <> "," <> T.pack (escapeURIString isUnreserved (toString raw)) else "data:" <> mime' <> ";base64," <> toText (encode raw') where textual = "text/" `T.isPrefixOf` mime raw' = if "+xml" `T.isSuffixOf` mime then B.filter (/= '\r') raw -- strip off CRs else raw- mime' = if textual && T.any (== ';') mime+ mime' = if textual && not (T.any (== ';') mime) then mime <> ";charset=utf-8" else mime -- mime type already has charset @@ -70,9 +67,10 @@ data ConvertState = ConvertState- { isHtml5 :: Bool- , svgMap :: M.Map T.Text (T.Text, [Attribute T.Text])+ { svgMap :: M.Map T.Text (T.Text, [Attribute T.Text]) -- map from hash to (id, svg attributes)+ , fetchCache :: M.Map (MimeType, T.Text) GetDataResult+ -- cache of fetched resources, keyed on mime type hint and url } deriving (Show) convertTags :: PandocMonad m =>@@ -102,7 +100,7 @@ | ("text/javascript" `T.isPrefixOf` mime || "application/javascript" `T.isPrefixOf` mime || "application/x-javascript" `T.isPrefixOf` mime) &&- not ("</script" `B.isInfixOf` bs) ->+ not ("</script" `B.isInfixOf` B.map toLower bs) -> return $ TagOpen "script" [(k,v) | (k,v) <- as , k == "type" ||@@ -155,12 +153,15 @@ Nothing -> False Just cs -> "inline-svg" `elem` cs as' <- mapM (processAttribute inlineSvgs) as- let attrs = addRole "img" $ addAriaLabel $ rights as'+ let attrs = rights as' let svgContents = lefts as' rest <- convertTags ts case svgContents of [] -> return $ TagOpen tagname attrs : rest ((hash, tags) : _) -> do+ -- inlining the SVG loses the img element's alt text, so+ -- we add role and aria-label to the svg element:+ let svgImgAttrs = addRole "img" $ addAriaLabel attrs -- drop "</img>" if present let rest' = case rest of TagClose tn : xs | tn == tagname -> xs@@ -168,7 +169,8 @@ svgmap <- gets svgMap case M.lookup hash svgmap of Just (svgid, svgattrs) -> do- let attrs' = [(k,v) | (k,v) <- combineSvgAttrs svgattrs attrs+ let attrs' = [(k,v) | (k,v) <- combineSvgAttrs svgattrs+ svgImgAttrs , k /= "id"] return $ TagOpen "svg" attrs' : TagOpen "use" [("href", "#" <> svgid),@@ -180,7 +182,7 @@ Nothing -> case dropWhile (not . isTagOpenName "svg") tags of TagOpen "svg" svgattrs : tags' -> do- let attrs' = combineSvgAttrs svgattrs attrs+ let attrs' = combineSvgAttrs svgattrs svgImgAttrs let svgid = case lookup "id" attrs' of Just id' -> id' Nothing -> "svg_" <> hash@@ -188,11 +190,7 @@ [(k,v) | (k,v) <- attrs', k /= "id"] modify $ \st -> st{ svgMap = M.insert hash (svgid, attrs'') (svgMap st) }- let fixUrl x =- case T.breakOn "url(#" x of- (_,"") -> x- (before, after) -> before <>- "url(#" <> svgid <> "_" <> T.drop 5 after+ let fixUrl = T.replace "url(#" ("url(#" <> svgid <> "_") let addIdPrefix ("id", x) = ("id", svgid <> "_" <> x) addIdPrefix (k, x) | k == "xlink:href" || k == "href" =@@ -290,7 +288,7 @@ _ -> [] cssURLs :: PandocMonad m- => FilePath -> ByteString -> m ByteString+ => FilePath -> ByteString -> StateT ConvertState m ByteString cssURLs d orig = do res <- runParserT (parseCSSUrls d) () "css" orig case res of@@ -300,21 +298,22 @@ Right bs -> return bs parseCSSUrls :: PandocMonad m- => FilePath -> ParsecT ByteString () m ByteString+ => FilePath+ -> ParsecT ByteString () (StateT ConvertState m) ByteString parseCSSUrls d = B.concat <$> P.many (pCSSWhite <|> pCSSComment <|> pCSSImport d <|> pCSSUrl d <|> pCSSOther) pCSSImport :: PandocMonad m- => FilePath -> ParsecT ByteString () m ByteString+ => FilePath+ -> ParsecT ByteString () (StateT ConvertState m) ByteString pCSSImport d = P.try $ do P.string "@import" P.spaces res <- (pQuoted <|> pUrl) >>= handleCSSUrl d P.spaces P.char ';'- P.spaces case res of- Left b -> return $ B.pack "@import " <> b+ Left b -> return $ B.pack "@import " <> b <> B.pack ";" Right (_, b) -> return b -- Note: some whitespace in CSS is significant, so we can't collapse it!@@ -334,7 +333,8 @@ (B.singleton <$> P.char '/') pCSSUrl :: PandocMonad m- => FilePath -> ParsecT ByteString () m ByteString+ => FilePath+ -> ParsecT ByteString () (StateT ConvertState m) ByteString pCSSUrl d = P.try $ do res <- pUrl >>= handleCSSUrl d case res of@@ -366,7 +366,7 @@ handleCSSUrl :: PandocMonad m => FilePath -> (T.Text, ByteString)- -> ParsecT ByteString () m+ -> ParsecT ByteString () (StateT ConvertState m) (Either ByteString (MimeType, ByteString)) handleCSSUrl d (url, fallback) = case escapeURIString (/='|') (T.unpack $ trim url) of@@ -382,7 +382,7 @@ (mt, b) <- if "text/css" `T.isPrefixOf` mt' -- see #5725: in HTML5, content type -- isn't allowed on style type attribute- then ("text/css",) <$> cssURLs d raw+ then ("text/css",) <$> lift (cssURLs d raw) else return (mt', raw) return $ Right (mt, b) CouldNotFetch _ -> return $ Left fallback@@ -393,19 +393,45 @@ | Fetched (MimeType, ByteString) deriving (Show) +-- | Decompress gzipped data, catching decompression errors instead+-- of throwing an imprecise exception from pure code.+decompressGzip :: ByteString -> Either T.Text ByteString+decompressGzip bs =+ B.concat <$>+ Zlib.foldDecompressStreamWithInput+ (\chunk rest -> (chunk :) <$> rest)+ (const (Right []))+ (Left . T.pack . show)+ (Zlib.decompressST Zlib.gzipFormat Zlib.defaultDecompressParams)+ (L.fromStrict bs)+ getData :: PandocMonad m => MimeType -> T.Text- -> m GetDataResult+ -> StateT ConvertState m GetDataResult getData mimetype src | "data:" `T.isPrefixOf` src = return $ AlreadyDataURI src -- already data: uri- | otherwise = catchError fetcher handler+ | otherwise = do+ cache <- gets fetchCache+ case M.lookup (mimetype, src) cache of+ Just res -> return res+ Nothing -> do+ res <- catchError fetcher handler+ modify $ \st ->+ st{ fetchCache = M.insert (mimetype, src) res (fetchCache st) }+ return res where fetcher = do let ext = T.toLower $ T.pack $ takeExtension $ T.unpack src (raw, respMime) <- fetchItem src- let raw' = if ext `elem` [".gz", ".svgz"]- then B.concat $ L.toChunks $ Gzip.decompress $ L.fromChunks [raw]- else raw+ if ext `elem` [".gz", ".svgz"]+ then case decompressGzip raw of+ Right raw' -> processFetched raw' respMime+ Left err -> do+ let msg = "could not decompress: " <> err+ report $ CouldNotFetchResource src msg+ return $ CouldNotFetch $ PandocSomeError msg+ else processFetched raw respMime+ processFetched raw' respMime = do let mime = case (mimetype, respMime) of ("",Nothing) -> "application/octet-stream" (x, Nothing) -> x@@ -441,10 +467,7 @@ makeSelfContained :: PandocMonad m => T.Text -> m T.Text makeSelfContained inp = do let tags = parseTags inp- let html5 = case tags of- (TagOpen "!DOCTYPE" [("html","")]:_) -> True- _ -> False- let convertState = ConvertState { isHtml5 = html5,- svgMap = mempty }+ let convertState = ConvertState { svgMap = mempty,+ fetchCache = mempty } out' <- evalStateT (convertTags tags) convertState return $ renderTags' out'
@@ -1,5 +1,4 @@ {-# LANGUAGE BangPatterns #-}-{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -45,9 +44,13 @@ removeFormatting, deNote, stringify,+ stringifyInlines, capitalize, compactify, compactifyDL,+ compactifyTable,+ plainToPara,+ paraToPlain, linesToPara, figureDiv, makeSections,@@ -96,7 +99,7 @@ import qualified Data.List as L import qualified Data.Map as M import Data.Maybe (mapMaybe)-import Data.Monoid (Any (..) )+import Data.Monoid (Any (..), All (..) ) import Data.Semigroup (Min (..)) import Data.Sequence (ViewL (..), ViewR (..), viewl, viewr) import qualified Data.Set as Set@@ -369,17 +372,17 @@ -- Footnotes are skipped (since we don't want their contents in link -- labels). stringify :: Walkable Inline a => a -> T.Text-stringify = query go . walk fixInlines- where go :: Inline -> T.Text- go Space = " "- go SoftBreak = " "- go (Str x) = x- go (Code _ x) = x- go (Math _ x) = x- go (RawInline (Format "html") (T.unpack -> ('<':'b':'r':_)))- = " " -- see #2105- go LineBreak = " "- go _ = ""+stringify = T.concat . query go . walk fixInlines+ where go :: Inline -> [T.Text]+ go Space = [" "]+ go SoftBreak = [" "]+ go (Str x) = [x]+ go (Code _ x) = [x]+ go (Math _ x) = [x]+ go (RawInline (Format "html") t)+ | "<br" `T.isPrefixOf` t = [" "] -- see #2105+ go LineBreak = [" "]+ go _ = [] fixInlines :: Inline -> Inline fixInlines (Cite _ ils) = Cite [] ils@@ -387,6 +390,38 @@ fixInlines (q@Quoted{}) = deQuote q fixInlines x = x +-- | Like 'stringify', but specialized to sequences of inlines+-- (e.g. @['Inline']@ or 'Inlines'). Produces the same result as+-- 'stringify' in a single pass, without rebuilding the tree.+stringifyInlines :: Foldable t => t Inline -> T.Text+stringifyInlines ils0 = T.concat $ foldr go [] ils0+ where+ go :: Inline -> [T.Text] -> [T.Text]+ go il acc = case il of+ Str x -> x : acc+ Space -> " " : acc+ SoftBreak -> " " : acc+ Code _ x -> x : acc+ Math _ x -> x : acc+ LineBreak -> " " : acc+ RawInline (Format "html") t+ | "<br" `T.isPrefixOf` t -> " " : acc -- see #2105+ RawInline _ _ -> acc+ Note _ -> acc -- footnotes are skipped+ Cite _ ils -> foldr go acc ils -- citation metadata is dropped+ Quoted SingleQuote ils -> "\8216" : foldr go ("\8217" : acc) ils+ Quoted DoubleQuote ils -> "\8220" : foldr go ("\8221" : acc) ils+ Emph ils -> foldr go acc ils+ Underline ils -> foldr go acc ils+ Strong ils -> foldr go acc ils+ Strikeout ils -> foldr go acc ils+ Superscript ils -> foldr go acc ils+ Subscript ils -> foldr go acc ils+ SmallCaps ils -> foldr go acc ils+ Span _ ils -> foldr go acc ils+ Link _ ils _ -> foldr go acc ils+ Image _ ils _ -> foldr go acc ils+ -- | Unwrap 'Quoted' inline elements, enclosing the contents with -- English-style Unicode quotes instead. deQuote :: Inline -> Inline@@ -423,10 +458,15 @@ -> items _ -> map (fmap plainToPara) items +-- | Convert a @Plain@ block to a @Para@; leave other blocks unchanged. plainToPara :: Block -> Block plainToPara (Plain ils) = Para ils plainToPara x = x +-- | Convert a @Para@ block to a @Plain@; leave other blocks unchanged.+paraToPlain :: Block -> Block+paraToPlain (Para ils) = Plain ils+paraToPlain x = x -- | Like @compactify@, but acts on items of definition lists. compactifyDL :: [(Inlines, [Blocks])] -> [(Inlines, [Blocks])]@@ -441,6 +481,24 @@ _ -> items _ -> items +-- | If every cell of the table is either empty or consists of a+-- single Para or Plain element, convert all Para to Plain for a compact+-- table.+compactifyTable ::+ (Walkable Block a, Walkable [Block] a, Walkable Inline a) => a -> a+compactifyTable x = if isSimpleTable x+ then walk fixNotes $ walk paraToPlain x+ else x+ where+ isSimpleCell :: [Block] -> All+ isSimpleCell [] = All True+ isSimpleCell [Para _] = All True+ isSimpleCell [Plain _] = All True+ isSimpleCell _ = All False+ isSimpleTable = getAll . query isSimpleCell+ -- walk descends into the notes, so we need to fix them back up:+ fixNotes (Note [Plain ils]) = Note [Para ils]+ fixNotes i = i -- | Combine a list of lines by adding hard linebreaks. combineLines :: [[Inline]] -> [Inline]@@ -463,7 +521,8 @@ , ["figure"] `union` classes , kv )- captkv = maybe mempty (\s -> [("short-caption", stringify s)]) shortcapt+ captkv = maybe mempty (\s -> [("short-caption", stringifyInlines s)])+ shortcapt capt = [Div ("", ["caption"], captkv) longcapt | not (null longcapt)] in Div divattr (body ++ capt) @@ -475,7 +534,7 @@ -- | Convert Pandoc inline list to plain text identifier. inlineListToIdentifier :: Extensions -> [Inline] -> T.Text inlineListToIdentifier exts =- textToIdentifier exts . stringify . unEmojify+ textToIdentifier exts . stringifyInlines . unEmojify where unEmojify :: [Inline] -> [Inline] unEmojify@@ -672,7 +731,7 @@ taskListItemFromAscii = handleTaskListItem fromMd where fromMd (Str "[" : Space : Str "]" : Space : is) = Str "☐" : Space : is- fromMd (Str "[ ]" : Space : is) = Str "☒" : Space : is+ fromMd (Str "[ ]" : Space : is) = Str "☐" : Space : is fromMd (Str "[x]" : Space : is) = Str "☒" : Space : is fromMd (Str "[X]" : Space : is) = Str "☒" : Space : is fromMd [Str "[" , Space , Str "]"] = [Str "☐"]@@ -743,7 +802,8 @@ fmt = concatMap go . groupBy (\a b -> isPlaintext a && isPlaintext b) where go xs- | all isPlaintext xs = B.toList $ B.codeWith attr $ stringify xs+ | all isPlaintext xs = B.toList $ B.codeWith attr $+ stringifyInlines xs | otherwise = xs --
@@ -27,6 +27,8 @@ , ensureFinalNewlines , addToInput , satisfy+ , takeWhileP+ , takeWhile1P , oneOf , noneOf , anyChar@@ -43,6 +45,8 @@ where import qualified Text.Parsec as P import Text.Parsec (Stream(..), ParsecT)+import Text.Parsec.Prim (mkPT, Consumed(..), Reply(..), State(..))+import Text.Parsec.Error (ParseError, newErrorMessage, Message(SysUnExpect)) import Text.Parsec.Pos as P import Data.Text (Text) import qualified Data.Text as T@@ -157,6 +161,80 @@ satisfy f = P.tokenPrim show updateSourcePos matcher where matcher !c = if f c then Just c else Nothing++-- | Consume characters while the predicate holds, returning them as+-- a 'Text'. Always succeeds (returning an empty 'Text' if no+-- characters match). Equivalent to @'Data.Text.pack' \<$> 'P.many'+-- ('satisfy' f)@ (including source position and error behavior), but+-- faster, because it processes whole chunks of text at a time.+takeWhileP :: Monad m => (Char -> Bool) -> ParsecT Sources u m Text+takeWhileP f = mkPT $ \st ->+ case spanSources f st of+ (t, st', err)+ | T.null t -> return (Empty (return (Ok t st' err)))+ | otherwise -> return (Consumed (return (Ok t st' err)))++-- | Like 'takeWhileP', but requires at least one matching character.+-- Equivalent to @'Data.Text.pack' \<$> 'P.many1' ('satisfy' f)@.+takeWhile1P :: Monad m => (Char -> Bool) -> ParsecT Sources u m Text+takeWhile1P f = mkPT $ \st ->+ case spanSources f st of+ (t, st', err)+ | T.null t -> return (Empty (return (Error err)))+ | otherwise -> return (Consumed (return (Ok t st' err)))++-- Consume characters matching the predicate from the beginning of+-- the input, returning the consumed text, the updated parser state,+-- and the error that a corresponding sequence of 'satisfy' parsers+-- would have recorded at the position where it stopped.+spanSources :: (Char -> Bool)+ -> State Sources u+ -> (Text, State Sources u, ParseError)+spanSources f (State (Sources input0) pos0 usr) = go pos0 input0 []+ where+ -- pos and committed are the position and input after the last+ -- consumed character (uncons drops leading empty chunks only when a+ -- character is actually consumed, so they must be retained if we+ -- stop at a chunk boundary).+ go !pos committed acc =+ case dropWhile (T.null . snd) committed of+ [] -> stop pos committed acc ""+ (p, t) : rest ->+ case T.span f t of+ (pre, post)+ | T.null pre -> stop pos committed acc (show (T.head t))+ | T.null post ->+ -- consumed the whole chunk: as with 'satisfy', the+ -- position jumps to the stored position of the next+ -- chunk, if any.+ let pos' = case rest of+ (pnext, _) : _ -> pnext+ [] -> advancePos pos pre+ in go pos' ((p, post) : rest) (pre : acc)+ | otherwise ->+ let pos' = advancePos pos pre+ in stop pos' ((p, post) : rest) (pre : acc)+ (show (T.head post))+ stop pos committed acc msg =+ ( case acc of+ [] -> mempty+ [t] -> t+ _ -> T.concat (reverse acc)+ , State (Sources committed) pos usr+ , newErrorMessage (SysUnExpect msg) pos )++-- Advance a source position over a stretch of text, using the same+-- position updates as the 'UpdateSourcePos' instance for 'Sources'.+advancePos :: SourcePos -> Text -> SourcePos+advancePos pos t+ | T.any (\c -> c == '\n' || c == '\t') t = T.foldl' advanceChar pos t+ | otherwise = incSourceColumn pos (T.length t)+ where+ advanceChar p c =+ case c of+ '\n' -> incSourceLine (setSourceColumn p 1) 1+ '\t' -> incSourceColumn p (4 - ((sourceColumn p - 1) `mod` 4))+ _ -> incSourceColumn p 1 oneOf :: (Monad m, Stream s m Char, UpdateSourcePos s Char) => [Char] -> ParsecT s u m Char
@@ -50,5 +50,49 @@ data Macro = Macro MacroScope ExpansionPoint [ArgSpec] (Maybe [Tok]) [Tok] deriving Show -data ArgSpec = ArgNum Int | Pattern [Tok]+data ArgSpec = ArgNum Int+ | Pattern [Tok]+ | BoolArg Bool Tok+ -- ^ xparse @s@ and @t@ specifiers: an optional token+ -- (e.g. a star); expands to @\\BooleanTrue@ or+ -- @\\BooleanFalse@. The Bool is False if the @!@+ -- modifier was used (no space-skipping before the+ -- token).+ | DelimArg Bool Tok Tok (Maybe [Tok])+ -- ^ xparse @o@, @O@, @d@, @D@, @r@, @R@ specifiers:+ -- argument between opening and closing delimiter+ -- tokens, with an optional default; when absent and+ -- no default is given, expands to @\\NoValue@. The+ -- Bool is False if the @!@ modifier was used (no+ -- space-skipping before the opening delimiter).+ | VerbArg+ -- ^ xparse @v@ specifier: a verbatim argument, either+ -- braced or between two identical delimiter+ -- characters; substituted as a single Word token+ -- containing the raw text.+ | EmbellishArg [(Tok, Maybe [Tok])]+ -- ^ xparse @e@ and @E@ specifiers: a set of optional+ -- \"embellishments\" (a token followed by an+ -- argument), matched in any order, each with an+ -- optional default; a missing embellishment expands+ -- to its default, or to @\\NoValue@.+ | ProcessedArg [[Tok]] ArgSpec+ -- ^ xparse @>{processor}@ modifier: the processors+ -- are applied to the grabbed argument from right to+ -- left (i.e., the one nearest the specifier first).+ | BodyArg Bool Int+ -- ^ xparse @b@ specifier (environments only): the+ -- environment body, grabbed up to the following+ -- 'Pattern' (the @\\end{...}@). The Bool is False if+ -- the @!@ modifier was used (no space-trimming at the+ -- ends of the body).+ | VerbBodyArg Bool Int+ -- ^ xparse @c@ specifier (environments only): the+ -- environment body, grabbed verbatim up to the+ -- following 'Pattern' (the @\\end{...}@); substituted+ -- as one raw Word token per line, with spaces+ -- replaced by U+2423 and lines separated by @\\\\@,+ -- mirroring how LaTeX typesets it. The Bool is+ -- False if the @!@ modifier was used (no trimming of+ -- leading and trailing blank lines). deriving Show
@@ -108,6 +108,7 @@ "asciidoc_legacy" -> getDefaultTemplate "asciidoc" "docx" -> getDefaultTemplate "openxml" "odt" -> getDefaultTemplate "opendocument"+ "fodt" -> getDefaultTemplate "opendocument" "html" -> getDefaultTemplate "html5" "docbook" -> getDefaultTemplate "docbook5" "epub" -> getDefaultTemplate "epub3"
@@ -127,11 +127,13 @@ removeANSI (CodeBlock attr code) = CodeBlock attr (removeANSIEscapes code) removeANSI x = x- removeANSIEscapes t- | Just cs <- T.stripPrefix "\x1b[" t =- removeANSIEscapes $ T.drop 1 $ T.dropWhile (/='m') cs- | Just (c, cs) <- T.uncons t = T.cons c $ removeANSIEscapes cs- | otherwise = ""+ removeANSIEscapes = T.concat . stripEscapes+ stripEscapes t =+ case T.breakOn "\x1b[" t of+ (before, rest)+ | T.null rest -> [before]+ | otherwise -> before :+ stripEscapes (T.drop 1 $ T.dropWhile (/= 'm') rest) go x = x -- | Remove soft breaks between East Asian characters.
@@ -1,5 +1,5 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-} {- | Module : Text.Pandoc.Translations Copyright : Copyright (C) 2017-2024 John MacFarlane@@ -27,18 +27,19 @@ import Text.Pandoc.Logging (LogMessage(..)) import Control.Monad.Except (catchError) import qualified Data.Text as T-import qualified Data.Yaml as Yaml-import qualified Text.Pandoc.UTF8 as UTF8-import Data.Yaml (prettyPrintParseException)+import qualified Data.List.NonEmpty as NE+import qualified Yamlet as Y+import Yamlet.Aeson import Text.Collate.Lang (Lang(..), renderLang) --- | Parse YAML translations.-readTranslations :: T.Text -> Either T.Text Translations-readTranslations s =- case Yaml.decodeAllEither' $ UTF8.fromText s of- Left err' -> Left $ T.pack $ prettyPrintParseException err'- Right (t:_) -> Right t- Right [] -> Left "empty YAML document"+-- | Parse YAML translations. The file path is used in error messages.+readTranslations :: FilePath -> T.Text -> Either T.Text Translations+readTranslations fp s =+ case Y.decodeAllText @(ViaAeson Translations) s of+ Left errs -> Left $ T.intercalate "\n" $+ map (T.pack . Y.prettyError fp) (NE.toList errs)+ Right (ViaAeson t:_) -> Right t+ Right [] -> Left $ T.pack fp <> ": empty YAML document" -- | Select the language to use with 'translateTerm'. -- Note that this does not read a translation file;@@ -46,7 +47,12 @@ -- used. setTranslations :: PandocMonad m => Lang -> m () setTranslations lang =- modifyCommonState $ \st -> st{ stTranslations = Just (lang, Nothing) }+ modifyCommonState $ \st ->+ case stTranslations st of+ -- if a translation table is already loaded for this language,+ -- keep it, so we don't have to parse the translation file again:+ Just (l, Just _) | l == lang -> st+ _ -> st{ stTranslations = Just (lang, Nothing) } -- | Load term map. getTranslations :: PandocMonad m => m Translations@@ -63,12 +69,11 @@ let getTrans [] = return mempty getTrans (fp:fps) = do result <- catchError (Right <$> (readDataFile fp >>= toTextM fp))- (\_ -> pure (Left ""))- case result >>= readTranslations of+ (\_ -> pure (Left (T.pack fp)))+ case result >>= readTranslations fp of Left e | null fps -> do- report $ CouldNotLoadTranslations (renderLang lang)- (T.pack fp <> ": " <> e)+ report $ CouldNotLoadTranslations (renderLang lang) e -- make sure we don't try again... modifyCommonState $ \st -> st{ stTranslations = Nothing }
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-}@@ -33,7 +32,6 @@ import qualified Data.Map as M import qualified Data.Text as T import GHC.Generics (Generic)-import Text.Pandoc.Shared (safeRead) data Term = Abstract@@ -58,13 +56,21 @@ | SeeAlso | Table | To- deriving (Show, Eq, Ord, Generic, Enum, Read)+ deriving (Show, Eq, Ord, Generic, Enum, Bounded, Read) newtype Translations = Translations (M.Map Term T.Text) deriving (Show, Generic, Semigroup, Monoid) +-- | Map from term names to terms. This is much faster than+-- using the derived 'Read' instance to parse term names.+termNameMap :: M.Map T.Text Term+termNameMap = M.fromList [(T.pack (show t), t) | t <- [minBound..maxBound]]++readTerm :: T.Text -> Maybe Term+readTerm t = M.lookup t termNameMap+ instance FromJSON Term where- parseJSON (String t) = case safeRead t of+ parseJSON (String t) = case readTerm t of Just t' -> pure t' Nothing -> Prelude.fail $ "Invalid Term name " ++ show t@@ -75,7 +81,7 @@ xs <- parseJSON o >>= mapM addItem . M.toList return $ Translations (M.fromList xs) where addItem (k,v) =- case safeRead k of+ case readTerm k of Nothing -> Prelude.fail $ "Invalid Term name " ++ show k Just t -> case v of
@@ -126,7 +126,7 @@ pBase64DataURI = base64uri where base64uri = do- A.string "data:"+ A.asciiCI "data:" -- the scheme is case-insensitive (RFC 3986) mime <- do n1 <- restrictedName A.char '/'
@@ -50,9 +50,7 @@ putStrLn, readFile, writeFile) readFile :: FilePath -> IO Text-readFile f = do- h <- openFile (encodePath f) ReadMode- hGetContents h+readFile f = withFile (encodePath f) ReadMode hGetContents getContents :: IO Text getContents = hGetContents stdin@@ -103,7 +101,11 @@ if "\xEF\xBB\xBF" `B.isPrefixOf` bs then B.drop 3 bs else bs- filterCRs = B.filter (/='\r')+ -- Only allocate a filtered copy if a CR is actually present;+ -- B.elem compiles to a fast memchr.+ filterCRs bs = if '\r' `B.elem` bs+ then B.filter (/='\r') bs+ else bs -- | Convert UTF8-encoded ByteString to String, also -- removing '\\r' characters.@@ -118,7 +120,13 @@ if "\xEF\xBB\xBF" `BL.isPrefixOf` bs then BL.drop 3 bs else bs- filterCRs = BL.filter (/='\r')+ -- Work chunk-wise (rather than using BL.elem on the whole+ -- input) to preserve laziness; skip allocation for chunks+ -- that contain no CRs.+ filterCRs = BL.fromChunks . map filterChunk . BL.toChunks+ filterChunk bs = if '\r' `B.elem` bs+ then B.filter (/='\r') bs+ else bs -- | Convert UTF8-encoded ByteString to String, also -- removing '\\r' characters.
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {- | Module : Text.Pandoc.Version Copyright : Copyright (C) 2022-2024 John MacFarlane
@@ -61,6 +61,7 @@ , writeMuse , writeNative , writeODT+ , writeFODT , writeOPML , writeOpenDocument , writeOrg@@ -161,6 +162,7 @@ ,("json" , TextWriter writeJSON) ,("docx" , ByteStringWriter writeDocx) ,("odt" , ByteStringWriter writeODT)+ ,("fodt" , TextWriter writeFODT) ,("pptx" , ByteStringWriter writePowerpoint) ,("epub" , ByteStringWriter writeEPUB3) ,("epub2" , ByteStringWriter writeEPUB2)
@@ -79,9 +79,7 @@ metadata <- metaToContext opts (blockListToANSI opts) (inlineListToANSI opts) meta- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing+ let colwidth = wrapColumns opts let title = titleBlock colwidth metadata let blocks' = makeSections (writerNumberSections opts) Nothing blocks body <- blockListToANSI opts blocks'@@ -190,7 +188,12 @@ blockToANSI opts (BlockQuote blocks) = do contents <- withFewerColumns 2 $ blockListToANSI opts blocks- return ( D.prefixed "│ " contents $$ D.blankline)+ -- D.cr ensures we start the blockquote on its own line: without it,+ -- a blockquote that begins mid-line (e.g. as the first block of a+ -- list item, sharing a line with the item's marker) would have its+ -- prefix omitted on the first line, since D.prefixed only prefixes+ -- lines that begin at column 0.+ return ( D.cr <> D.prefixed "│ " contents $$ D.blankline) blockToANSI opts (Table _ (Caption _ caption) colSpecs (TableHead _ thead) tbody (TableFoot _ tfoot)) = do let captionInlines = blocksToInlines caption
@@ -53,7 +53,6 @@ import Text.Pandoc.Builder (emptyCell) import Text.Pandoc.Shared import Text.Pandoc.URI-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Writers.Shared import Text.Pandoc.Walk (walk) @@ -111,9 +110,6 @@ pandocToAsciiDoc opts (Pandoc meta blocks) = do let titleblock = not $ null (docTitle meta) && null (docAuthors meta) && null (docDate meta)- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing metadata <- metaToContext opts (blockListToAsciiDoc opts) (fmap chomp . inlineListToAsciiDoc opts)@@ -126,10 +122,7 @@ isJust (writerTemplate opts)) $ defField "math" (hasMath st && not (legacy st)) $ defField "titleblock" titleblock metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main data EscContext = Normal | InTable deriving (Show, Eq)
@@ -26,10 +26,10 @@ import Text.Pandoc.Citeproc.BibTeX as BibTeX import Text.Pandoc.Citeproc.MetaValue (metaValueToReference) import Text.Pandoc.Writers.Shared (lookupMetaString, defField,- addVariablesToContext)-import Text.DocLayout (render, vcat)+ addVariablesToContext,+ renderWithTemplate)+import Text.DocLayout (vcat) import Text.DocTemplates (Context(..))-import Text.Pandoc.Templates (renderTemplate) -- | Write BibTeX based on the references metadata from a Pandoc document. writeBibTeX :: PandocMonad m => WriterOptions -> Pandoc -> m Text@@ -50,10 +50,4 @@ let main = vcat $ map (BibTeX.writeBibtexString opts variant mblang) refs let context = defField "body" main $ addVariablesToContext opts (mempty :: Context Text)- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main
@@ -16,7 +16,7 @@ ) where import Text.Pandoc.Definition import Text.Pandoc.Options (WriterOptions(..))-import Text.Pandoc.Shared (stringify, tshow)+import Text.Pandoc.Shared (stringifyInlines, tshow) import Text.Pandoc.Class (PandocMonad, getPOSIXTime, runPure, fetchItem, insertMedia, getMediaBag) import Text.Pandoc.MediaBag (mediaItems)@@ -133,7 +133,8 @@ where opts' = opts{ writerVariables = addContextVars opts' topChunk chunk $ writerVariables opts }- meta' = setMeta "pagetitle" (MetaString (stringify $ chunkHeading chunk)) meta+ meta' = setMeta "pagetitle"+ (MetaString (stringifyInlines $ chunkHeading chunk)) meta blocks = chunkContents chunk tocTreeToContext :: Tree SecInfo -> Context Text@@ -146,7 +147,7 @@ secInfoToContext :: SecInfo -> Context Text secInfoToContext sec = Context $ M.fromList- [ ("title", SimpleVal $ literal $ stringify $ secTitle sec)+ [ ("title", SimpleVal $ literal $ stringifyInlines $ secTitle sec) , ("number", maybe NullVal (SimpleVal . literal) (secNumber sec)) , ("id", SimpleVal $ literal $ secId sec) , ("path", SimpleVal $ literal $ secPath sec)
@@ -37,7 +37,6 @@ import Text.DocLayout import Text.Pandoc.Shared import Text.Pandoc.URI (isURI)-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Walk (query) import Text.Pandoc.Writers.Shared import Text.Printf (printf)@@ -86,9 +85,6 @@ pandocToConTeXt :: PandocMonad m => WriterOptions -> Pandoc -> WM m Text pandocToConTeXt options (Pandoc meta blocks) = do- let colwidth = if writerWrapText options == WrapAuto- then Just $ writerColumns options- else Nothing metadata <- metaToContext options blockListToConTeXt (fmap chomp . inlineListToConTeXt)@@ -144,10 +140,7 @@ _ -> id) metadata let context' = defField "context-dir" (maybe mempty toContextDir $ getField "dir" context) context- return $ render colwidth $- case writerTemplate options of- Nothing -> main- Just tpl -> renderTemplate tpl context'+ return $ renderWithTemplate options context' main -- change rtl to r2l, ltr to l2r toContextDir :: Doc Text -> Doc Text
@@ -27,11 +27,11 @@ import qualified Data.Text as T import qualified Data.Map as M import qualified Text.Pandoc.UTF8 as UTF8-import Text.Pandoc.Writers.Shared ( metaToContext, defField, toLegacyTable )-import Text.Pandoc.Shared (isTightList, tshow, stringify, onlySimpleTableCells,- makeSections)+import Text.Pandoc.Writers.Shared ( metaToContext, defField, toLegacyTable,+ renderWithTemplate )+import Text.Pandoc.Shared (isTightList, tshow, stringifyInlines,+ onlySimpleTableCells, makeSections) import Text.DocLayout-import Text.DocTemplates (renderTemplate) import Control.Monad.State (StateT(..), gets, modify) import Control.Monad (zipWithM, when)@@ -43,9 +43,6 @@ -- | Convert Pandoc to Djot. writeDjot :: PandocMonad m => WriterOptions -> Pandoc -> m Text writeDjot opts (Pandoc meta blocks) = do- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing let ropts = RenderOptions{ preserveSoftBreaks = writerWrapText opts == WrapPreserve } metadata <- metaToContext opts@@ -56,10 +53,7 @@ main <- renderDjot ropts <$> bodyToDjot opts (makeSections False Nothing blocks) let context = defField "body" main metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main data DjotState = DjotState@@ -240,7 +234,7 @@ inlineToDjot (Link attr ils (src,tit)) = do opts <- gets options description <- inlinesToDjot ils- let ilstring = stringify ils+ let ilstring = stringifyInlines ils let autolink = ilstring == src let email = ("mailto:" <> ilstring) == src let removeClass name (ident, cls, kvs) = (ident, filter (/= name) cls, kvs)
@@ -66,9 +66,7 @@ authorToDocBook :: PandocMonad m => WriterOptions -> [Inline] -> DB m B.Inlines authorToDocBook opts name' = do name <- render Nothing <$> inlinesToDocBook opts name'- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing+ let colwidth = wrapColumns opts return $ B.rawInline "docbook" $ render colwidth $ inTags True "personname" [] $ if T.any (== ',') name@@ -99,9 +97,7 @@ writeDocBook :: PandocMonad m => WriterOptions -> Pandoc -> DB m Text writeDocBook opts doc = do let Pandoc meta blocks = ensureValidXmlIdentifiers doc- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing+ let colwidth = wrapColumns opts let startLvl = getStartLvl opts let fromBlocks = blocksToDocBook opts . makeSections False (Just startLvl)@@ -126,11 +122,6 @@ blocksToDocBook :: PandocMonad m => WriterOptions -> [Block] -> DB m (Doc Text) blocksToDocBook opts = fmap vcat . mapM (blockToDocBook opts) --- | Auxiliary function to convert Plain block to Para.-plainToPara :: Block -> Block-plainToPara (Plain x) = Para x-plainToPara x = x- -- | Convert a list of pairs of terms and definitions into a list of -- DocBook varlistentrys. deflistItemsToDocBook :: PandocMonad m@@ -162,8 +153,8 @@ inTagsIndented "listitem" <$> blocksToDocBook opts (map plainToPara item) imageToDocBook :: WriterOptions -> Attr -> Text -> Doc Text-imageToDocBook _ attr src = selfClosingTag "imagedata" $- ("fileref", src) : idAndRole attr <> dims+imageToDocBook opts attr src = selfClosingTag "imagedata" $+ ("fileref", src) : idAndRole opts attr <> dims where dims = go Width "width" <> go Height "depth" go dir dstr = case dimension dir attr of@@ -206,7 +197,8 @@ return $ inTags True tag attribs $ inTagsSimple "title" title' $$ contents blockToDocBook opts (Div (ident,classes,_) bs) = do version <- ask- let identAttribs = [(idName version, ident) | not (T.null ident)]+ let identAttribs = [(idName version, writerIdentifierPrefix opts <> ident)+ | not (T.null ident)] admonitions = ["caution","danger","important","note","tip","warning"] case classes of (l:_) | l `elem` admonitions -> do@@ -396,7 +388,8 @@ version <- ask ((if T.null ident then mempty- else selfClosingTag "anchor" [(idName version, ident)]) <>) <$>+ else selfClosingTag "anchor"+ [(idName version, writerIdentifierPrefix opts <> ident)]) <>) <$> inlinesToDocBook opts ils inlineToDocBook _ (Code _ str) = return $ inTagsSimple "literal" $ literal (escapeStringForXML str)@@ -444,10 +437,10 @@ then let tag = if null txt then "xref" else "link" in inTags False tag $ ("linkend", writerIdentifierPrefix opts <> T.drop 1 src) :- idAndRole attr+ idAndRole opts attr else if version == DocBook5- then inTags False "link" $ ("xlink:href", src) : idAndRole attr- else inTags False "ulink" $ ("url", src) : idAndRole attr )+ then inTags False "link" $ ("xlink:href", src) : idAndRole opts attr+ else inTags False "ulink" $ ("url", src) : idAndRole opts attr ) <$> inlinesToDocBook opts txt inlineToDocBook opts (Image attr ils (src, tit)) = return $ let titleDoc = if T.null tit@@ -457,7 +450,7 @@ alt = if null ils then mempty else inTagsIndented "textobject" $- inTagsSimple "phrase" $ literal (stringify ils)+ inTagsSimple "phrase" $ literal (stringifyInlines ils) in inTagsIndented "inlinemediaobject" $ inTagsIndented "imageobject" (titleDoc $$ imageToDocBook opts attr src)@@ -469,10 +462,10 @@ isMathML MathML = True isMathML _ = False -idAndRole :: Attr -> [(Text, Text)]-idAndRole (id',cls,_) = ident <> role+idAndRole :: WriterOptions -> Attr -> [(Text, Text)]+idAndRole opts (id',cls,_) = ident <> role where- ident = [("id", id') | not (T.null id')]+ ident = [("id", writerIdentifierPrefix opts <> id') | not (T.null id')] role = [("role", T.unwords cls) | not (null cls)] -- Used in blockToDocBook for Header (section) to create or extend
@@ -23,6 +23,7 @@ findEntryByPath, fromArchive, toArchive,+ toArchiveOrFail, toEntry, Entry(eRelativePath) ) import Control.Monad (MonadPlus(mplus), foldM)@@ -500,16 +501,24 @@ loadArchives opts = do username <- P.lookupEnv "USERNAME" utctime <- P.getTimestamp- oldUserDataDir <- P.getUserDataDir- P.setUserDataDir Nothing res <- readDefaultDataFile "reference.docx"- P.setUserDataDir oldUserDataDir let distArchive = toArchive $ BL.fromStrict res refArchive <- case writerReferenceDoc opts of- Just f -> toArchive . BL.fromStrict . fst+ Just f -> do+ arch <- toArchiveOrFail . BL.fromStrict . fst <$> P.fetchItem (T.pack f)- Nothing -> toArchive . BL.fromStrict <$>- readDataFile "reference.docx"+ case arch of+ Left err -> throwError $ PandocParseError $+ "Could not parse reference-doc " <>+ tshow f <> ": " <> T.pack err+ Right x -> pure x+ Nothing -> do+ arch <- toArchiveOrFail . BL.fromStrict <$>+ readDataFile "reference.docx"+ case arch of+ Left err -> throwError $ PandocParseError $+ "Could not parse reference.docx: " <> T.pack err+ Right x -> pure x return (refArchive, distArchive, username, utctime) isWmlNamespace :: QName -> Bool@@ -761,7 +770,7 @@ mkCorePropsEntry :: Integer -> UTCTime -> Meta -> Entry mkCorePropsEntry epochtime utctime meta = let metaValueToText (MetaString s) = s- metaValueToText (MetaInlines ils) = stringify ils+ metaValueToText (MetaInlines ils) = stringifyInlines ils metaValueToText (MetaBlocks bs) = stringify bs metaValueToText (MetaBool b) = T.pack (show b) metaValueToText _ = ""@@ -785,8 +794,8 @@ ,("xmlns:dcterms","http://purl.org/dc/terms/") ,("xmlns:dcmitype","http://purl.org/dc/dcmitype/") ,("xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance")]- $ mktnode "dc:title" [] (stringify $ docTitle meta)- : mktnode "dc:creator" [] (T.intercalate "; " (map stringify $ docAuthors meta))+ $ mktnode "dc:title" [] (stringifyInlines $ docTitle meta)+ : mktnode "dc:creator" [] (T.intercalate "; " (map stringifyInlines $ docAuthors meta)) : [ mktnode (M.findWithDefault "" k extraCorePropsMap) [] (lookupMetaString' k meta) | k <- M.keys (unMeta meta), k `elem` extraCoreProps] ++ mknode "cp:keywords" [] (T.intercalate ", " keywords)
@@ -25,12 +25,12 @@ import Control.Monad.Except (catchError) import Crypto.Hash (hashWith, SHA1(SHA1)) import qualified Data.ByteString.Lazy as BL-import Data.Char (isLetter, isSpace)+import Data.Char (isSpace, isAlphaNum) import Text.Pandoc.Char (isCJK) import Data.Ord (comparing) import Data.String (fromString) import qualified Data.Map as M-import Data.Maybe (fromMaybe, maybeToList, isJust)+import Data.Maybe (fromMaybe, maybeToList, isJust, listToMaybe) import Control.Monad.State ( gets, modify, MonadTrans(lift) ) import Control.Monad.Reader ( asks, MonadReader(local) ) import qualified Data.Set as Set@@ -110,18 +110,62 @@ , "oMath" ] [0..]) -sortSquashed :: [Element] -> [Element]-sortSquashed l =- sortBy (comparing tagIndex) l+-- from wml.xsd EG_PPrBase+pPrTagOrder :: M.Map Text Int+pPrTagOrder =+ M.fromList+ (zip [ "pStyle"+ , "keepNext"+ , "keepLines"+ , "pageBreakBefore"+ , "framePr"+ , "widowControl"+ , "numPr"+ , "suppressLineNumbers"+ , "pBdr"+ , "shd"+ , "tabs"+ , "suppressAutoHyphens"+ , "kinsoku"+ , "wordWrap"+ , "overflowPunct"+ , "topLinePunct"+ , "autoSpaceDE"+ , "autoSpaceDN"+ , "bidi"+ , "adjustRightInd"+ , "snapToGrid"+ , "spacing"+ , "ind"+ , "contextualSpacing"+ , "mirrorIndents"+ , "suppressOverlap"+ , "jc"+ , "textDirection"+ , "textAlignment"+ , "textboxTightWrap"+ , "outlineLvl"+ , "divId"+ , "cnfStyle"+ , "rPr"+ , "sectPr"+ , "pPrChange"+ ] [0..])++sortSquashed :: M.Map Text Int -> Set.Set Element -> [Element]+sortSquashed tagOrder l =+ sortBy (comparing tagIndex) (Set.toList l) where tagIndex :: Element -> Int tagIndex el =- fromMaybe 0 (M.lookup tag rPrTagOrder)+ fromMaybe 0 (M.lookup tag tagOrder) where tag = (qName . elName) el -squashProps :: EnvProps -> [Element]-squashProps (EnvProps Nothing es) = sortSquashed es-squashProps (EnvProps (Just e) es) = sortSquashed (e : es)+squashProps :: M.Map Text Int -> EnvProps -> [Element]+squashProps tagOrder (EnvProps Nothing es) =+ sortSquashed tagOrder es+squashProps tagOrder (EnvProps (Just e) es) =+ sortSquashed tagOrder (Set.insert e es) -- | Certain characters are invalid in XML even if escaped. -- See #1992@@ -236,12 +280,18 @@ -> WS m (Text, [Element], [Element]) writeOpenXML opts (Pandoc meta blocks) = do setupTranslations meta+ -- Cache the rStyle element for each highlighting token type, so that+ -- it need not be recomputed for every Code inline. It depends only+ -- on the style maps, which don't change during writing.+ tokTypesMap <- M.fromList <$>+ mapM (\tt -> (tt,) <$> rStyleM (fromString $ show tt)) [KeywordTok ..]+ modify $ \st -> st{ stTokTypesMap = tokTypesMap } let includeTOC = writerTableOfContents opts || lookupMetaBool "toc" meta let includeLOF = writerListOfFigures opts || lookupMetaBool "lof" meta let includeLOT = writerListOfTables opts || lookupMetaBool "lot" meta abstractTitle <- case lookupMeta "abstract-title" meta of Just (MetaBlocks bs) -> pure $ stringify bs- Just (MetaInlines ils) -> pure $ stringify ils+ Just (MetaInlines ils) -> pure $ stringifyInlines ils Just (MetaString s) -> pure s _ -> translateTerm Abstract abstract <-@@ -326,9 +376,13 @@ -- | Convert a list of Pandoc blocks to OpenXML. blocksToOpenXML :: (PandocMonad m) => WriterOptions -> [Block] -> WS m [Content]-blocksToOpenXML opts =- fmap concat . mapM (blockToOpenXML opts)- . separateTables . filter (not . isForeignRawBlock)+blocksToOpenXML opts bs = do+ oldFirstPara <- gets stFirstPara+ modify $ \st -> st{ stFirstPara = True }+ result <- concat <$> mapM (blockToOpenXML opts)+ (separateTables (filter (not . isForeignRawBlock) bs))+ modify $ \st -> st{ stFirstPara = oldFirstPara }+ pure result isForeignRawBlock :: Block -> Bool isForeignRawBlock (RawBlock format _) = format /= "openxml"@@ -358,12 +412,44 @@ dynamicStyleKey :: Text dynamicStyleKey = "custom-style" +-- | Paragraph properties for a CSL-generated bibliography, derived from+-- the hints @Text.Pandoc.Citeproc@ puts on the bibliography's Div:+-- a @hanging-indent@ class and @line-spacing@/@entry-spacing@ attributes.+cslBibParaProps :: [Text] -> [(Text, Text)] -> [Element]+cslBibParaProps classes kvs =+ [ mknode "w:ind" [("w:left", "720"), ("w:hanging", "720")] ()+ | "hanging-indent" `elem` classes ] +++ [ mknode "w:spacing" spacingAttrs () | not (null spacingAttrs) ]+ where+ spacingAttrs = lineAttr ++ entryAttr+ lineAttr = case lookup "line-spacing" kvs >>= safeRead of+ Just ls | ls > (1 :: Double) ->+ [ ("w:line", tshow (round (ls * 240) :: Int))+ , ("w:lineRule", "auto") ]+ _ -> []+ entryAttr = case lookup "entry-spacing" kvs >>= safeRead of+ Just es | es > (0 :: Double) ->+ -- entry-spacing is given in em; 1 em ~ 240 twips+ [ ("w:after", tshow (round (es * 240) :: Int)) ]+ _ -> []+ -- | Convert a Pandoc block element to OpenXML. blockToOpenXML :: (PandocMonad m) => WriterOptions -> Block -> WS m [Content] blockToOpenXML opts blk = withDirection $ blockToOpenXML' opts blk blockToOpenXML' :: (PandocMonad m) => WriterOptions -> Block -> WS m [Content]-blockToOpenXML' opts (Div (ident,_classes,kvs) bs) = do+-- A section's bookmark goes in its heading's paragraph, around the+-- heading's text, as Word writes one: it then marks the heading rather+-- than the whole section (#11845, #8825), and the docx reader, which+-- reads bookmarks only inside paragraphs, finds it, so a link to the+-- heading survives a round trip.+blockToOpenXML' opts (Div (ident,classes,kvs) (Header lev ("",hcls,hkvs) ils : bs))+ | "section" `elem` classes+ , not (T.null ident)+ = blockToOpenXML' opts+ (Div ("",classes,kvs) (Header lev (ident,hcls,hkvs) ils : bs))+blockToOpenXML' opts (Div (ident,classes,kvs) bs) = do+ when ("math" `elem` classes) $ setFirstPara stylemod <- case lookup dynamicStyleKey kvs of Just (fromString . T.unpack -> sty) -> do modify $ \s ->@@ -384,9 +470,18 @@ let langmod = case lookup "lang" kvs of Nothing -> id Just lang -> local (\env -> env{envLang = Just lang})+ -- citeproc adds formatting hints for bibliographies generated+ -- from a CSL style; see Text.Pandoc.Citeproc (#11871).+ let isCslBib = ident == "refs" || "csl-bib-body" `elem` classes+ let cslmod = if not isCslBib+ then id+ else case cslBibParaProps classes kvs of+ [] -> id+ props -> foldr (.) id (map withParaProp props) header <- dirmod $ stylemod $ blocksToOpenXML opts hs- contents <- dirmod $ bibmod $ stylemod $ langmod $ blocksToOpenXML opts bs'+ contents <- dirmod $ bibmod $ cslmod $ stylemod $ langmod $ blocksToOpenXML opts bs' wrapBookmark ident $ header <> contents+ blockToOpenXML' opts (Header lev (ident,_,kvs) lst) = do setFirstPara let isSection = case writerTopLevelDivision opts of@@ -447,16 +542,16 @@ let displayMathPara = case lst of [x] -> isDisplayMath x _ -> False- paraProps <- getParaProps displayMathPara bodyTextStyle <- pStyleM $ if isFirstPara then "First Paragraph" else "Body Text"- let paraProps' = case paraProps of- [] -> [mknode "w:pPr" [] [bodyTextStyle]]- ps -> ps+ paraProps <- local (\env -> env{ envParaProperties =+ envParaProperties env <>+ EnvProps (Just bodyTextStyle) mempty })+ (getParaProps displayMathPara) modify $ \s -> s { stFirstPara = False } contents <- inlinesToOpenXML opts lst- return [Elem $ mknode "w:p" [] (map Elem paraProps' ++ contents)]+ return [Elem $ mknode "w:p" [] (map Elem paraProps ++ contents)] blockToOpenXML' opts (LineBlock lns) = blockToOpenXML opts $ linesToPara lns blockToOpenXML' _ b@(RawBlock format str) | format == Format "openxml" = return [@@ -530,7 +625,7 @@ then "Figure" else "Captioned Figure" paraProps <- local- (\env -> env { envParaProperties = EnvProps (Just prop) [] <>+ (\env -> env { envParaProperties = EnvProps (Just prop) mempty <> envParaProperties env }) (getParaProps False) @@ -677,15 +772,29 @@ let langnode = case mblang of Nothing -> mempty Just l -> EnvProps Nothing- [mknode "w:lang" [("w:val", l)] ()]- let squashed = squashProps (props <> langnode)+ (Set.singleton+ (mknode "w:lang" [("w:val", l)] ()))+ let squashed = squashProps rPrTagOrder (props <> langnode) return [mknode "w:rPr" [] squashed | (not . null) squashed] withTextProp :: PandocMonad m => Element -> WS m a -> WS m a-withTextProp d p =- local (\env -> env {envTextProperties = ep <> envTextProperties env}) p- where ep = if isStyle d then EnvProps (Just d) [] else EnvProps Nothing [d]+withTextProp d+ | isStyle d = local (\env -> env{ envTextProperties = EnvProps (Just d)+ (otherElements (envTextProperties env)) })+ | isToggle d = local (\env ->+ let EnvProps mbsty elts = envTextProperties env+ in env{ envTextProperties = EnvProps mbsty $+ if d `Set.member` elts+ then Set.delete d elts+ else Set.insert d elts })+ | otherwise = local (\env -> env{ envTextProperties =+ EnvProps Nothing (Set.singleton d) <>+ envTextProperties env }) +-- Let emphasis flip-flop, so emph within emph is upright:+isToggle :: Element -> Bool+isToggle el = qName (elName el) `elem` ["i", "iCs"]+ withTextPropM :: PandocMonad m => WS m Element -> WS m a -> WS m a withTextPropM md p = do d <- md@@ -697,14 +806,21 @@ listLevel <- asks envListLevel numid <- asks envListNumId numIdUsed <- gets stNumIdUsed- -- clear numId after first use to support multiple paragraphs in the same bullet- -- baseListId is the code for no list marker- let numid' = if numIdUsed then baseListId else numid modify $ \st -> st{ stNumIdUsed = True }- let listPr = [mknode "w:numPr" []- [ mknode "w:ilvl" [("w:val",tshow listLevel)] ()- , mknode "w:numId" [("w:val",tshow numid')] () ] | listLevel >= 0 && not displayMathPara]- return $ case squashProps (EnvProps Nothing listPr <> props) of+ -- Only the first paragraph of a list item gets numbering. Subsequent+ -- paragraphs in the same item are not numbered, but indented to line+ -- up with the item's text (see the w:ind in mkLvl), so that they are+ -- not mistaken for items of another list (#9197):+ let listPr+ | listLevel < 0 || displayMathPara = []+ | numIdUsed = [mknode "w:ind"+ [ ("w:left", tshow (listLevel * 720 + 720))+ , ("w:firstLine", "0") ] ()]+ | otherwise = [mknode "w:numPr" []+ [ mknode "w:ilvl" [("w:val",tshow listLevel)] ()+ , mknode "w:numId" [("w:val",tshow numid)] () ]]+ return $ case squashProps pPrTagOrder+ (EnvProps Nothing (Set.fromList listPr) <> props) of [] -> [] ps -> [mknode "w:pPr" [] ps] @@ -747,6 +863,69 @@ props <- getTextProps return $ mknode "w:r" [] $ props ++ els +-- | Inject a highlight into every run of a converted math element:+-- glyph runs in a @w:rPr@ child, and a structure's non-glyph parts+-- (bars, operator spacing) in the @m:ctrlPr@ of its property element+-- (@m:sSupPr@ and the like), which Word pens when an equation is+-- highlighted by hand and which the converters omit when empty.+highlightMathRuns :: XML.Element -> XML.Element -> XML.Element+highlightMathRuns hl = go+ where+ isTag pfx n e = XML.qName (XML.elName e) == n+ && XML.qPrefix (XML.elName e) == Just pfx+ go e+ | isTag "m" "r" e || isTag "m" "ctrlPr" e =+ e{ XML.elContent = insertHl (map walkContent (XML.elContent e)) }+ | Just "m" <- XML.qPrefix (XML.elName e)+ , XML.qName (XML.elName e) `elem` structures =+ e{ XML.elContent =+ ensureCtrlPr (XML.qName (XML.elName e) <> "Pr")+ (map walkContent (XML.elContent e)) }+ | otherwise = e{ XML.elContent = map walkContent (XML.elContent e) }+ walkContent (XML.Elem e) = XML.Elem (go e)+ walkContent c = c+ -- OMML structures (ECMA-376 22.1.2); each takes an m:<name>Pr+ -- property element as its first child+ structures =+ [ "sSup", "sSub", "sSubSup", "sPre"+ , "f", "nary", "d", "rad", "func", "groupChr"+ , "limLow", "limUpp", "m", "eqArr", "bar"+ , "phant", "box", "borderBox", "acc" ]+ pennedCtrlPr = XML.Elem (mknode "m:ctrlPr" [] (mknode "w:rPr" [] hl))+ ensureCtrlPr prName cs = case cs of+ (XML.Elem e : rest) | isTag "m" prName e ->+ if any isCtrlPr (XML.elContent e)+ then cs+ else XML.Elem e{ XML.elContent = XML.elContent e ++ [pennedCtrlPr] } : rest+ _ -> XML.Elem (mknode ("m:" <> prName) [] pennedCtrlPr) : cs+ isCtrlPr (XML.Elem e) = isTag "m" "ctrlPr" e+ isCtrlPr _ = False+ -- merge into an existing w:rPr child if there is one, else insert a+ -- fresh w:rPr after an optional m:rPr+ insertHl cs =+ case break isWrPr cs of+ (before, XML.Elem e : rest) ->+ before ++ XML.Elem e{ XML.elContent = insertInRPr (XML.elContent e) } : rest+ _ ->+ case cs of+ (XML.Elem e : rest) | isTag "m" "rPr" e ->+ XML.Elem e : wrPr : rest+ _ -> wrPr : cs+ -- w:rPr children come in a fixed order (rPrTagOrder): the+ -- highlight goes before the first child that must follow it, and+ -- after children not in the order+ insertInRPr [] = [XML.Elem hl]+ insertInRPr (c:rest)+ | followsHl c = XML.Elem hl : c : rest+ | otherwise = c : insertInRPr rest+ followsHl (XML.Elem e) =+ maybe False (> rPrTagOrder M.! "highlight") $+ M.lookup (XML.qName (XML.elName e)) rPrTagOrder+ followsHl _ = False+ isWrPr (XML.Elem e) = isTag "w" "rPr" e+ isWrPr _ = False+ wrPr = XML.Elem (mknode "w:rPr" [] hl)+ -- | Convert an inline element to OpenXML. inlineToOpenXML :: PandocMonad m => WriterOptions -> Inline -> WS m [Content] inlineToOpenXML opts il = withDirection $ inlineToOpenXML' opts il@@ -754,8 +933,8 @@ inlineToOpenXML' :: PandocMonad m => WriterOptions -> Inline -> WS m [Content] inlineToOpenXML' _ (Str str) = map Elem <$> formattedString str-inlineToOpenXML' opts Space = inlineToOpenXML opts (Str " ")-inlineToOpenXML' opts SoftBreak = inlineToOpenXML opts (Str " ")+inlineToOpenXML' opts Space = inlineToOpenXML' opts (Str " ")+inlineToOpenXML' opts SoftBreak = inlineToOpenXML' opts (Str " ") inlineToOpenXML' opts (Span ("",["mark"],[]) ils) = withTextProp (mknode "w:highlight" [("w:val","yellow")] ()) $ inlinesToOpenXML opts ils@@ -773,16 +952,13 @@ inlineToOpenXML' opts (Span ("",["csl-indent"],[]) ils) = inlinesToOpenXML opts ils inlineToOpenXML' _ (Span (ident,["comment-start"],kvs) ils) = do- -- prefer the "id" in kvs, since that is the one produced by the docx- -- reader.- let ident' = fromMaybe ident (lookup "id" kvs)- kvs' = filter (("id" /=) . fst) kvs+ let ident' = fromMaybe ident (lookup "comment-id" kvs <|> lookup "id" kvs)+ kvs' = filter ((\x -> x /= "comment-id" && x /= "id") . fst) kvs modify $ \st -> st{ stComments = (("id",ident'):kvs', ils) : stComments st } return [ Elem $ mknode "w:commentRangeStart" [("w:id", ident')] () ] inlineToOpenXML' opts (Span (ident,["comment-end"],kvs) content) = do- -- prefer the "id" in kvs, since that is the one produced by the docx- -- reader.- let ident' = fromMaybe ident (lookup "id" kvs)+ -- now we use comment-id, but support id for legacy compat:+ let ident' = fromMaybe ident (lookup "comment-id" kvs <|> lookup "id" kvs) -- process nested content: see #8189 nestedContent <- inlinesToOpenXML opts content let thisCommentEnd =@@ -883,19 +1059,25 @@ when (mathType == DisplayMath) setFirstPara res <- (lift . lift) (convertMath writeOMML mathType str) case res of- Right r -> return [Elem $ fromXLElement r]+ Right r -> do+ tprops <- asks envTextProperties+ let r' = fromXLElement r+ mbhl = listToMaybe+ [ e | e <- Set.toList (otherElements tprops)+ , XML.qName (XML.elName e) == "highlight"+ , XML.qPrefix (XML.elName e) == Just "w" ]+ return [Elem (maybe r' (`highlightMathRuns` r') mbhl)] Left il -> inlineToOpenXML' opts il inlineToOpenXML' opts (Cite _ lst) = inlinesToOpenXML opts lst inlineToOpenXML' opts (Code attrs str) = do- let alltoktypes = [KeywordTok ..]- tokTypesMap <- mapM (\tt -> (,) tt <$> rStyleM (fromString $ show tt)) alltoktypes+ tokTypesMap <- gets stTokTypesMap let unhighlighted = (map Elem . intercalate [br]) `fmap` mapM formattedString (T.lines str) formatOpenXML _fmtOpts = intercalate [br] . map (map toHlTok) toHlTok (toktype,tok) = mknode "w:r" [] [ mknode "w:rPr" [] $- maybeToList (lookup toktype tokTypesMap)+ maybeToList (M.lookup toktype tokTypesMap) , mknode "w:t" [("xml:space","preserve")] tok ] let highlighted = case highlight (writerSyntaxMap opts) formatOpenXML attrs str of@@ -911,7 +1093,6 @@ inlineToOpenXML' opts (Note bs) = do notes <- gets stFootnotes notenum <- getUniqueId- oldFirstPara <- gets stFirstPara footnoteStyle <- rStyleM "Footnote Reference" let notemarker = mknode "w:r" [] [ mknode "w:rPr" [] footnoteStyle@@ -927,19 +1108,19 @@ , envInNote = True }) (withParaPropM (pStyleM "Footnote Text") $ blocksToOpenXML opts $ insertNoteRef bs)- modify $ \s -> s{ stFirstPara = oldFirstPara } let newnote = mknode "w:footnote" [("w:id", notenum)] contents modify $ \s -> s{ stFootnotes = newnote : notes } return [ Elem $ mknode "w:r" [] [ mknode "w:rPr" [] footnoteStyle , mknode "w:footnoteReference" [("w:id", notenum)] () ] ] -- internal link:-inlineToOpenXML' opts (Link _ txt (T.uncons -> Just ('#', xs),_)) = do+inlineToOpenXML' opts (Link _ txt (T.uncons -> Just ('#', xs),title)) = do contents <- withTextPropM (rStyleM "Hyperlink") $ inlinesToOpenXML opts txt return- [ Elem $ mknode "w:hyperlink" [("w:anchor", toBookmarkName xs)] contents ]+ [ Elem $ mknode "w:hyperlink"+ (("w:anchor", toBookmarkName xs) : tooltipAttr title) contents ] -- external link:-inlineToOpenXML' opts (Link _ txt (src,_)) = do+inlineToOpenXML' opts (Link _ txt (src,title)) = do contents <- withTextPropM (rStyleM "Hyperlink") $ inlinesToOpenXML opts txt extlinks <- gets stExternalLinks id' <- case M.lookup src extlinks of@@ -949,7 +1130,8 @@ modify $ \st -> st{ stExternalLinks = M.insert src i extlinks } return i- return [ Elem $ mknode "w:hyperlink" [("r:id",id')] contents ]+ return [ Elem $ mknode "w:hyperlink" (("r:id",id') : tooltipAttr title)+ contents ] inlineToOpenXML' opts (Image attr@(imgident, _, _) alt (src, title)) = do pageWidth <- asks envPrintWidth imgs <- gets stImages@@ -995,10 +1177,23 @@ (xpt,ypt) = desiredSizeInPoints opts attr (either (const def) id (imageSize opts img)) -- 12700 emu = 1 pt- pageWidthPt = case dimension Width attr of- Just (Percent a) -> pageWidth * floor (a * 127)- _ -> pageWidth * 12700- (xemu,yemu) = fitToPage (xpt * 12700, ypt * 12700) pageWidthPt+ pageWidthPt = fromIntegral pageWidth+ pageWidthEmu = pageWidth * 12700+ (xpt', ypt') =+ case (dimension Width attr, dimension Height attr) of+ (Just (Percent a), Just (Percent b))+ -> ((a / 100.0) * pageWidthPt, (b / 100.0) * pageWidthPt)+ -- note, should use pageHeightPt but we don't have this+ -- information.+ (Just (Percent a), _)+ -> ((a / 100.0) * pageWidthPt,+ (a / 100.0) * pageWidthPt * (ypt / xpt))+ (_, Just (Percent b))+ -> ((b / 100.0) * pageWidthPt * (xpt / ypt),+ (b / 100.0) * pageWidthPt)+ (_, _) -> (xpt, ypt)+ (xemu,yemu) = fitToPage (xpt' * 12700,+ ypt' * 12700) pageWidthEmu cNvPicPr = mknode "pic:cNvPicPr" [] $ mknode "a:picLocks" [("noChangeArrowheads","1") ,("noChangeAspect","1")] ()@@ -1041,7 +1236,7 @@ , mknode "wp:effectExtent" [("b","0"),("l","0"),("r","0"),("t","0")] () , mknode "wp:docPr"- [ ("descr", stringify alt)+ [ ("descr", stringifyInlines alt) , ("title", title) , ("id", docprid) , ("name","Picture")@@ -1102,15 +1297,25 @@ -- We want to clean all bidirection (bidi) and right-to-left (rtl) -- properties from the props first. This is because we don't want -- them to stack up.- let paraProps' = filter (\e -> (qName . elName) e /= "bidi") (otherElements paraProps)- textProps' = filter (\e -> (qName . elName) e /= "rtl") (otherElements textProps)+ let hasBidi = any (\e -> (qName . elName) e == "bidi") (otherElements paraProps)+ hasRtl = any (\e -> (qName . elName) e == "rtl") (otherElements textProps)+ paraProps' = Set.filter (\e -> (qName . elName) e /= "bidi")+ (otherElements paraProps)+ textProps' = Set.filter (\e -> (qName . elName) e /= "rtl")+ (otherElements textProps) paraStyle = styleElement paraProps textStyle = styleElement textProps- if isRTL+ if not isRTL && not hasBidi && not hasRtl+ -- fast path: LTR with no bidi/rtl props to remove, so the+ -- environment is unchanged; skip the 'local' rebuild.+ then x+ else if isRTL -- if we are going right-to-left, we (re?)add the properties. then flip local x $- \env -> env { envParaProperties = EnvProps paraStyle $ mknode "w:bidi" [] () : paraProps'- , envTextProperties = EnvProps textStyle $ mknode "w:rtl" [] () : textProps'+ \env -> env { envParaProperties = EnvProps paraStyle $+ Set.insert (mknode "w:bidi" [] ()) paraProps'+ , envTextProperties = EnvProps textStyle $+ Set.insert (mknode "w:rtl" [] ()) textProps' } else flip local x $ \env -> env { envParaProperties = EnvProps paraStyle paraProps' , envTextProperties = EnvProps textStyle textProps'@@ -1127,20 +1332,35 @@ return $ Elem bookmarkStart : contents ++ [Elem bookmarkEnd] -- Word imposes a 40 character limit on bookmark names and requires--- that they begin with a letter. So we just use a hash of the--- identifier when otherwise we'd have an illegal bookmark name.+-- that they begin with a letter or @_@ and contain only letters,+-- numbers or underscores. Bookmarks beginning with @_@ are+-- hidden in the user interface (and in particular hidden from screen+-- readers, which we want); these are to be used for cross-references.+-- When the id is otherwise illegal we use a hash of the identifier. toBookmarkName :: Text -> Text toBookmarkName s- | Just (c, _) <- T.uncons s- , isLetter c- , T.length s <= 40 = s- | otherwise = T.pack $ 'X' : drop 1 (show (hashWith SHA1 (fromText s)))+ | T.length s < 40+ , T.all (\c -> isAlphaNum c || c == '_') s+ = "_" <> s+ | otherwise = "_" <> T.pack (drop 1 (show (hashWith SHA1 (fromText s))))+ -- we drop 1 because a SHA1 is 40 characters and we need room for the `_` +-- A link's title is written as its ScreenTip (@w:tooltip@).+tooltipAttr :: Text -> [(Text, Text)]+tooltipAttr title = [("w:tooltip", title) | not (T.null title)]+ maxListLevel :: Int maxListLevel = 8 +-- Merge adjacent Strs, and any Space between two Strs, into a single+-- Str. Chunks are accumulated and concatenated all at once, to avoid+-- quadratic copying when a long Str/Space sequence (e.g. an entire+-- paragraph) collapses into one Str. convertSpace :: [Inline] -> [Inline]-convertSpace (Str x : Space : Str y : xs) = convertSpace (Str (x <> " " <> y) : xs)-convertSpace (Str x : Str y : xs) = convertSpace (Str (x <> y) : xs)-convertSpace (x:xs) = x : convertSpace xs-convertSpace [] = []+convertSpace (Str x : xs) = go [x] xs+ where+ go acc (Str y : ys) = go (y : acc) ys+ go acc (Space : Str y : ys) = go (y : " " : acc) ys+ go acc ys = Str (T.concat (reverse acc)) : convertSpace ys+convertSpace (x:xs) = x : convertSpace xs+convertSpace [] = []
@@ -16,6 +16,7 @@ module Text.Pandoc.Writers.Docx.StyleMap ( StyleMaps(..) , ParaStyleName , CharStyleName+ , TableStyleName , getStyleMaps , getStyleIdFromName , hasStyleName@@ -30,7 +31,10 @@ import Data.String import Data.Char (isSpace) -data StyleMaps = StyleMaps { smCharStyle :: CharStyleNameMap, smParaStyle :: ParaStyleNameMap }+data StyleMaps = StyleMaps { smCharStyle :: CharStyleNameMap+ , smParaStyle :: ParaStyleNameMap+ , smTableStyle :: TableStyleNameMap }+type TableStyleNameMap = M.Map TableStyleName TableStyle type ParaStyleNameMap = M.Map ParaStyleName ParStyle type CharStyleNameMap = M.Map CharStyleName CharStyle @@ -44,4 +48,6 @@ hasStyleName styleName = M.member styleName getStyleMaps :: Archive -> StyleMaps-getStyleMaps = uncurry StyleMaps . archiveToStyles' getStyleName getStyleName+getStyleMaps arch =+ let (s1, s2, s3) = archiveToStyles' getStyleName getStyleName getStyleName arch+ in StyleMaps s1 s2 s3
@@ -23,6 +23,7 @@ import Data.Array ( elems, (!), assocs, indices ) import Data.Text (Text) import Data.Maybe (catMaybes, fromMaybe)+import Data.String import Text.Pandoc.Definition ( ColSpec, Caption(Caption),@@ -42,6 +43,7 @@ WriterEnv(..), setFirstPara, pStyleM,+ tblStyleM, withParaProp, withParaPropM ) import Control.Monad.Reader (asks)@@ -110,11 +112,12 @@ let (gridCols, tblWattr) = tableLayout (elems colspecs) listLevel <- asks envListLevel let tblStyle = fromMaybe "Table" (lookup "custom-style" tableAttr)+ sty' <- tblStyleM (fromString $ T.unpack tblStyle) let indent = (listLevel + 1) * 720 let hasWidths = not $ all ((== ColWidthDefault) . snd) colspecs let tbl = mknode "w:tbl" [] ( mknode "w:tblPr" []- ( [ mknode "w:tblStyle" [("w:val",tblStyle)] (),+ ( [ sty', mknode "w:tblW" tblWattr () ] ++ [ mknode "w:jc" [("w:val","left")] () | indent > 0 ] ++ [ mknode "w:tblInd" [("w:w", tshow indent),("w:type","dxa")] ()@@ -178,7 +181,9 @@ rowwidth = round (fullrow * sum widths) :: Int widthToTwips w = floor (textwidth * w) :: Int mkGridCol w = mknode "w:gridCol" [("w:w", tshow (widthToTwips w))] ()- in if all (== 0) widths+ -- A "mixed" table with both default and given widths is treated as+ -- a table with all default widths.+ in if any (== 0) widths then ( replicate ncols $ mkGridCol (1.0 / fromIntegral ncols) , [ ("w:type", "auto"), ("w:w", "0")]) else ( map mkGridCol widths
@@ -17,6 +17,7 @@ , ListMarker (..) , listMarkerToId , pStyleM+ , tblStyleM , isStyle , setFirstPara , withParaProp@@ -27,6 +28,7 @@ import Control.Monad.Reader import Control.Monad.State.Strict import Data.Text (Text)+import Skylighting (TokenType) import Text.Pandoc.Class.PandocMonad (PandocMonad) import Text.Pandoc.Definition import Text.Pandoc.MIME (MimeType)@@ -67,14 +69,14 @@ data EnvProps = EnvProps{ styleElement :: Maybe Element- , otherElements :: [Element]- }+ , otherElements :: Set.Set Element+ } deriving (Show) instance Semigroup EnvProps where- EnvProps s es <> EnvProps s' es' = EnvProps (s <|> s') (es ++ es')+ EnvProps s es <> EnvProps s' es' = EnvProps (s <|> s') (es <> es') instance Monoid EnvProps where- mempty = EnvProps Nothing []+ mempty = EnvProps Nothing mempty mappend = (<>) data WriterEnv = WriterEnv@@ -87,7 +89,7 @@ , envInNote :: Bool , envChangesAuthor :: Text , envChangesDate :: Text- , envPrintWidth :: Integer+ , envPrintWidth :: Integer -- in points , envLang :: Maybe Text , envSectPr :: Maybe Element }@@ -120,6 +122,9 @@ , stInsId :: Int , stDelId :: Int , stStyleMaps :: StyleMaps+ , stTokTypesMap :: M.Map TokenType Element+ -- ^ cached rStyle element for each highlighting token type;+ -- computed once from stStyleMaps at the start of writing , stFirstPara :: Bool , stFirstSectionHeader :: Bool -- ^ True until first section header is processed , stNumIdUsed :: Bool -- ^ True if the current numId (envListNumId) has been used.@@ -145,7 +150,8 @@ , stExampleId = Nothing , stInsId = 1 , stDelId = 1- , stStyleMaps = StyleMaps M.empty M.empty+ , stStyleMaps = StyleMaps M.empty M.empty M.empty+ , stTokTypesMap = M.empty , stFirstPara = False , stFirstSectionHeader = True , stNumIdUsed = False@@ -189,12 +195,20 @@ withParaProp :: PandocMonad m => Element -> WS m a -> WS m a withParaProp d p = local (\env -> env {envParaProperties = ep <> envParaProperties env}) p- where ep = if isStyle d then EnvProps (Just d) [] else EnvProps Nothing [d]+ where ep = if isStyle d+ then EnvProps (Just d) mempty+ else EnvProps Nothing (Set.singleton d) withParaPropM :: PandocMonad m => WS m Element -> WS m a -> WS m a withParaPropM md p = do d <- md withParaProp d p++tblStyleM :: (PandocMonad m) => TableStyleName -> WS m XML.Element+tblStyleM styleName = do+ tStyleMap <- gets (smTableStyle . stStyleMaps)+ let sty' = getStyleIdFromName styleName tStyleMap+ return $ mknode "w:tblStyle" [("w:val", fromStyleId sty')] () isStyle :: Element -> Bool isStyle e = isElem [] "w" "rStyle" e ||
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE PatternGuards #-} {-# LANGUAGE OverloadedStrings #-}@@ -22,10 +21,11 @@ import Control.Monad.Except (catchError, throwError) import Control.Monad.State.Strict (State, StateT, evalState, evalStateT, get, gets, lift, modify)+import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as B import qualified Data.ByteString.Lazy.Char8 as B8 import Data.Char (isAlphaNum, isAscii, isDigit)-import Data.List (isInfixOf, isPrefixOf)+import Data.List (isPrefixOf) import qualified Data.Map as M import Data.Maybe (fromMaybe, isNothing, mapMaybe, isJust, catMaybes) import qualified Data.Set as Set@@ -38,7 +38,6 @@ import Text.Pandoc.Writers.Shared (ensureValidXmlIdentifiers) import Data.Tree (Tree(..)) import Text.Pandoc.Class (PandocMonad, report)-import qualified Text.Pandoc.Class.PandocPure as P import Text.Pandoc.Data (readDataFile) import qualified Text.Pandoc.Class.PandocMonad as P import Data.Time@@ -52,10 +51,10 @@ ObfuscationMethod (NoObfuscation), WrapOption (..), WriterOptions (..)) import Text.Pandoc.Shared (normalizeDate, renderTags',- stringify, uniqueIdent, tshow)+ stringify, stringifyInlines, uniqueIdent, tshow) import qualified Text.Pandoc.UTF8 as UTF8 import Text.Pandoc.UUID (getRandomUUID)-import Text.Pandoc.Walk (walk, walkM)+import Text.Pandoc.Walk (query, walk, walkM) import Text.Pandoc.Writers.HTML (writeHtmlStringForEPUB) import Text.Printf (printf) import Text.Pandoc.XML.Light@@ -204,18 +203,19 @@ return m{ epubLanguage = localeLang } else return m let fixDate m =- if null (epubDate m)- then do+ -- drop dates we could not normalize (dc:date must not be empty)+ case filter (not . T.null . dateText) (epubDate m) of+ [] -> do currentTime <- lift P.getTimestamp return $ m{ epubDate = [ Date{ dateText = showDateTimeISO8601 currentTime , dateEvent = Nothing } ] }- else return m+ ds -> return m{ epubDate = ds } let addAuthor m = if any (\c -> creatorRole c == Just "aut") $ epubCreator m then return m else do- let authors' = map stringify $ docAuthors meta+ let authors' = map stringifyInlines $ docAuthors meta let toAuthor name = Creator{ creatorText = name , creatorRole = Just "aut" , creatorFileAs = Nothing }@@ -280,7 +280,7 @@ metaValueToString :: MetaValue -> Text metaValueToString (MetaString s) = s-metaValueToString (MetaInlines ils) = stringify ils+metaValueToString (MetaInlines ils) = stringifyInlines ils metaValueToString (MetaBlocks bs) = stringify bs metaValueToString (MetaBool True) = "true" metaValueToString (MetaBool False) = "false"@@ -483,7 +483,7 @@ [] -> case epubTitle metadata of [] -> "UNTITLED" (x:_) -> titleText x- x -> stringify x+ x -> stringifyInlines x -- stylesheet stylesheets <- case epubStylesheets metadata of@@ -589,14 +589,11 @@ [("page-progression-direction", "rtl")] _ -> [] - -- incredibly inefficient (TODO):- let containsMathML ent = epub3 &&- "<math" `isInfixOf`- B8.unpack (fromEntry ent)- let containsSVG ent = epub3 &&- "<svg" `isInfixOf`- B8.unpack (fromEntry ent)- let props ent = ["mathml" | containsMathML ent] ++ ["svg" | containsSVG ent]+ let props ent+ | epub3 = let contents = B.toStrict (fromEntry ent)+ in ["mathml" | "<math" `BS.isInfixOf` contents] +++ ["svg" | "<svg" `BS.isInfixOf` contents]+ | otherwise = [] let chapterNode ent = unode "item" ! ([("id", toId $ makeRelative epubSubdir@@ -859,7 +856,7 @@ let secnum' = case secNumber secinfo of Just t -> t <> " " Nothing -> ""- let title' = secnum' <> stringify (secTitle secinfo)+ let title' = secnum' <> stringifyInlines (secTitle secinfo) return $ Just $ unode "navPoint" ! [("id", "navPoint-" <> tshow n)] $ [ unode "navLabel" $ unode "text" title'@@ -869,7 +866,7 @@ let tpNode = unode "navPoint" ! [("id", "navPoint-0")] $ [ unode "navLabel" $ unode "text"- (stringify $ docTitle' meta)+ (stringifyInlines $ docTitle' meta) , unode "content" ! [("src", "text/title_page.xhtml")] $ () ] @@ -913,8 +910,7 @@ -> StateT EPUBState m Entry createNavEntry opts meta metadata vars cssvars writeHtml tocTitle version (Node _ secs) = do- let mkItem :: Tree SecInfo -> State Int (Maybe Element)- mkItem (Node secinfo subsecs)+ let mkItem (Node secinfo subsecs) | secLevel secinfo > writerTOCDepth opts = return Nothing | otherwise = do n <- get@@ -929,13 +925,14 @@ let clean (Link _ ils _) = Span ("", [], []) ils clean (Note _) = Str "" clean x = x- let titRendered = case P.runPure- (writeHtmlStringForEPUB version- opts{ writerTemplate = Nothing }- (Pandoc nullMeta- [Plain $ walk clean title'])) of- Left _ -> stringify title'- Right x -> x+ -- render in the host monad, so that a translation table+ -- loaded for one title is reused for the others:+ titRendered <- lift $ catchError+ (writeHtmlStringForEPUB version+ opts{ writerTemplate = Nothing }+ (Pandoc nullMeta+ [Plain $ walk clean title']))+ (\_ -> return $ stringifyInlines title') let titElements = either (const []) id $ parseXMLContents (TL.fromStrict titRendered) @@ -949,7 +946,7 @@ (_:_) -> [unode "ol" ! [("class","toc")] $ subs] let navtag = if version == EPUB3 then "nav" else "div"- let tocBlocks = evalState (catMaybes <$> mapM mkItem secs) 1+ tocBlocks <- lift $ evalStateT (catMaybes <$> mapM mkItem secs) (1 :: Int) let navBlocks = [RawBlock (Format "html") $ showElement $ -- prettyprinting introduces bad spaces unode navtag ! ([("epub:type","toc") | version == EPUB3] ++@@ -1261,8 +1258,10 @@ -- Add identifiers to any headers without them. addIdentifiers :: WriterOptions -> [Block] -> [Block]-addIdentifiers opts bs = evalState (mapM go bs) Set.empty- where go (Header n (ident,classes,kvs) ils) = do+addIdentifiers opts bs = evalState (mapM go bs) (Set.fromList (query getId bs))+ where getId (Header _ (ident,_,_) _) = [ident]+ getId _ = []+ go (Header n (ident,classes,kvs) ils) = do ids <- get let ident' = if T.null ident then uniqueIdent (writerExtensions opts) ils ids
@@ -38,7 +38,7 @@ import Text.Pandoc.Logging import Text.Pandoc.Options (MathMethod (..), WriterOptions (..), def) import Text.Pandoc.Shared (blocksToInlines, capitalize, orderedListMarkers,- makeSections, tshow, stringify)+ makeSections, tshow, stringifyInlines) import Text.Pandoc.Walk (walk) import Text.Pandoc.Writers.Shared (lookupMetaString, toLegacyTable, ensureValidXmlIdentifiers)@@ -118,7 +118,7 @@ im <- insertImage InlineImage img return [el "coverpage" im] coverpage <- case lookupMeta "cover-image" meta' of- Just (MetaInlines ils) -> coverimage (stringify ils)+ Just (MetaInlines ils) -> coverimage (stringifyInlines ils) Just (MetaString s) -> coverimage s _ -> return [] return $ el "description"
@@ -48,7 +48,7 @@ import Text.Pandoc.URI (urlEncode) import Numeric (showHex) import Text.DocLayout (render, literal, Doc)-import Text.Blaze.Internal (MarkupM (Empty), customLeaf, customParent)+import Text.Blaze.Internal (MarkupM (Append, Empty), customLeaf, customParent) import Text.DocTemplates (FromContext (lookupContext), Context (..), Val(..)) import qualified Text.DocTemplates.Internal as DT import Text.Blaze.Html hiding (contents)@@ -125,20 +125,24 @@ strToHtml :: Text -> Html strToHtml t- | T.any isSpecial t =- let !x = L.foldl' go mempty $ T.groupBy samegroup t- in x+ | T.any isSpecial t = go t | otherwise = toHtml t where- samegroup c d = d == '\xFE0E' || not (isSpecial c || isSpecial d) isSpecial '\'' = True isSpecial '"' = True isSpecial c = needsVariationSelector c- go h "\'" = h <> preEscapedString "\'"- go h "\"" = h <> preEscapedString "\""- go h txt | T.length txt == 1 && T.all needsVariationSelector txt- = h <> preEscapedString (T.unpack txt <> "\xFE0E")- go h txt = h <> toHtml txt+ go s =+ let (plain, rest) = T.break isSpecial s+ html = if T.null plain then mempty else toHtml plain+ in case T.uncons rest of+ Nothing -> html+ Just ('\'', rest') -> html <> preEscapedText "'" <> go rest'+ Just ('"', rest') -> html <> preEscapedText "\"" <> go rest'+ Just (c, rest')+ -- don't add a variation selector if one is already there:+ | T.take 1 rest' == "\xFE0E" -> html <> toHtml c <> go rest'+ | otherwise -> html <> preEscapedText (T.pack [c, '\xFE0E'])+ <> go rest' -- See #5469: this prevents iOS from substituting emojis. needsVariationSelector :: Char -> Bool@@ -150,6 +154,14 @@ nl :: Html nl = preEscapedString "\n" +-- | True if the markup contains no content at all. 'mconcat' and+-- '<>' on 'MarkupM' build 'Append' nodes without collapsing empty+-- markup, so simply matching on 'Empty' is not enough.+isEmptyMarkup :: MarkupM a -> Bool+isEmptyMarkup (Empty _) = True+isEmptyMarkup (Append x y) = isEmptyMarkup x && isEmptyMarkup y+isEmptyMarkup _ = False+ -- | Convert Pandoc document to Html 5 string. writeHtml5String :: PandocMonad m => WriterOptions -> Pandoc -> m Text writeHtml5String = writeHtmlString'@@ -286,7 +298,7 @@ (fmap layoutMarkup . blockListToHtml opts) (fmap layoutMarkup . inlineListToHtml opts) meta- let stringifyHTML = escapeStringForXML . stringify+ let stringifyHTML = escapeStringForXML . stringifyInlines let authsMeta = map (literal . stringifyHTML) $ docAuthors meta let dateMeta = stringifyHTML $ docDate meta let descriptionMeta = literal $ escapeStringForXML $@@ -352,7 +364,7 @@ ] nl H.link ! A.rel "stylesheet" !- A.href (toValue $ toURI html5 url <> "katex.min.css")+ A.href (toValue $ toURI html5 $ url <> "katex.min.css") _ -> mempty let mCss :: Maybe [Text] = lookupContext "css" metadata@@ -467,7 +479,7 @@ defField "slidy-url" ("https://www.w3.org/Talks/Tools/Slidy2" :: Doc Text) . defField "slideous-url" ("slideous" :: Doc Text) .- defField "revealjs-url" ("https://unpkg.com/reveal.js@^5" :: Doc Text) $+ defField "revealjs-url" ("https://unpkg.com/reveal.js@^6" :: Doc Text) $ defField "s5-url" ("s5/default" :: Doc Text) . defField "table-caption-below" (writerTableCaptionPosition opts == CaptionBelow) .@@ -572,7 +584,7 @@ let container x | html5 , epubVersion == Just EPUB3- = H5.section ! A.id (fromString idName)+ = H5.section ! prefixedId opts (fromString idName) ! A.class_ className ! customAttribute "epub:type" "footnotes" $ x | html5@@ -633,16 +645,24 @@ (linkText, altText) = if txt == T.drop 7 s' -- autolink then ("e", name' <> " at " <> domain')- else ("'" <> obfuscateString txt <> "'",+ else ("'" <>+ T.replace "</" "<\\/" (obfuscateMarkup txt) <> "'", txt <> " (" <> name' <> " at " <> domain' <> ")")- (_, classNames, _) = attr+ (ident, classNames, kvs) = attr classNamesStr = T.concat $ map (" "<>) classNames+ otherAttrsStr = T.concat $+ [ " id=\"" <>+ escapeJSAttrVal (writerIdentifierPrefix opts <> ident) <>+ "\"" | not (T.null ident) ] +++ [ " " <> k <> "=\"" <> escapeJSAttrVal v <> "\""+ | (k, v) <- kvs ] in case meth of ReferenceObfuscation ->- -- need to use preEscapedString or &'s are escaped to & in URL- return $- preEscapedText $ "<a href=\"" <> obfuscateString s'- <> "\" class=\"email\">" <> obfuscateString txt <> "</a>"+ -- preEscaped is needed or the &'s in the+ -- entity-obfuscated text are escaped to &+ addAttrs opts (ident, "email":classNames, kvs) $+ H.a ! A.href (preEscapedToValue $ obfuscateString s')+ $ preEscapedText $ obfuscateMarkup txt JavascriptObfuscation -> return $ (H.script ! A.type_ "text/javascript" $@@ -650,12 +670,12 @@ obfuscateString domain <> "';a='" <> at' <> "';n='" <> obfuscateString name' <> "';e=n+a+h;\n" <> "document.write('<a h'+'ref'+'=\"ma'+'ilto'+':'+e+'\" clas'+'s=\"em' + 'ail" <>- classNamesStr <> "\">'+" <>+ classNamesStr <> "\"" <> otherAttrsStr <> ">'+" <> linkText <> "+'<\\/'+'a'+'>');\n// -->\n")) >>- H.noscript (preEscapedText $ obfuscateString altText)+ H.noscript (preEscapedText $ obfuscateMarkup altText) _ -> throwError $ PandocSomeError $ "Unknown obfuscation method: " <> tshow meth _ -> addAttrs opts attr $ H.a ! A.href (toValue $ toURI html5 s)- $ toHtml txt -- malformed email+ $ preEscapedText txt -- malformed email -- | Obfuscate character as entity. obfuscateChar :: Char -> Text@@ -668,6 +688,37 @@ obfuscateString :: Text -> Text obfuscateString = T.concatMap obfuscateChar . fromEntities +-- | Obfuscate the character data in a rendered HTML fragment,+-- leaving the tags themselves intact.+obfuscateMarkup :: Text -> Text+obfuscateMarkup t+ | T.null t = ""+ | otherwise =+ let (chars, rest) = T.break (== '<') t+ (tag, rest') = T.break (== '>') rest+ in obfuscateString chars <>+ case T.uncons rest' of+ Just ('>', rest'') -> tag <> ">" <> obfuscateMarkup rest''+ _ -> tag -- unterminated tag; emit as is++-- | Escape text for an HTML attribute value that is embedded in a+-- single-quoted JavaScript string literal (as used in+-- 'JavascriptObfuscation'). Everything problematic is replaced+-- with an entity, which the HTML parser decodes when the string is+-- written to the document.+escapeJSAttrVal :: Text -> Text+escapeJSAttrVal = T.concatMap $ \c ->+ case c of+ '&' -> "&"+ '<' -> "<"+ '>' -> ">"+ '"' -> """+ '\'' -> "'"+ '\\' -> "\"+ '\n' -> " "+ '\r' -> " "+ _ -> T.singleton c+ -- | Create HTML tag with attributes. tagWithAttributes :: WriterOptions -> Bool -- ^ True for HTML5@@ -703,7 +754,7 @@ addAttr html5 mbEpubVersion x y | T.null x = id -- see #7546 | html5- = if (x `Set.member` (html5Attributes <> rdfaAttributes)+ = if (x `Set.member` html5AttrsPlusRdfa && x /= "label") -- #10048 || T.any (== ':') x -- e.g. epub: namespace || "data-" `T.isPrefixOf` x@@ -711,12 +762,19 @@ then (customAttribute (textTag x) (toValue y) :) else (customAttribute (textTag ("data-" <> x)) (toValue y) :) | mbEpubVersion == Just EPUB2- , not (x `Set.member` (html4Attributes <> rdfaAttributes) ||+ , not (x `Set.member` html4AttrsPlusRdfa || "xml:" `T.isPrefixOf` x) = id | otherwise = (customAttribute (textTag x) (toValue y) :) +-- Top-level constants, so that the set unions are computed only once.+html5AttrsPlusRdfa :: Set.Set Text+html5AttrsPlusRdfa = html5Attributes <> rdfaAttributes++html4AttrsPlusRdfa :: Set.Set Text+html4AttrsPlusRdfa = html4Attributes <> rdfaAttributes+ attrsToHtml :: PandocMonad m => WriterOptions -> Attr -> StateT WriterState m [Attribute] attrsToHtml opts (id',classes',keyvals) = do@@ -764,9 +822,9 @@ inlineToHtml opts (Image attr txt (src, tit)) _ -> do contents <- inlineListToHtml opts lst- case contents of- Empty _ | not (isEnabled Ext_empty_paragraphs opts) -> return mempty- _ -> return $ H.p contents+ if isEmptyMarkup contents && not (isEnabled Ext_empty_paragraphs opts)+ then return mempty+ else return $ H.p contents blockToHtmlInner opts (LineBlock lns) = do htmlLines <- inlineListToHtml opts $ intercalate [LineBreak] lns return $ H.div ! A.class_ "line-block" $ htmlLines@@ -796,17 +854,18 @@ let inDiv' zs = RawBlock (Format "html") ("<div class=\"" <> fragmentClass <> "\">") : (zs ++ [RawBlock (Format "html") "</div>"])- let breakOnPauses zs- | slide = case splitBy isPause zs of+ let breakOnPauses zs = case splitBy isPause zs of [] -> [] y:ys -> y ++ concatMap inDiv' ys- | otherwise = zs+ let breakPauses = if slide+ then walk breakOnPauses+ else id -- avoid a pointless traversal let (titleBlocks, innerSecs) = if titleSlide -- title slides have no content of their own then let (as, bs) = break isSec xs- in (walk breakOnPauses as, bs)- else ([], walk breakOnPauses xs)+ in (breakPauses as, bs)+ else ([], breakPauses xs) let secttag = if html5 then H5.section else H.div@@ -901,8 +960,6 @@ classes' = case slideVariant of NoSlides -> classes _ -> filter (\k -> k /= "incremental" && k /= "nonincremental") classes- let paraToPlain (Para ils) = Plain ils- paraToPlain x = x let bs' = if "csl-entry" `elem` classes' then walk paraToPlain bs else bs@@ -910,7 +967,7 @@ then -- we don't use blockListToHtml because it inserts -- a newline between the column divs, which throws -- off widths! see #4028- mconcat <$> mapM (blockToHtml opts) bs'+ mconcat <$> mapM (blockToHtml opts') bs' else blockListToHtml opts' bs' let contents' = nl >> contents >> nl let (divtag, classes'') = if html5 && "section" `elem` classes'@@ -1110,7 +1167,7 @@ else foldl (!) H.div (A.class_ "float" : figAttrs) innards where captionIsAlt capt [Plain [Image (_, _, kv) desc _]] =- let alt = fromMaybe (stringify desc) $ lookup "alt" kv+ let alt = fromMaybe (stringifyInlines desc) $ lookup "alt" kv in stringify capt == alt captionIsAlt _ _ = False @@ -1169,7 +1226,7 @@ let attr' = case lookup "style" kvs of Nothing | totalWidth < 1 && totalWidth > 0 -> (ident,classes, ("style","width:" <>- T.pack (show (round (totalWidth * 100) :: Int))+ T.pack (show (truncate (totalWidth * 100) :: Int)) <> "%;"):kvs) _ -> attr addAttrs opts attr' $ H.table $ do@@ -1378,10 +1435,8 @@ blockListToHtml :: PandocMonad m => WriterOptions -> [Block] -> StateT WriterState m Html blockListToHtml opts lst =- mconcat . intersperse (nl) . filter nonempty+ mconcat . intersperse (nl) . filter (not . isEmptyMarkup) <$> mapM (blockToHtml opts) lst- where nonempty (Empty _) = False- nonempty _ = True -- | Convert list of Pandoc inline elements to HTML. inlineListToHtml :: PandocMonad m => WriterOptions -> [Inline] -> StateT WriterState m Html@@ -1601,7 +1656,7 @@ else link' ! A.title (toValue tit) (Image attr@(_, _, attrList) txt (s, tit)) -> do epubVersion <- gets stEPUBVersion- let alternate = stringify txt+ let alternate = stringifyInlines txt slideVariant <- gets stSlideVariant let isReveal = slideVariant == RevealJsSlides attrs <- imgAttrsToHtml opts attr@@ -1788,7 +1843,7 @@ intrinsicEventsHTML4 :: [Text] intrinsicEventsHTML4 = [ "onclick", "ondblclick", "onmousedown", "onmouseup", "onmouseover"- , "onmouseout", "onmouseout", "onkeypress", "onkeydown", "onkeyup"]+ , "onmousemove", "onmouseout", "onkeypress", "onkeydown", "onkeyup"] -- | Check to see if Format is valid HTML
@@ -16,7 +16,7 @@ module Text.Pandoc.Writers.Haddock (writeHaddock) where import Control.Monad (zipWithM) import Control.Monad.State.Strict- ( StateT, MonadState(get), modify, evalStateT )+ ( StateT, MonadState(get), gets, modify, evalStateT ) import Data.Char (isAlphaNum) import Data.Default import Data.Text (Text)@@ -28,7 +28,6 @@ import Text.DocLayout import Text.Pandoc.Shared import Text.Pandoc.URI-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Writers.Shared type Notes = [[Block]]@@ -46,32 +45,35 @@ pandocToHaddock :: PandocMonad m => WriterOptions -> Pandoc -> StateT WriterState m Text pandocToHaddock opts (Pandoc meta blocks) = do- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing body <- blockListToHaddock opts blocks- st <- get- notes' <- notesToHaddock opts (reverse $ stNotes st)+ notes' <- notesToHaddock opts let main = body <> (if isEmpty notes' then empty else blankline <> notes') metadata <- metaToContext opts (blockListToHaddock opts) (fmap chomp . inlineListToHaddock opts) meta let context = defField "body" main metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main --- | Return haddock representation of notes.+-- | Return haddock representation of the collected notes. Rendering a+-- note may add further notes to the state (notes nested inside+-- notes); keep going until all of them have been rendered. notesToHaddock :: PandocMonad m- => WriterOptions -> [[Block]] -> StateT WriterState m (Doc Text)-notesToHaddock opts notes =- if null notes- then return empty- else do- contents <- blockToHaddock opts $ OrderedList (1,DefaultStyle,DefaultDelim) notes- return $ text "#notes#" <> blankline <> contents+ => WriterOptions -> StateT WriterState m (Doc Text)+notesToHaddock opts = do+ contents <- go 0+ return $ if null contents+ then empty+ else text "#notes#" <> blankline <> vcat contents+ where+ go done = do+ notes <- gets (drop done . reverse . stNotes)+ if null notes+ then return []+ else do+ contents <- blockToHaddock opts $+ OrderedList (done + 1, DefaultStyle, DefaultDelim) notes+ (contents :) <$> go (done + length notes) -- | Escape special characters for Haddock. escapeString :: Text -> Text@@ -170,7 +172,7 @@ -> StateT WriterState m (Doc Text) orderedListItemToHaddock opts marker items = do contents <- blockListToHaddock opts items- let sps = case T.length marker - writerTabStop opts of+ let sps = case writerTabStop opts - T.length marker of n | n > 0 -> text $ replicate n ' ' _ -> text " " let start = literal marker <> sps@@ -264,7 +266,7 @@ inlineToHaddock _ Space = return space inlineToHaddock opts (Cite _ lst) = inlineListToHaddock opts lst inlineToHaddock _ (Link _ txt (src, _)) = do- let linktext = literal $ escapeString $ stringify txt+ let linktext = literal $ escapeString $ stringifyInlines txt let useAuto = isURI src && case txt of [Str s] | escapeURI s == src -> True
@@ -138,9 +138,7 @@ writeICML :: PandocMonad m => WriterOptions -> Pandoc -> m Text writeICML opts doc = do let Pandoc meta blocks = ensureValidXmlIdentifiers doc- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing+ let colwidth = wrapColumns opts renderBlockMeta f s = fst <$> runStateT (f opts [] s) defaultWriterState renderInlineMeta f s = fst <$> runStateT (f opts [] "" s) defaultWriterState metadata <- metaToContext opts
@@ -170,9 +170,7 @@ (Header lev (ident,hcls,hkvs) hils : rs) : bs) splitBackBlocks b (fs, bs) = (b:fs, bs) let (bodyblocks, backblocks) = foldr splitBackBlocks ([],[]) blocks- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing+ let colwidth = wrapColumns opts metadata <- metaToContext opts (blocksToJATS opts . makeSections False (Just startLvl)) (fmap chomp . inlinesToJATS opts)@@ -240,11 +238,6 @@ then inTags True "p" [("specific-use","wrapper")] inner else inner --- | Auxiliary function to convert Plain block to Para.-plainToPara :: Block -> Block-plainToPara (Plain x) = Para x-plainToPara x = x- -- | Convert a list of pairs of terms and definitions into a list of -- JATS varlistentrys. deflistItemsToJATS :: PandocMonad m@@ -302,7 +295,7 @@ codeAttr :: WriterOptions -> Attr -> (Text, [(Text, Text)]) codeAttr opts (ident,classes,kvs) = (lang, attr) where- attr = [("id", escapeNCName ident) | not (T.null ident)] +++ attr = idAttr opts ident ++ [("language",lang) | not (T.null lang)] ++ [(k,v) | (k,v) <- kvs, k `elem` ["code-type", "code-version", "executable",@@ -310,6 +303,12 @@ "platforms", "position", "specific-use"]] lang = languageFor opts classes +-- | The id attribute for an element, with the identifier prefix added.+idAttr :: WriterOptions -> Text -> [(Text, Text)]+idAttr opts ident =+ [("id", writerIdentifierPrefix opts <> escapeNCName ident)+ | not (T.null ident)]+ -- <break/> is only allowed as a direct child of <td> or <title> or -- <article-title> fixLineBreak :: Inline -> Inline@@ -319,10 +318,8 @@ -- | Convert a Pandoc block element to JATS. blockToJATS :: PandocMonad m => WriterOptions -> Block -> JATS m (Doc Text) blockToJATS opts (Div (id',"section":_,kvs) (Header _lvl (_,_,hkvs) ils : xs)) = do- let idAttr = [ ("id", writerIdentifierPrefix opts <> escapeNCName id')- | not (T.null id')] let otherAttrs = ["sec-type", "specific-use"]- let attribs = idAttr ++ [(k,v) | (k,v) <- kvs, k `elem` otherAttrs]+ let attribs = idAttr opts id' ++ [(k,v) | (k,v) <- kvs, k `elem` otherAttrs] title' <- inlinesToJATS opts (map fixLineBreak ils) let label = if writerNumberSections opts then@@ -336,7 +333,7 @@ inTagsSimple "title" title' $$ contents -- Bibliography reference: blockToJATS opts (Div (ident,_,_) [Para lst]) | "ref-" `T.isPrefixOf` ident =- inTags True "ref" [("id", escapeNCName ident)] .+ inTags True "ref" (idAttr opts ident) . inTagsSimple "mixed-citation" <$> inlinesToJATS opts lst blockToJATS opts (Div ("refs",_,_) xs) = do@@ -353,7 +350,7 @@ return $ inTagsIndented "ref-list" contents blockToJATS opts (Div (ident,[cls],kvs) bs) | cls `elem` ["fig", "caption", "table-wrap"] = do contents <- blocksToJATS opts bs- let attr = [("id", escapeNCName ident) | not (T.null ident)] +++ let attr = idAttr opts ident ++ [("xml:lang",l) | ("lang",l) <- kvs] ++ [(k,v) | (k,v) <- kvs, k `elem` ["specific-use", "content-type", "orientation", "position"]]@@ -361,7 +358,7 @@ blockToJATS opts (Div (ident,_,kvs) bs) = do contents <- blocksToJATS opts bs -- Attributes that are allowed on both @<p>@ and @<boxed-text>@ elements- let generic_attr = [("id", escapeNCName ident) | not (T.null ident)] +++ let generic_attr = idAttr opts ident ++ [("xml:lang",l) | ("lang",l) <- kvs] ++ [(k,v) | (k,v) <- kvs, k `elem` ["specific-use", "content-type"]]@@ -380,10 +377,10 @@ title' <- inlinesToJATS opts (map fixLineBreak title) return $ inTagsSimple "title" title' -- Special cases for bare images, which are rendered as graphics-blockToJATS _opts (Plain [Image attr alt tgt]) =- return $ graphic attr alt tgt-blockToJATS _opts (Para [Image attr alt tgt]) =- return $ graphic attr alt tgt+blockToJATS opts (Plain [Image attr alt tgt]) =+ return $ graphic opts attr alt tgt+blockToJATS opts (Para [Image attr alt tgt]) =+ return $ graphic opts attr alt tgt -- No Plain, everything needs to be in a block-level tag blockToJATS opts (Plain lst) = blockToJATS opts (Para lst) blockToJATS opts (Para lst) =@@ -445,7 +442,7 @@ -- Remove the alt text from images if it's the same as the caption text. let unsetAltIfDupl = \case Image attr alt tgt- | stringify alt == stringify longcapt -> Image attr [] tgt+ | stringifyInlines alt == stringify longcapt -> Image attr [] tgt inline -> inline capt <- if null longcapt then pure empty@@ -476,7 +473,7 @@ else ("fig" , ["fig-type", "orientation", "position", "specific-use"] )- let xmlattr = [("id", escapeNCName ident) | not (T.null ident)] +++ let xmlattr = idAttr opts ident ++ [(k,v) | (k,v) <- kvs , k `elem` allowedAttributes] return $ inTags True tag xmlattr $ capt $$ children@@ -487,7 +484,7 @@ where fixCitations [] = [] fixCitations (x:xs) | needsFixing x =- x : Str (stringify ys) : fixCitations zs+ x : Str (stringifyInlines ys) : fixCitations zs where needsFixing (RawInline (Format "jats") z) = "<pub-id pub-id-type=" `T.isPrefixOf` z@@ -544,19 +541,20 @@ let notenum = case notes of (n, _):_ -> n + 1 [] -> 1- thenote <- inTags True "fn" [("id", "fn" <> tshow notenum)]+ thenote <- inTags True "fn" (idAttr opts ("fn" <> tshow notenum)) . (inTagsSimple "label" (literal $ tshow notenum) <>) <$> wrappedBlocksToJATS (not . isPara) opts (walk demoteHeaderAndRefs contents) modify $ \st -> st{ jatsNotes = (notenum, thenote) : notes } return $ inTags False "xref" [("ref-type", "fn"),- ("rid", "fn" <> tshow notenum)]+ ("rid", writerIdentifierPrefix opts <>+ "fn" <> tshow notenum)] $ text (show notenum) inlineToJATS opts (Cite _ lst) = inlinesToJATS opts lst inlineToJATS opts (Span (ident,classes,kvs) ils) = do contents <- inlinesToJATS opts ils- let commonAttr = [("id", escapeNCName ident) | not (T.null ident)] +++ let commonAttr = idAttr opts ident ++ [("xml:lang",l) | ("lang",l) <- kvs] ++ [(k,v) | (k,v) <- kvs, k `elem` ["alt", "specific-use"]] -- A named-content element is a good fit for spans, but requires a@@ -615,9 +613,9 @@ return $ inTagsSimple "email" $ literal (escapeStringForXML email) inlineToJATS opts (Link (ident,_,kvs) txt (T.uncons -> Just ('#', src), _)) = do let attr = mconcat- [ [("id", escapeNCName ident) | not (T.null ident)]- , [("alt", stringify txt) | not (null txt)]- , [("rid", escapeNCName src)]+ [ idAttr opts ident+ , [("alt", stringifyInlines txt) | not (null txt)]+ , [("rid", writerIdentifierPrefix opts <> escapeNCName src)] , [(k,v) | (k,v) <- kvs, k `elem` ["ref-type", "specific-use"]] , [("ref-type", "bibr") | "ref-" `T.isPrefixOf` src] ]@@ -627,7 +625,7 @@ contents <- inlinesToJATS opts txt return $ inTags False "xref" attr contents inlineToJATS opts (Link (ident,_,kvs) txt (src, tit)) = do- let attr = [("id", escapeNCName ident) | not (T.null ident)] +++ let attr = idAttr opts ident ++ [("ext-link-type", "uri"), ("xlink:href", src)] ++ [("xlink:title", tit) | not (T.null tit)] ++@@ -637,23 +635,23 @@ "xlink:type"]] contents <- inlinesToJATS opts txt return $ inTags False "ext-link" attr contents-inlineToJATS _ (Image attr alt tgt) = do- let elattr = graphicAttr attr alt tgt+inlineToJATS opts (Image attr alt tgt) = do+ let elattr = graphicAttr opts attr alt tgt return $ case altToJATS alt of Nothing -> selfClosingTag "inline-graphic" elattr Just altTag -> inTags True "inline-graphic" elattr altTag -graphic :: Attr -> [Inline] -> Target -> (Doc Text)-graphic attr alt tgt =- let elattr = graphicAttr attr alt tgt+graphic :: WriterOptions -> Attr -> [Inline] -> Target -> (Doc Text)+graphic opts attr alt tgt =+ let elattr = graphicAttr opts attr alt tgt in case altToJATS alt of Nothing -> selfClosingTag "graphic" elattr Just altTag -> inTags True "graphic" elattr altTag -graphicAttr :: Attr -> [Inline] -> Target -> [(Text, Text)]-graphicAttr (ident, _, kvs) _alt (src, tit) =+graphicAttr :: WriterOptions -> Attr -> [Inline] -> Target -> [(Text, Text)]+graphicAttr opts (ident, _, kvs) _alt (src, tit) = let (maintype, subtype) = imageMimeType src kvs- in [("id", escapeNCName ident) | not (T.null ident)] +++ in idAttr opts ident ++ [ ("mimetype", maintype) , ("mime-subtype", subtype) , ("xlink:href", src)@@ -670,7 +668,7 @@ if null alt then Nothing else Just . inTagsSimple "alt-text" .- hsep . map literal . T.words $ stringify alt+ hsep . map literal . T.words $ stringifyInlines alt imageMimeType :: Text -> [(Text, Text)] -> (Text, Text) imageMimeType src kvs =
@@ -26,7 +26,7 @@ import Text.DocLayout (Doc, empty, isEmpty, literal, vcat) import Text.Pandoc.Class.PandocMonad (PandocMonad) import Text.Pandoc.Builder (Inlines)-import Text.Pandoc.Options (WriterOptions)+import Text.Pandoc.Options (WriterOptions (..)) import Text.Pandoc.Shared (tshow) import Text.Pandoc.Writers.JATS.Types import Text.Pandoc.XML (escapeNCName, escapeStringForXML, inTags)@@ -43,11 +43,11 @@ => WriterOptions -> Reference Inlines -> JATS m (Doc Text)-referenceToJATS _opts ref = do+referenceToJATS opts ref = do let refType = referenceType ref let pubType = [("publication-type", refType) | not (T.null refType)] let ident = escapeNCName $ "ref-" <> unItemId (referenceId ref)- let wrap = inTags True "ref" [("id", ident)]+ let wrap = inTags True "ref" [("id", writerIdentifierPrefix opts <> ident)] . inTags True "element-citation" pubType return . wrap . vcat $ [ authors
@@ -21,7 +21,7 @@ import Text.DocLayout (Doc, empty, vcat, ($$)) import Text.Pandoc.Class.PandocMonad (PandocMonad) import Text.Pandoc.Definition-import Text.Pandoc.Options (WriterOptions)+import Text.Pandoc.Options (WriterOptions (..)) import Text.Pandoc.Shared (tshow) import Text.Pandoc.Writers.JATS.Types import Text.Pandoc.XML (escapeNCName, inTags, inTagsIndented, selfClosingTag)@@ -65,7 +65,7 @@ , "frame", "rules", "specific-use", "style", "summary" , "width" ]- let attribs = toAttribs attr validAttribs+ let attribs = toAttribs opts attr validAttribs return $ inTags True "table" attribs $ vcat [ colSpecListToJATS colspecs , head'@@ -82,7 +82,7 @@ -> Ann.TableBody -> JATS m (Doc Text) tableBodyToJats opts (Ann.TableBody attr _rowHeadCols inthead rows) = do- let attribs = toAttribs attr validTablePartAttribs+ let attribs = toAttribs opts attr validTablePartAttribs intermediateHead <- if null inthead then return mempty else headerRowsToJats opts Thead inthead@@ -117,7 +117,7 @@ Thead -> "thead" Tfoot -> "tfoot" Tbody -> "tbody" -- this would be unexpected- let attribs = toAttribs attr validTablePartAttribs+ let attribs = toAttribs opts attr validTablePartAttribs inTags True tag' attribs <$> headerRowsToJats opts tblpart rows where isEmptyRow (Ann.HeaderRow _attr _rownum cells) = all isEmptyCell cells@@ -183,7 +183,7 @@ let validAttribs = [ "align", "char", "charoff", "content-type" , "style", "valign" ]- let attr' = toAttribs attr validAttribs+ let attr' = toAttribs opts attr validAttribs let celltype = case tblpart of Thead -> HeaderCell _ -> BodyCell@@ -220,9 +220,11 @@ let align = fst colspec in tableCellToJats opts celltype align cell -toAttribs :: Attr -> [Text] -> [(Text, Text)]-toAttribs (ident, _classes, kvs) knownAttribs =- (if T.null ident then id else (("id", escapeNCName ident) :)) $+toAttribs :: WriterOptions -> Attr -> [Text] -> [(Text, Text)]+toAttribs opts (ident, _classes, kvs) knownAttribs =+ (if T.null ident+ then id+ else (("id", writerIdentifierPrefix opts <> escapeNCName ident) :)) $ filter ((`elem` knownAttribs) . fst) kvs tableCellToJats :: PandocMonad m@@ -255,7 +257,7 @@ let attribs = maybeCons (alignmentAttrib align') . maybeCons (rowspanAttrib rowspan) . maybeCons (colspanAttrib colspan)- $ toAttribs attr validAttribs+ $ toAttribs opts attr validAttribs inTags False tag' attribs <$> cellContents item -- | Whether the JATS produced from this block should be wrapped in a
@@ -25,7 +25,7 @@ import Text.Pandoc.Definition import Text.Pandoc.Options (WriterOptions (writerTemplate, writerWrapText), WrapOption (..))-import Text.Pandoc.Shared (linesToPara, stringify)+import Text.Pandoc.Shared (linesToPara, stringifyInlines) import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Writers.Math (texMathToInlines) import Text.Pandoc.Writers.Shared (defField, metaToContext, toLegacyTable)@@ -253,7 +253,7 @@ -> JiraConverter m [Jira.Inline] imageToJira (_, classes, kvs) caption src title = let imageWithParams ps = Jira.Image ps (Jira.URL src)- alt = stringify caption+ alt = stringifyInlines caption in pure . singleton . imageWithParams $ if "thumbnail" `elem` classes then [Jira.Parameter "thumbnail" ""]@@ -270,7 +270,8 @@ -> JiraConverter m [Jira.Inline] toJiraLink (_, classes, _) (url, _) alias = do let (linkType, url') = toLinkType url- description <- if url `elem` [stringify alias, "mailto:" <> stringify alias]+ description <- if url `elem` [stringifyInlines alias,+ "mailto:" <> stringifyInlines alias] then pure mempty else toJiraInlines alias pure . singleton $ Jira.Link linkType description (Jira.URL url')
@@ -33,14 +33,14 @@ import Crypto.Hash (hashWith, MD5(MD5)) import Data.Containers.ListUtils (nubOrd) import Data.Char (isDigit, isAscii, isLetter)-import Data.List (intersperse, partition, (\\))+import Data.List (find, intersperse, partition) import qualified Data.Set as Set import Data.Maybe (catMaybes, fromMaybe, isJust, listToMaybe, mapMaybe, isNothing) import Data.Monoid (Any (..)) import Data.Text (Text) import qualified Data.Text as T import Network.URI (unEscapeString)-import Text.DocTemplates (Context(..), FromContext(lookupContext), Val(..), renderTemplate)+import Text.DocTemplates (Context(..), FromContext(lookupContext), Val(..)) import qualified Data.Map as M import Text.Collate.Lang (renderLang) import Text.Pandoc.Class.PandocMonad (PandocMonad, getPOSIXTime, lookupEnv,@@ -128,14 +128,6 @@ let blocks' = if method == Biblatex || method == Natbib then filter (not . isRefsDiv) blocks else blocks- -- see if there are internal links- let isInternalLink (Link _ _ (s,_))- | Just ('#', xs) <- T.uncons s = [xs]- isInternalLink _ = []- modify $ \s -> s{ stInternalLinks = query isInternalLink blocks' }- let colwidth = if writerWrapText options == WrapAuto- then Just $ writerColumns options- else Nothing docLangs <- catMaybes <$> mapM (toLang . Just) (nubOrd (query (extract "lang") blocks)) mblang <- toLang $ case getLang options meta of@@ -186,22 +178,26 @@ biblioTitle <- inlineListToLaTeX lastHeader st <- get titleMeta <- escapeCommas <$> -- see #10501- stringToLaTeX TextString (stringify $ docTitle meta)- subtitleMeta <- stringToLaTeX TextString (stringify $ lookupMetaInlines "subtitle" meta)- authorsMeta <- mapM (stringToLaTeX TextString . stringify) $ docAuthors meta+ stringToLaTeX TextString (stringifyInlines $ docTitle meta)+ subtitleMeta <- stringToLaTeX TextString+ (stringifyInlines $ lookupMetaInlines "subtitle" meta)+ authorsMeta <- mapM (stringToLaTeX TextString . stringifyInlines) $+ docAuthors meta -- The trailer ID is as hash used to identify the PDF. Taking control of its -- value is important when aiming for reproducible PDF generation. Setting -- `SOURCE_DATE_EPOCH` is the traditional method used to control -- reproducible builds. There are no cryptographic requirements for the ID, -- so the 128bits (16 bytes) of MD5 are appropriate. reproduciblePDF <- isJust <$> lookupEnv "SOURCE_DATE_EPOCH"- trailerID <- do- time <- getPOSIXTime- let hash = T.pack . show . hashWith MD5 $ mconcat- [ UTF8.fromString $ show time- , UTF8.fromText $ render Nothing main- ]- pure $ mconcat [ "<", hash, "> <", hash, ">" ]+ trailerID <- if reproduciblePDF+ then do+ time <- getPOSIXTime+ let hash = T.pack . show . hashWith MD5 $ mconcat+ [ UTF8.fromString $ show time+ , UTF8.fromText $ render Nothing main+ ]+ pure $ Just $ mconcat [ "<", hash, "> <", hash, ">" ]+ else pure Nothing -- we need a default here since lang is used in template conditionals let hasStringValue x = isJust (getField x metadata :: Maybe (Doc Text)) let geometryFromMargins = mconcat $ intersperse ("," :: Doc Text) $@@ -247,6 +243,7 @@ defField "documentclass" documentClass $ defField "verbatim-in-note" (stVerbInNote st) $ defField "tables" (stTable st) $+ defField "deferred-notes" (stDeferredNoteNum st > 0) $ defField "multirow" (stMultiRow st) $ defField "cancel" (stCancel st) $ defField "strikeout" (stStrikeout st) $@@ -297,9 +294,7 @@ | not (T.null ds) && T.all isDigit ds -> resetField "papersize" ("a" <> ds) _ -> id) .- (if reproduciblePDF- then defField "pdf-trailer-id" trailerID- else id) $+ maybe id (defField "pdf-trailer-id") trailerID $ (if not (null (pdfStandards pdfStd)) || isJust (pdfVersion pdfStd) then resetField "pdfstandard" $ MapVal $ Context $ M.fromList [ ("standards", ListVal $ map (SimpleVal . literal) (pdfStandards pdfStd))@@ -329,10 +324,7 @@ $ defField "latex-dir-rtl" ((render Nothing <$> getField "dir" context) == Just ("rtl" :: Text)) context- return $ render colwidth $- case writerTemplate options of- Nothing -> main- Just tpl -> renderTemplate tpl context'+ return $ renderWithTemplate options context' main -- Commas in title-meta need to be put in braces; see #10501 escapeCommas :: Text -> Text@@ -774,12 +766,12 @@ modify $ \s -> s{ stInItem = True } term' <- withExternalNotes $ inlineListToLaTeX term modify $ \s -> s{ stInItem = False }- -- put braces around term if it contains an internal link,- -- since otherwise we get bad bracket interactions: \item[\hyperref[..]- let isInternalLink (Link _ _ (src,_))+ -- put braces around term if it contains an internal link or an =:+ let needsBraces (Link _ _ (src,_)) | Just ('#', _) <- T.uncons src = True- isInternalLink _ = False- let term'' = if any isInternalLink term+ needsBraces (Str t) = T.any (=='=') t+ needsBraces _ = False+ let term'' = if any needsBraces term then braces term' else term' def' <- case concat defs of@@ -809,21 +801,19 @@ let unnumbered = "unnumbered" `elem` classes let unlisted = "unlisted" `elem` classes txt <- inlineListToLaTeX lst- plain <- stringToLaTeX TextString $ T.concat $ map stringify lst+ plain <- stringToLaTeX TextString $ stringifyInlines lst let removeInvalidInline (Note _) = [] removeInvalidInline (Span (id', _, _) _) | not (T.null id') = [] removeInvalidInline Image{} = [] removeInvalidInline x = [x] let lstNoNotes = foldr (mappend . (\x -> walkM removeInvalidInline x)) mempty lst- txtNoNotes <- inlineListToLaTeX lstNoNotes- txtNoLinksNoNotes <- inlineListToLaTeX (removeLinks lstNoNotes) -- footnotes in sections don't work (except for starred variants) -- unless you specify an optional argument: -- \section[mysec]{mysec\footnote{blah}} optional <- if unnumbered || lstNoNotes == lst || null lstNoNotes then return empty else- return $ brackets txtNoNotes+ brackets <$> inlineListToLaTeX lstNoNotes let contents = if render Nothing txt == plain then braces txt else braces (text "\\texorpdfstring"@@ -862,45 +852,46 @@ lab <- labelFor ident let star = if unnumbered then text "*" else empty let title = star <> optional <> contents+ tocEntry <- if unnumbered && not unlisted+ then do+ txtNoLinksNoNotes <- inlineListToLaTeX+ (removeLinks lstNoNotes)+ pure $ "\\addcontentsline{toc}" <>+ braces (text sectionType) <>+ braces txtNoLinksNoNotes+ else pure empty return $ if level' > 5 then txt else prefix $$ text ('\\':sectionType) <> title <> lab- $$ if unnumbered && not unlisted- then "\\addcontentsline{toc}" <>- braces (text sectionType) <>- braces txtNoLinksNoNotes- else empty+ $$ tocEntry -- | Convert list of inline elements to LaTeX. inlineListToLaTeX :: PandocMonad m => [Inline] -- ^ Inlines to convert -> LW m (Doc Text) inlineListToLaTeX lst = hcat <$>- mapM inlineToLaTeX- (addKerns . fixLineInitialSpaces . fixInitialLineBreaks $ lst)- -- nonbreaking spaces (~) in LaTeX don't work after line breaks,- -- so we insert a strut: this is mostly used in verse.- where fixLineInitialSpaces [] = []- fixLineInitialSpaces (LineBreak : Str s : xs)- | Just ('\160', _) <- T.uncons s- = LineBreak : RawInline "latex" "\\strut " : Str s- : fixLineInitialSpaces xs- fixLineInitialSpaces (x:xs) = x : fixLineInitialSpaces xs- -- We need \hfill\break for a line break at the start+ mapM inlineToLaTeX (fixInlines . fixInitialLineBreaks $ lst)+ where -- We need \hfill\break for a line break at the start -- of a paragraph. See #5591. fixInitialLineBreaks (LineBreak:xs) = RawInline (Format "latex") "\\hfill\\break\n" : fixInitialLineBreaks xs fixInitialLineBreaks xs = xs- addKerns [] = []- addKerns (Str s : q@Quoted{} : rest)+ fixInlines [] = []+ -- nonbreaking spaces (~) in LaTeX don't work after line breaks,+ -- so we insert a strut: this is mostly used in verse.+ fixInlines (LineBreak : Str s : xs)+ | Just ('\160', _) <- T.uncons s+ = LineBreak : RawInline "latex" "\\strut " : fixInlines (Str s : xs)+ -- insert a thin space (kern) between adjacent quote characters:+ fixInlines (Str s : q@Quoted{} : rest) | isQuote (T.takeEnd 1 s) =- Str s : RawInline (Format "latex") "\\," : addKerns (q:rest)- addKerns (q@Quoted{} : Str s : rest)+ Str s : RawInline (Format "latex") "\\," : fixInlines (q:rest)+ fixInlines (q@Quoted{} : Str s : rest) | isQuote (T.take 1 s) =- q : RawInline (Format "latex") "\\," : addKerns (Str s : rest)- addKerns (x:xs) = x : addKerns xs+ q : RawInline (Format "latex") "\\," : fixInlines (Str s : rest)+ fixInlines (x:xs) = x : fixInlines xs isQuote "\"" = True isQuote "'" = True isQuote "\x2018" = True@@ -994,9 +985,9 @@ listingsopts) <> "]" inNote <- gets stInNote when inNote $ modify $ \s -> s{ stVerbInNote = True }- let chr = case "!\"'()*,-./:;?@" \\ T.unpack str of- (c:_) -> c- [] -> '!'+ let chr = fromMaybe '!' $+ find (\c -> not (T.any (== c) str))+ ("!\"'()*,-./:;?@" :: String) let isEscapable '\\' = True isEscapable '{' = True isEscapable '}' = True@@ -1027,7 +1018,7 @@ unless (T.null msg) $ report $ CouldNotHighlight msg rawCode Right h -> modify (\st -> st{ stHighlighting = True }) >>- return (text (T.unpack h))+ return (literal h) -- for soul commands we need to protect VERB in an mbox or we get an error -- (see #1294). with regular texttt we don't get an error, but we get -- incorrect results if there is a space (see #5529).@@ -1183,7 +1174,7 @@ Nothing | null description -> pure Nothing | otherwise -> Just <$> stringToLaTeX TextString- (stringify description)+ (stringifyInlines description) let showDim dir = let d = text (show dir) <> "=" in case dimension dir attr of Just (Pixel a) ->@@ -1209,7 +1200,8 @@ _ -> ["keepaspectratio"]) <> maybe [] (\x -> ["page=" <> literal x]) (lookup "page" kvs) <> maybe [] (\x -> ["trim=" <> literal x]) (lookup "trim" kvs) <>- maybe [] (\x -> ["alt=" <> braces (literal x)]) mbalt <>+ maybe [] (\x -> ["alt=" <>+ braces (hsep $ map literal $ T.words x)]) mbalt <> maybe [] (const ["clip"]) (lookup "clip" kvs) options = if null optList then empty@@ -1229,6 +1221,7 @@ inlineToLaTeX (Note contents) = do setEmptyLine False modify (\s -> s{stInNote = True })+ numNotesBefore <- gets (length . stNotes) contents' <- withExternalNotes $ blockListToLaTeX contents modify (\s -> s {stInNote = False }) let optnl = case reverse contents of@@ -1247,8 +1240,14 @@ externalNotes <- gets stExternalNotes if externalNotes then do- modify $ \st -> st{ stNotes = noteContents : stNotes st }- return "\\footnotemark{}"+ -- the marks of notes nested in this one are only set when+ -- this note's text is typeset, so their texts must come after it+ notes <- gets stNotes+ let (nested, rest) = splitAt (length notes - numNotesBefore) notes+ num <- gets ((+ 1) . stDeferredNoteNum)+ modify $ \st -> st{ stNotes = nested ++ (num, noteContents) : rest+ , stDeferredNoteNum = num }+ return $ "\\pandocfootnotemark" <> braces (text (show num)) -- note: a \n before } needed when note ends with a Verbatim environment else return $ "\\footnote" <> beamerMark <> braces noteContents
@@ -1,4 +1,3 @@-{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {- | Module : Text.Pandoc.Writers.LaTeX.Notes@@ -9,26 +8,21 @@ Stability : alpha Portability : portable -Output tables as LaTeX.+Output deferred footnote texts as LaTeX. -} module Text.Pandoc.Writers.LaTeX.Notes ( notesToLaTeX ) where -import Data.List (intersperse)-import Text.DocLayout ( Doc, braces, empty, text, vcat, ($$))+import Text.DocLayout ( Doc, braces, text, vcat ) import Data.Text (Text) -notesToLaTeX :: [Doc Text] -> Doc Text-notesToLaTeX = \case- [] -> empty- ns -> (case length ns of- n | n > 1 -> "\\addtocounter" <>- braces "footnote" <>- braces (text $ show $ 1 - n)- | otherwise -> empty)- $$- vcat (intersperse- ("\\addtocounter" <> braces "footnote" <> braces "1")- $ map (\x -> "\\footnotetext" <> braces x)- $ reverse ns)+-- | Render the texts of notes whose marks were produced with+-- @\\pandocfootnotemark{n}@. @\\pandocfootnotetext{n}@ restores+-- the footnote mark and hyperref anchor saved by the corresponding+-- mark (see the template). Notes are stored in reverse order.+notesToLaTeX :: [(Int, Doc Text)] -> Doc Text+notesToLaTeX = vcat . map toNoteText . reverse+ where+ toNoteText (num, x) =+ "\\pandocfootnotetext" <> braces (text (show num)) <> braces x
@@ -28,7 +28,7 @@ import Text.DocLayout ( Doc, braces, brackets, cr, empty, hcat, hsep, isEmpty, literal, nest , text, vcat, ($$))-import Text.Pandoc.Shared (splitBy, tshow)+import Text.Pandoc.Shared (deNote, splitBy, tshow) import Text.Pandoc.Walk (walk, query) import Data.Monoid (Any(..)) import Text.Pandoc.Writers.LaTeX.Caption (getCaption)@@ -53,14 +53,15 @@ $ lookup "latex-placement" kvs -- if the float class is included in table attributes, we generate a floating -- table environment; otherwise we use longtable- beamer <- gets stBeamer let float = "float" `elem` classes+ let repeatCaption = not float && "repeat-caption" `elem` classes let renderTable = do let unnumbered = "unnumbered" `elem` classes- capt <- case caption of- Caption Nothing []- | unnumbered || T.null ident -> pure mempty- _ -> captionToLaTeX inlnsToLaTeX caption ident+ (capt, repeatedCapt) <-+ case caption of+ Caption Nothing []+ | unnumbered || T.null ident -> pure (mempty, mempty)+ _ -> captionToLaTeX inlnsToLaTeX repeatCaption caption ident let isSimpleTable = all ((== ColWidthDefault) . snd) specs && all (all isSimpleCell)@@ -77,17 +78,15 @@ let makeUnnumbered x = "{\\def\\LTcaptype{none} % do not increment counter" $$ x $$ "}" let makeTable = if float- then tableToLaTeXTable placement- else tableToLaTeXLongtable+ then tableToLaTeXTable placement colDesc+ mkHead mkRow capt+ else tableToLaTeXLongtable colDesc mkHead mkRow capt+ repeatedCapt (if unnumbered || isEmpty capt then makeUnnumbered else id) <$>- makeTable colDesc mkHead mkRow capt thead tbodies tfoot- -- See #5367 -- footnotehyper/footnote don't work in beamer,- -- so we need to produce the notes outside the table...- if float || beamer- then ($$) <$> withExternalNotes renderTable <*> getAccumulatedNotes- else renderTable+ makeTable thead tbodies tfoot+ ($$) <$> withExternalNotes renderTable <*> getAccumulatedNotes tableToLaTeXTable :: PandocMonad m => Doc Text@@ -137,28 +136,33 @@ -> (Ann.TableHead -> LW m (Doc Text)) -> ([Ann.Cell] -> LW m (Doc Text)) -> Doc Text+ -> Doc Text -> Ann.TableHead -> [Ann.TableBody] -> Ann.TableFoot -> LW m (Doc Text)-tableToLaTeXLongtable colDesc mkHead mkRow capt thead tbodies tfoot = do+tableToLaTeXLongtable colDesc mkHead mkRow capt repeatedCapt+ thead tbodies tfoot = do opts <- gets stOptions let hasTopCaption = not (isEmpty capt) && writerTableCaptionPosition opts == CaptionAbove let hasBottomCaption = not (isEmpty capt) && writerTableCaptionPosition opts == CaptionBelow- -- The first head is not repeated on the following pages. If we were to just- -- use a single head, without a separate first head, then the caption would be- -- repeated on all pages that contain a part of the table. We avoid this by- -- making the caption part of the first head. The downside is that we must- -- duplicate the header rows for this.+ -- The first head is not repeated on the following pages. Keep the numbered+ -- caption in that head. Add the unnumbered repeated caption to the regular+ -- head only when requested. The downside is that we must duplicate the+ -- header rows. head' <- do case (hasTopCaption, isEmptyHead thead) of (False, True) -> return "\\toprule\\noalign{}" (False, False) -> mkHead thead (True, True) -> return (capt <> "\\tabularnewline" $$ "\\toprule\\noalign{}"- $$ "\\endfirsthead")+ $$ "\\endfirsthead"+ $$ (if isEmpty repeatedCapt+ then mempty+ else repeatedCapt <> "\\tabularnewline"+ $$ "\\toprule\\noalign{}")) (True, False) -> do -- avoid duplicate notes in head and firsthead: firsthead <- mkHead thead@@ -168,6 +172,9 @@ return $ capt <> "\\tabularnewline" $$ firsthead $$ "\\endfirsthead"+ $$ (if isEmpty repeatedCapt+ then mempty+ else repeatedCapt <> "\\tabularnewline") $$ repeated rows' <- mapM mkRow $ mconcat (map bodyRows tbodies) lastfoot <- mapM mkRow $ footRows tfoot@@ -178,6 +185,9 @@ $$ (if hasBottomCaption then "\\tabularnewline" $$ capt else mempty)+ let repeatedFoot = "\\bottomrule\\noalign{}"+ $$ "\\tabularnewline"+ $$ repeatedCapt modify $ \s -> s{ stTable = True } beamer <- gets stBeamer return $@@ -193,9 +203,12 @@ then [ vcat rows' , foot' ]- else [ foot'+ else (if hasBottomCaption && not (isEmpty repeatedCapt)+ then [ repeatedFoot, "\\endfoot" ]+ else []) <>+ [ foot' , "\\endlastfoot"- , vcat rows'+ , vcat rows' ]) $$ "\\end{longtable}" @@ -262,16 +275,25 @@ captionToLaTeX :: PandocMonad m => ([Inline] -> LW m (Doc Text))+ -> Bool -- ^ repeat caption on subsequent pages -> Caption -> Text -- ^ table identifier (label)- -> LW m (Doc Text)-captionToLaTeX inlnsToLaTeX caption ident = do+ -> LW m (Doc Text, Doc Text)+captionToLaTeX inlnsToLaTeX repeatCaption caption ident = do (captionText, captForLot) <- getCaption inlnsToLaTeX caption+ repeatedCaptionText <-+ if repeatCaption+ then fst <$> getCaption inlnsToLaTeX (walk deNote caption)+ else pure empty label <- labelFor ident- return $ if isEmpty captionText && isEmpty label- then empty- else "\\caption" <> captForLot <>- braces captionText <> label+ let capt = if isEmpty captionText && isEmpty label+ then empty+ else "\\caption" <> captForLot <>+ braces captionText <> label+ let repeatedCapt = if isEmpty repeatedCaptionText+ then empty+ else "\\caption[]" <> braces repeatedCaptionText+ return (capt, repeatedCapt) type BlocksWriter m = [Block] -> LW m (Doc Text) @@ -353,11 +375,8 @@ -- For simple latex tables (without minipages or parboxes), -- we need to go to some lengths to get line breaks working: -- as LineBreak bs = \vtop{\hbox{\strut as}\hbox{\strut bs}}.-fixLineBreaks :: Block -> Block-fixLineBreaks = walk fixLineBreaks'--fixLineBreaks' :: [Inline] -> [Inline]-fixLineBreaks' ils = case splitBy (== LineBreak) ils of+fixLineBreaks :: [Inline] -> [Inline]+fixLineBreaks ils = case splitBy (== LineBreak) ils of [] -> [] [xs] -> xs chunks -> RawInline "tex" "\\vtop{" :
@@ -29,7 +29,8 @@ , stInItem :: Bool -- ^ true if in \item[..] , stInFigure :: Bool -- ^ true if in figure environment , stInCite :: Bool -- ^ true if in a Cite- , stNotes :: [Doc Text] -- ^ notes in a minipage+ , stNotes :: [(Int, Doc Text)] -- ^ deferred notes (id, text)+ , stDeferredNoteNum :: Int -- ^ number of deferred notes so far , stOLLevel :: Int -- ^ level of ordered list nesting , stOptions :: WriterOptions -- ^ writer options, so they don't have to -- be parameter@@ -47,7 +48,6 @@ , stHighlighting :: Bool -- ^ true if document has highlighted code , stIncremental :: Bool -- ^ true if beamer lists should be , stZwnj :: Bool -- ^ true if document has a ZWNJ character- , stInternalLinks :: [Text] -- ^ list of internal link targets , stBeamer :: Bool -- ^ produce beamer , stEmptyLine :: Bool -- ^ true if no content on line , stHasCslRefs :: Bool -- ^ has a Div with class refs@@ -77,6 +77,7 @@ , stInFigure = False , stInCite = False , stNotes = []+ , stDeferredNoteNum = 0 , stOLLevel = 1 , stOptions = options , stVerbInNote = False@@ -96,7 +97,6 @@ , stHighlighting = False , stIncremental = writerIncremental options , stZwnj = False- , stInternalLinks = [] , stBeamer = False , stEmptyLine = True , stHasCslRefs = False
@@ -39,7 +39,7 @@ import qualified Data.Text as T import Text.Pandoc.Extensions (Extension(Ext_smart)) import Data.Char (isLetter, isSpace, isDigit, isAscii, ord, isAlphaNum)-import Text.Printf (printf)+import Numeric (showHex) import Text.Pandoc.Shared (safeRead) import qualified Data.Text.Normalize as Normalize import Data.List (uncons)@@ -199,7 +199,7 @@ go = T.concatMap $ \x -> case x of _ | (isLetter x || isDigit x) && isAscii x -> T.singleton x | T.any (== x) "_-+=:;." -> T.singleton x- | otherwise -> T.pack $ "ux" <> printf "%x" (ord x)+ | otherwise -> T.pack $ "ux" <> showHex (ord x) "" -- | Puts contents into LaTeX command. inCmd :: Text -> Doc Text -> Doc Text
@@ -31,7 +31,6 @@ import Text.DocLayout import Text.Pandoc.Shared import Text.Pandoc.URI-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Writers.Math import Text.Pandoc.Writers.Shared import Text.Pandoc.Writers.Roff@@ -48,9 +47,6 @@ -- | Return roff man representation of document. pandocToMan :: PandocMonad m => WriterOptions -> Pandoc -> StateT WriterState m Text pandocToMan opts (Pandoc meta blocks) = do- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing titleText <- inlineListToMan opts $ docTitle meta let title' = render Nothing titleText let setFieldsFromTitle =@@ -80,10 +76,7 @@ $ setFieldsFromTitle $ defField "has-tables" hasTables metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main escString :: WriterOptions -> Text -> Text escString opts = escapeString True (if writerPreferAscii opts
@@ -43,14 +43,14 @@ import Text.DocLayout import Text.Pandoc.Shared import Text.Pandoc.Writers.Shared-import Text.Pandoc.Templates (renderTemplate) import Text.DocTemplates (Val(..), Context(..), FromContext(..)) import Text.Pandoc.Walk import Text.Pandoc.Writers.HTML (writeHtml5String) import Text.Pandoc.Writers.Markdown.Inline (inlineListToMarkdown, linkAttributes, attrsToMarkdown,- attrsToMarkua)+ attrsToMarkua,+ linkDestination) import Text.Pandoc.Writers.Markdown.Table (pipeTable, pandocTable) import Text.Pandoc.Writers.Markdown.Types (MarkdownVariant(..), WriterState(..),@@ -221,9 +221,6 @@ -- | Return markdown representation of document. pandocToMarkdown :: PandocMonad m => WriterOptions -> Pandoc -> MD m Text pandocToMarkdown opts (Pandoc meta blocks) = do- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing variant <- asks envVariant metadata <- metaToContext' (blockListToMarkdown opts)@@ -271,10 +268,7 @@ then id else defField "titleblock" titleblock) $ addVariablesToContext opts metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main -- | Return markdown representation of reference key table. refsToMarkdown :: PandocMonad m => WriterOptions -> Refs -> MD m (Doc Text)@@ -290,16 +284,26 @@ then empty else space <> "\"" <> literal tit <> "\"" return $ nest 2 $ hang 2- ("[" <> literal label' <> "]:" <> space) (literal src <> tit')+ ("[" <> literal label' <> "]:" <> space)+ (literal (linkDestination src) <> tit') <+> linkAttributes opts attr --- | Return markdown representation of notes.-notesToMarkdown :: PandocMonad m => WriterOptions -> [[Block]] -> MD m (Doc Text)-notesToMarkdown opts notes = do- n <- gets stNoteNum- notes' <- zipWithM (noteToMarkdown opts) [n..] notes- modify $ \st -> st { stNoteNum = stNoteNum st + length notes }- return $ vsep notes'+-- | Return markdown representation of the collected notes, clearing+-- them from the state. Rendering a note may add further notes to the+-- state (notes nested inside notes); keep going until all of them+-- have been rendered.+notesToMarkdown :: PandocMonad m => WriterOptions -> MD m (Doc Text)+notesToMarkdown opts = vsep <$> go+ where+ go = do+ notes <- gets (reverse . stNotes)+ if null notes+ then return []+ else do+ n <- gets stNoteNum+ modify $ \st -> st { stNotes = [], stNoteNum = n + length notes }+ docs <- zipWithM (noteToMarkdown opts) [n..] notes+ (docs ++) <$> go -- | Return markdown representation of a note. noteToMarkdown :: PandocMonad m => WriterOptions -> Int -> [Block] -> MD m (Doc Text)@@ -345,8 +349,7 @@ notesAndRefs :: PandocMonad m => WriterOptions -> MD m (Doc Text) notesAndRefs opts = do- notes' <- gets stNotes >>= notesToMarkdown opts . reverse- modify $ \s -> s { stNotes = [] }+ notes' <- notesToMarkdown opts refs' <- gets stRefs >>= refsToMarkdown opts . reverse modify $ \s -> s { stPrevRefs = stPrevRefs s ++ stRefs s , stRefs = []}@@ -429,28 +432,39 @@ where (id',classes',kvs') = attrs attrs' = (id',classes',("markdown","1"):kvs') blockToMarkdown' opts (Plain inlines) = do- -- escape if para starts with ordered list marker+ -- escape if a line starts with a list marker. This must be done at the+ -- start of every line, not just the start of the block: a sublist can+ -- interrupt a list item's text after a LineBreak, so an unescaped marker+ -- there would be read back as a nested list, destroying the LineBreak. variant <- asks envVariant let escapeMarker = T.concatMap $ \x -> if T.any (== x) ".()" then T.pack ['\\', x] else T.singleton x let startsWithSpace (Space:_) = True startsWithSpace (SoftBreak:_) = True+ startsWithSpace (LineBreak:_) = True startsWithSpace _ = False+ let escapeLineStart ils =+ case ils of+ (Str t:ys)+ | null ys || startsWithSpace ys+ , beginsWithOrderedListMarker t+ -> RawInline (Format "markdown") (escapeMarker t):ys+ (Str t:_)+ | t == "+" || t == "-" ||+ (t == "%" && isEnabled Ext_pandoc_title_block opts &&+ isEnabled Ext_all_symbols_escapable opts)+ -> RawInline (Format "markdown") "\\" : ils+ _ -> ils+ -- apply escapeLineStart to the head of the list, then after each LineBreak+ let go ils = case ils of+ [] -> []+ (LineBreak:ys) -> LineBreak : go (escapeLineStart ys)+ (y:ys) -> y : go ys let inlines' = if variant == PlainText then inlines- else case inlines of- (Str t:ys)- | null ys || startsWithSpace ys- , beginsWithOrderedListMarker t- -> RawInline (Format "markdown") (escapeMarker t):ys- (Str t:_)- | t == "+" || t == "-" ||- (t == "%" && isEnabled Ext_pandoc_title_block opts &&- isEnabled Ext_all_symbols_escapable opts)- -> RawInline (Format "markdown") "\\" : inlines- _ -> inlines+ else go (escapeLineStart inlines) contents <- inlineListToMarkdown opts inlines' return $ contents <> cr blockToMarkdown' opts (Para inlines) =@@ -608,7 +622,10 @@ let (_,cls,_) = attribs in case getLangFromClasses opts cls of Just l -> " " <> literal l- Nothing -> empty+ Nothing -> -- see #11987+ case cls of+ l:_ -> literal l+ [] -> empty blockToMarkdown' opts (BlockQuote blocks) = do variant <- asks envVariant -- if we're writing literate haskell, put a space before the bird tracks@@ -672,13 +689,11 @@ isEnabled Ext_pipe_tables opts -> do tbl <- mkTable (pipeTable opts) return $ (tbl $$ caption''') $$ blankline- | not (hasBlocks || hasColRowSpans || hasFooter) &&+ | not (hasBlocks || hasColRowSpans || hasFooter || null rows) && isEnabled Ext_multiline_tables opts -> do tbl <- mkTable (pandocTable opts True) return $ nest 2 (tbl $$ caption''') $$ blankline- | isEnabled Ext_grid_tables opts &&- (hasColRowSpans || writerColumns opts >= 8 * numcols- || hasFooter) -> do+ | isEnabled Ext_grid_tables opts -> do tbl <- gridTable opts blockListToMarkdown specs thead tbody tfoot return $ (tbl $$ caption''') $$ blankline@@ -754,8 +769,8 @@ (i,c,kv) | not (null alt) , Nothing <- lookup "alt" kv- , stringify descr /= stringify alt ->- (i, c, ("alt", stringify alt) : kv)+ , stringifyInlines descr /= stringifyInlines alt ->+ (i, c, ("alt", stringifyInlines alt) : kv) _ -> imgAttr' contents <- inlineListToMarkdown opts [Image imgAttr'' descr tgt'] return $ contents <> blankline
@@ -14,14 +14,15 @@ inlineListToMarkdown, linkAttributes, attrsToMarkdown,- attrsToMarkua+ attrsToMarkua,+ linkDestination ) where import Control.Monad (when, liftM2) import Control.Monad.Reader ( asks, MonadReader(local) ) import Control.Monad.State.Strict ( MonadState(get), gets, modify )-import Data.Char (isAlphaNum, isDigit)+import Data.Char (isAlphaNum, isDigit, isSpace) import Data.List (find, intersperse) import Data.List.NonEmpty (nonEmpty) import qualified Data.Map as M@@ -86,7 +87,7 @@ go ('\\':c:cs) | isEnabled Ext_raw_tex opts = '\\':'\\':go (c:cs) | isAlphaNum c = '\\' : go (c:cs)- | otherwise = '\\':'\\': go cs+ | otherwise = '\\':'\\': go (c:cs) go ('!':'[':cs) = '\\':'!':'[': go cs go ('=':'=':cs) | isEnabled Ext_mark opts = '\\':'=':go ('=':cs)@@ -338,6 +339,17 @@ toList (Concat a b) = a : toList b toList x = [x] +-- | Render a link destination, enclosing it in angle brackets if+-- it contains whitespace, which is not allowed in a bare destination.+linkDestination :: Text -> Text+linkDestination src+ | T.any isSpace src = "<" <> T.concatMap escapeChar src <> ">"+ | otherwise = src+ where+ escapeChar c+ | c `elem` ['<', '>', '\\'] = T.pack ['\\', c]+ | otherwise = T.singleton c+ -- | Convert Pandoc inline element to markdown. inlineToMarkdown :: PandocMonad m => WriterOptions -> Inline -> MD m (Doc Text) inlineToMarkdown opts (Span ("",["emoji"],kvs) [Str s]) =@@ -678,13 +690,13 @@ where result = "[" <> linktext <> "](" <> (literal src) <> ")" attributes = addKeyValueToAttr attr ("title", tit) -- Use wikilinks where possible- _ | src == stringify txt && useWikilink ->- return $ "[[" <> literal (stringify txt) <> "]]"+ _ | src == stringifyInlines txt && useWikilink ->+ return $ "[[" <> literal (stringifyInlines txt) <> "]]" | useAuto -> return $ "<" <> literal srcSuffix <> ">" | useWikilink && isEnabled Ext_wikilinks_title_after_pipe opts -> return $- "[[" <> literal src <> "|" <> literal (stringify txt) <> "]]"+ "[[" <> literal src <> "|" <> literal (stringifyInlines txt) <> "]]" | useWikilink && isEnabled Ext_wikilinks_title_before_pipe opts -> return $- "[[" <> literal (stringify txt) <> "|" <> literal src <> "]]"+ "[[" <> literal (stringifyInlines txt) <> "|" <> literal src <> "]]" | useRefLinks -> let first = "[" <> linktext <> "]" second = if getKey linktext == getKey reftext@@ -700,8 +712,8 @@ writeHtml5String opts{ writerTemplate = Nothing } (Pandoc nullMeta [Plain [lnk]]) | otherwise -> return $- "[" <> linktext <> "](" <> literal src <> linktitle <> ")" <>- linkAttributes opts attr+ "[" <> linktext <> "](" <> literal (linkDestination src) <> linktitle+ <> ")" <> linkAttributes opts attr inlineToMarkdown opts img@(Image attr alternate (source, tit)) | isEnabled Ext_raw_html opts && not (isEnabled Ext_link_attributes opts || isEnabled Ext_attributes opts) &&
@@ -147,7 +147,7 @@ blockToMediaWiki HorizontalRule = return $ blankline <> literal "-----" <> blankline blockToMediaWiki (Header level (ident,_,_) inlines) = do- let autoId = T.replace " " "_" $ stringify inlines+ let autoId = T.replace " " "_" $ stringifyInlines inlines contents <- inlineListToMediaWiki inlines let eqs = literal $ T.replicate level "=" return $
@@ -44,7 +44,6 @@ import Text.DocLayout hiding (Color) import Text.DocTemplates (lookupContext) import Text.Pandoc.Shared-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Writers.Math import Text.Pandoc.Writers.Shared import Text.Pandoc.Writers.Roff@@ -62,9 +61,6 @@ -- | Return roff ms representation of document. pandocToMs :: PandocMonad m => WriterOptions -> Pandoc -> MS m Text pandocToMs opts (Pandoc meta blocks) = do- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing title <- chomp <$> inlineListToMs' opts (lookupMetaInlines "title" meta) metadata <- metaToContext opts (blockListToMs opts)@@ -72,8 +68,8 @@ meta main <- blockListToMs opts blocks hasInlineMath <- gets stHasInlineMath- let titleMeta = (escapeStr opts . stringify) $ docTitle meta- let authorsMeta = map (escapeStr opts . stringify) $ docAuthors meta+ let titleMeta = (escapeStr opts . stringifyInlines) $ docTitle meta+ let authorsMeta = map (escapeStr opts . stringifyInlines) $ docAuthors meta hasHighlighting <- gets stHighlighting let highlightingMacros = if hasHighlighting then case writerHighlightMethod opts of@@ -91,10 +87,7 @@ $ defField "author-meta" (T.intercalate "; " authorsMeta) $ defField "highlighting-macros" highlightingMacros $ resetField "title" title metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main escapeStr :: WriterOptions -> Text -> Text escapeStr opts =@@ -515,7 +508,7 @@ Nothing -> contents inlineToMs opts (Image attr alternate (src, _)) = do let desc = literal "[IMAGE: " <>- literal (escapeStr opts (stringify alternate)) <> char ']'+ literal (escapeStr opts (stringifyInlines alternate)) <> char ']' let sizeAttrs = getSizeAttrs opts attr let ext = takeExtension (T.unpack src) let cmd = case ext of
@@ -47,7 +47,6 @@ import Text.DocLayout import Text.Pandoc.Shared import Text.Pandoc.URI-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Writers.Math import Text.Pandoc.Writers.Shared @@ -108,9 +107,6 @@ -> Muse m Text pandocToMuse (Pandoc meta blocks) = do opts <- asks envOptions- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing metadata <- metaToContext opts blockListToMuse (fmap chomp . inlineListToMuse)@@ -119,10 +115,7 @@ notes <- currentNotesToMuse let main = body $+$ notes let context = defField "body" main metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main -- | Helper function for flatBlockListToMuse -- | Render all blocks and insert blank lines between the first two
@@ -1,3 +1,4 @@+{-# LANGUAGE OverloadedStrings #-} {- | Module : Text.Pandoc.Writers.Native Copyright : Copyright (C) 2006-2024 John MacFarlane@@ -8,23 +9,297 @@ Portability : portable Conversion of a 'Pandoc' document to a string representation.++This used to be implemented using pretty-show's 'ppDoc' (which shows+the document, tokenizes and parses the result, and lays it out with+Text.PrettyPrint.HughesPJ). For performance, we now build the layout+directly from the AST, carefully reproducing the exact output of the+old implementation (with @ribbonsPerLine = 1.2@). -} module Text.Pandoc.Writers.Native ( writeNative ) where+import Data.List (intersperse)+import qualified Data.Map as M import Data.Text (Text) import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B import Text.Pandoc.Class.PandocMonad (PandocMonad) import Text.Pandoc.Definition import Text.Pandoc.Options (WriterOptions (..))-import Text.Show.Pretty (ppDoc)-import Text.PrettyPrint (renderStyle, Style(..), style, char) -- | Prettyprint Pandoc document. writeNative :: PandocMonad m => WriterOptions -> Pandoc -> m Text-writeNative opts (Pandoc meta blocks) = do- let style' = style{ lineLength = writerColumns opts,- ribbonsPerLine = 1.2 }- return $ T.pack $ renderStyle style' $- case writerTemplate opts of- Just _ -> ppDoc (Pandoc meta blocks) <> char '\n'- Nothing -> ppDoc blocks+writeNative opts doc@(Pandoc _ blocks) = do+ let cols = writerColumns opts+ -- HughesPJ computes the ribbon length this way (with Float division):+ let ribbon = round (fromIntegral cols / (1.2 :: Float))+ return $ case writerTemplate opts of+ -- The old code appended a (char '\n'), which participates in layout+ -- as one extra glued character on the last line; hence glue0 = 1.+ Just _ -> render cols ribbon 1 (vDoc (pandocV doc)) <> "\n"+ Nothing -> render cols ribbon 0 (vDoc (blocksV blocks))++--+-- Layout documents (mirroring the structures pretty-show builds)+--++-- | A document together with the width of its one-line rendering.+data Doc = Doc !Int DC++-- | Either literal text, or a group that is rendered like HughesPJ's+-- 'sep': all on one line (elements joined by single spaces) if it+-- fits, otherwise vertically, with each element after the first on+-- its own line, indented by its nesting relative to the column at+-- which the group starts.+data DC = DText !Text+ | DGroup [Elt]++-- | Group element: nesting, glued prefix text, document, glued suffix.+data Elt = Elt !Int !Text Doc !Text++width :: Doc -> Int+width (Doc w _) = w++dtext :: Text -> Doc+dtext t = Doc (T.length t) (DText t)++group :: [Elt] -> Doc+group es = Doc (foldl (\acc e -> acc + 1 + eltWidth e) (-1) es) (DGroup es)+ where eltWidth (Elt _ pre d post) = T.length pre + width d + T.length post++-- | A value, i.e. a document plus an indication of whether it needs+-- parentheses when used as a constructor argument.+data V = V !Bool Doc++vDoc :: V -> Doc+vDoc (V _ d) = d++-- | Constructor applied to arguments: @hang (text c) 2 (sep args)@,+-- where non-atomic arguments are parenthesized.+con :: Text -> [V] -> V+con c [] = V True (dtext c)+con c vs = V False $ group+ [ Elt 0 "" (dtext c) ""+ , Elt 2 "" (group (map atomElt vs)) "" ]+ where+ atomElt (V True d) = Elt 0 "" d ""+ atomElt (V False d) = Elt 0 "(" d ")"++-- | Bracketed, comma-separated block: @sep [open <+> x1, ...commas..., close]@.+block :: Text -> Text -> Text -> [Doc] -> Doc+block open comma close ds =+ group $ zipWith (\pre d -> Elt 0 pre d "") (open : repeat comma) ds+ ++ [Elt 0 "" (dtext close) ""]++listV :: (a -> V) -> [a] -> V+listV _ [] = V True (dtext "[]")+listV f xs = V True $ block "[ " ", " "]" (map (vDoc . f) xs)++tupleV :: [V] -> V+tupleV vs = V True $ block "( " ", " ")" (map vDoc vs)++-- | Record: @hang (text c) 2 (block '{' '}' fields)@ where each field+-- is @hang (text name <+> char '=') 2 value@. Records count as atoms.+recV :: Text -> [(Text, V)] -> V+recV c fields = V True $ group+ [ Elt 0 "" (dtext c) ""+ , Elt 2 "" (block "{ " ", " "}" (map fieldDoc fields)) "" ]+ where+ fieldDoc (name, v) = group+ [ Elt 0 "" (dtext (name <> " =")) ""+ , Elt 2 "" (vDoc v) "" ]++-- | Leaf rendered via 'show' (Text, Int, Double). Values whose+-- representation starts with @-@ get parentheses in argument position.+showV :: Show a => a -> V+showV x = V (not ("-" `T.isPrefixOf` t)) (dtext t)+ where t = T.pack (show x)++-- | Nullary constructors of enumeration types (and Bool).+enumV :: Show a => a -> V+enumV = V True . dtext . T.pack . show++mapV :: (a -> V) -> M.Map Text a -> V+mapV f m = con "fromList"+ [listV (\(k, v) -> tupleV [showV k, f v]) (M.toAscList m)]++--+-- Conversion of the Pandoc AST+--++pandocV :: Pandoc -> V+pandocV (Pandoc meta blocks) = con "Pandoc" [metaV meta, blocksV blocks]++metaV :: Meta -> V+metaV (Meta m) = recV "Meta" [("unMeta", mapV metaValueV m)]++metaValueV :: MetaValue -> V+metaValueV (MetaMap m) = con "MetaMap" [mapV metaValueV m]+metaValueV (MetaList xs) = con "MetaList" [listV metaValueV xs]+metaValueV (MetaBool b) = con "MetaBool" [enumV b]+metaValueV (MetaString t) = con "MetaString" [showV t]+metaValueV (MetaInlines ils) = con "MetaInlines" [inlinesV ils]+metaValueV (MetaBlocks bs) = con "MetaBlocks" [blocksV bs]++blocksV :: [Block] -> V+blocksV = listV blockV++inlinesV :: [Inline] -> V+inlinesV = listV inlineV++attrV :: Attr -> V+attrV (ident, classes, kvs) =+ tupleV [ showV ident+ , listV showV classes+ , listV (\(k, v) -> tupleV [showV k, showV v]) kvs ]++formatV :: Format -> V+formatV (Format f) = con "Format" [showV f]++blockV :: Block -> V+blockV blk =+ case blk of+ Plain ils -> con "Plain" [inlinesV ils]+ Para ils -> con "Para" [inlinesV ils]+ LineBlock ilss -> con "LineBlock" [listV inlinesV ilss]+ CodeBlock attr t -> con "CodeBlock" [attrV attr, showV t]+ RawBlock f t -> con "RawBlock" [formatV f, showV t]+ BlockQuote bs -> con "BlockQuote" [blocksV bs]+ OrderedList (start, sty, delim) bss ->+ con "OrderedList" [ tupleV [showV start, enumV sty, enumV delim]+ , listV blocksV bss ]+ BulletList bss -> con "BulletList" [listV blocksV bss]+ DefinitionList defs ->+ con "DefinitionList"+ [listV (\(ils, bss) -> tupleV [inlinesV ils, listV blocksV bss]) defs]+ Header lev attr ils -> con "Header" [showV lev, attrV attr, inlinesV ils]+ HorizontalRule -> con "HorizontalRule" []+ Table attr cap colspecs thead tbodies tfoot ->+ con "Table" [ attrV attr+ , captionV cap+ , listV colSpecV colspecs+ , tableHeadV thead+ , listV tableBodyV tbodies+ , tableFootV tfoot ]+ Figure attr cap bs -> con "Figure" [attrV attr, captionV cap, blocksV bs]+ Div attr bs -> con "Div" [attrV attr, blocksV bs]++captionV :: Caption -> V+captionV (Caption mshort bs) =+ con "Caption" [maybeV inlinesV mshort, blocksV bs]++maybeV :: (a -> V) -> Maybe a -> V+maybeV _ Nothing = con "Nothing" []+maybeV f (Just x) = con "Just" [f x]++colSpecV :: ColSpec -> V+colSpecV (align, cw) = tupleV [enumV align, colWidthV cw]++colWidthV :: ColWidth -> V+colWidthV (ColWidth d) = con "ColWidth" [showV d]+colWidthV ColWidthDefault = con "ColWidthDefault" []++tableHeadV :: TableHead -> V+tableHeadV (TableHead attr rows) =+ con "TableHead" [attrV attr, listV rowV rows]++tableBodyV :: TableBody -> V+tableBodyV (TableBody attr (RowHeadColumns rhc) hd bd) =+ con "TableBody" [ attrV attr+ , con "RowHeadColumns" [showV rhc]+ , listV rowV hd+ , listV rowV bd ]++tableFootV :: TableFoot -> V+tableFootV (TableFoot attr rows) =+ con "TableFoot" [attrV attr, listV rowV rows]++rowV :: Row -> V+rowV (Row attr cells) = con "Row" [attrV attr, listV cellV cells]++cellV :: Cell -> V+cellV (Cell attr align (RowSpan rs) (ColSpan cs) bs) =+ con "Cell" [ attrV attr+ , enumV align+ , con "RowSpan" [showV rs]+ , con "ColSpan" [showV cs]+ , blocksV bs ]++inlineV :: Inline -> V+inlineV inln =+ case inln of+ Str t -> con "Str" [showV t]+ Emph ils -> con "Emph" [inlinesV ils]+ Underline ils -> con "Underline" [inlinesV ils]+ Strong ils -> con "Strong" [inlinesV ils]+ Strikeout ils -> con "Strikeout" [inlinesV ils]+ Superscript ils -> con "Superscript" [inlinesV ils]+ Subscript ils -> con "Subscript" [inlinesV ils]+ SmallCaps ils -> con "SmallCaps" [inlinesV ils]+ Quoted qt ils -> con "Quoted" [enumV qt, inlinesV ils]+ Cite cits ils -> con "Cite" [listV citationV cits, inlinesV ils]+ Code attr t -> con "Code" [attrV attr, showV t]+ Space -> con "Space" []+ SoftBreak -> con "SoftBreak" []+ LineBreak -> con "LineBreak" []+ Math mt t -> con "Math" [enumV mt, showV t]+ RawInline f t -> con "RawInline" [formatV f, showV t]+ Link attr ils (url, title) ->+ con "Link" [attrV attr, inlinesV ils, tupleV [showV url, showV title]]+ Image attr ils (url, title) ->+ con "Image" [attrV attr, inlinesV ils, tupleV [showV url, showV title]]+ Note bs -> con "Note" [blocksV bs]+ Span attr ils -> con "Span" [attrV attr, inlinesV ils]++citationV :: Citation -> V+citationV cit = recV "Citation"+ [ ("citationId", showV (citationId cit))+ , ("citationPrefix", inlinesV (citationPrefix cit))+ , ("citationSuffix", inlinesV (citationSuffix cit))+ , ("citationMode", enumV (citationMode cit))+ , ("citationNoteNum", showV (citationNoteNum cit))+ , ("citationHash", showV (citationHash cit)) ]++--+-- Rendering+--++-- | Render a document, reproducing HughesPJ's layout exactly. A+-- group is rendered on one line if its width, plus any text glued+-- after it up to the next line break ('glue'), stays within both the+-- line length (measured from the start of the line) and the ribbon+-- length (measured from the end of the indentation). Otherwise it is+-- rendered vertically, each element after the first starting on a new+-- line, indented by the group's start column plus the element's+-- nesting; each element then makes its own layout decisions.+render :: Int -> Int -> Int -> Doc -> Text+render lineLen ribbonLen glue0 d0 =+ TL.toStrict $ B.toLazyText $ go 0 0 glue0 d0+ where+ go l c g d@(Doc w dc)+ | c + w + g <= lineLen && (c - l) + w + g <= ribbonLen = flat d+ | otherwise =+ case dc of+ DText t -> B.fromText t+ DGroup es -> vertical l c g es++ vertical l0 c0 g = goElts True+ where+ goElts _ [] = mempty+ goElts isFirst (Elt n pre d post : rest) =+ let ind = c0 + n+ (l, c) = if isFirst then (l0, c0) else (ind, ind)+ g' = T.length post + (if null rest then g else 0)+ lead = if isFirst+ then mempty+ else B.singleton '\n' <> B.fromText (T.replicate ind " ")+ in lead <> B.fromText pre <> go l (c + T.length pre) g' d+ <> B.fromText post <> goElts False rest++ flat (Doc _ (DText t)) = B.fromText t+ flat (Doc _ (DGroup es)) =+ mconcat $ intersperse (B.singleton ' ') $ map flatElt es++ flatElt (Elt _ pre d post) =+ B.fromText pre <> flat d <> B.fromText post
@@ -11,15 +11,16 @@ Conversion of 'Pandoc' documents to ODT. -}-module Text.Pandoc.Writers.ODT ( writeODT ) where+module Text.Pandoc.Writers.ODT ( writeODT, writeFODT ) where import Codec.Archive.Zip import Control.Monad.Except (catchError, throwError) import Control.Monad.State.Strict (StateT, evalStateT, gets, modify, lift) import Control.Monad (MonadPlus(mplus))+import Data.ByteString.Base64 (encode) import qualified Data.ByteString.Lazy as B-import Data.Maybe (fromMaybe, listToMaybe)+import Data.Maybe (catMaybes, fromMaybe, listToMaybe, mapMaybe, maybeToList) import Data.Generics (everywhere', mkT)-import Data.List (isPrefixOf)+import Data.List (isPrefixOf, partition) import qualified Data.Map as Map import qualified Data.Text as T import qualified Data.Text.Lazy as TL@@ -38,22 +39,29 @@ HighlightMethod(Skylighting, DefaultHighlighting)) import Text.Pandoc.Highlighting (defaultStyle) import Text.DocLayout-import Text.Pandoc.Shared (stringify, tshow)+import Text.Pandoc.Shared (stringify, stringifyInlines, tshow)+import Text.Pandoc.Templates (compileDefaultTemplate) import Text.Pandoc.Version (pandocVersionText) import Text.Pandoc.Writers.Shared (lookupMetaString, lookupMetaBlocks, fixDisplayMath, getLang, ensureValidXmlIdentifiers)-import Text.Pandoc.UTF8 (fromStringLazy, fromTextLazy, toTextLazy)+import Text.Pandoc.UTF8 (fromStringLazy, fromTextLazy, toText, toTextLazy) import Text.Pandoc.Walk import Text.Pandoc.Writers.OpenDocument (writeOpenDocument) import Text.Pandoc.XML import Text.Pandoc.XML.Light+import qualified Text.Pandoc.XML.Light as XML import Text.TeXMath import qualified Text.XML.Light as XL import Network.URI (parseRelativeReference, URI(uriPath), isURI) import Skylighting -newtype ODTState = ODTState { stEntries :: [Entry]+-- | ODF can be written either as a zip archive of several XML files or+-- as a single flat XML file.+data ODTVariant = Zipped | Flat deriving (Eq, Show)++data ODTState = ODTState { stEntries :: [Entry]+ , stVariant :: ODTVariant } type O m = StateT ODTState m@@ -65,14 +73,16 @@ -> m B.ByteString writeODT opts doc = let initState = ODTState{ stEntries = []+ , stVariant = Zipped } doc' = fixInternalLinks . ensureValidXmlIdentifiers $ doc in- evalStateT (pandocToODT opts doc') initState+ fromArchive <$> evalStateT (pandocToODT opts doc') initState -- | ODT internal links are evaluated relative to an imaginary folder -- structure that mirrors the zip structure. The result is that relative -- links in the document need to start with `..`. See #3524.+-- This does not apply to flat ODF, which is a single file. fixInternalLinks :: Pandoc -> Pandoc fixInternalLinks = walk go where@@ -85,11 +95,209 @@ | not (null (uriPath u)) -> tshow $ u{ uriPath = "../" <> uriPath u } _ -> uri --- | Produce an ODT file from a Pandoc document.+-- | Produce a flat OpenDocument text file (@.fodt@) from a Pandoc+-- document. This is the same content as an ODT, but as a single XML+-- file instead of a zip archive.+writeFODT :: PandocMonad m+ => WriterOptions -- ^ Writer options+ -> Pandoc -- ^ Document to convert+ -> m T.Text+writeFODT opts doc = do+ let initState = ODTState{ stEntries = []+ , stVariant = Flat+ }+ -- note: no fixInternalLinks, since a flat file has no zip+ -- structure for relative references to be resolved against+ doc' = ensureValidXmlIdentifiers doc+ evalStateT (pandocToODT opts doc') initState >>= flattenODT++officeNS :: T.Text+officeNS = "urn:oasis:names:tc:opendocument:xmlns:office:1.0"++officeQName :: T.Text -> QName+officeQName n = QName n (Just officeNS) (Just "office")++-- | The top-level child of an ODF part, e.g. @office:body@ of+-- @content.xml@. Only the local name is matched if the document gives+-- no namespace URI.+officeChild :: T.Text -> Element -> Maybe Element+officeChild name el = listToMaybe+ [ e | Elem e <- elContent el+ , qName (elName e) == name+ , maybe True (== officeNS) (qURI (elName e)) ]++-- | Convert an ODT archive into the equivalent flat OpenDocument file.+--+-- A flat ODF file is a single @office:document@ element whose children+-- are exactly the top-level elements that are otherwise distributed over+-- @content.xml@, @styles.xml@, @meta.xml@ and @settings.xml@, in this+-- order:+--+-- > office:meta? office:settings? office:scripts? office:font-face-decls?+-- > office:styles? office:automatic-styles? office:master-styles? office:body+--+-- Anything that would be a separate archive entry -- images and+-- formulas -- is embedded inline instead.+flattenODT :: forall m . PandocMonad m => Archive -> m T.Text+flattenODT archive = do+ contentRoot <- getRoot "content.xml"+ stylesRoot <- getRoot "styles.xml"+ metaRoot <- getRoot "meta.xml"+ -- the bundled reference.odt has no settings.xml, but a reference doc+ -- written by LibreOffice does+ settingsRoot <- traverse parseEntry (findEntryByPath "settings.xml" archive)+ let roots = [contentRoot, stylesRoot, metaRoot] ++ maybeToList settingsRoot+ -- only one of each of these is allowed in a document, so the ones+ -- from content.xml and styles.xml must be combined+ let (fontFaceDecls, _) = mergeParts (officeChild "font-face-decls" contentRoot)+ (officeChild "font-face-decls" stylesRoot)+ let (automaticStyles, dropped) =+ mergeParts (officeChild "automatic-styles" contentRoot)+ (officeChild "automatic-styles" stylesRoot)+ mapM_ (\e -> report $ UnusualConversion $+ "flat OpenDocument output: dropping the automatic style "+ <> snd (partKey e) <> " from styles.xml, because content.xml "+ <> "defines a style of the same name")+ dropped+ let children = catMaybes+ [ officeChild "meta" metaRoot+ , settingsRoot >>= officeChild "settings"+ , listToMaybe $ mapMaybe (officeChild "scripts") roots+ , fontFaceDecls+ , officeChild "styles" stylesRoot+ , automaticStyles+ , officeChild "master-styles" stylesRoot+ , officeChild "body" contentRoot+ ]+ let root = Element (officeQName "document")+ ( Attr (officeQName "mimetype")+ "application/vnd.oasis.opendocument.text"+ : Attr (officeQName "version") "1.3"+ : nsDecls roots )+ (map (Elem . embedArchiveRefs archive) children)+ Nothing+ -- note: not prettyConfigPP, which would inject whitespace into text+ -- content and thereby change the document+ return $ showTopElement root+ where+ parseEntry :: Entry -> m Element+ parseEntry e =+ case parseXMLElement (toTextLazy (fromEntry e)) of+ Left msg -> throwError $ PandocXMLError (T.pack (eRelativePath e)) msg+ Right el -> return el+ getRoot :: FilePath -> m Element+ getRoot path =+ case findEntryByPath path archive of+ Nothing -> throwError $ PandocSomeError $+ "Could not find " <> T.pack path <> " in the ODT archive"+ Just e -> parseEntry e++-- | The union of the namespace declarations on the given elements.+-- Declarations survive parsing as ordinary attributes with the prefix+-- @xmlns@, and are not synthesised on output, so they must be carried+-- over to the flat file's root by hand. Declarations on nested elements+-- are copied along with those elements.+nsDecls :: [Element] -> [XML.Attr]+nsDecls els = Map.elems $ Map.fromListWith (\_new old -> old)+ [ ((qPrefix (attrKey a), qName (attrKey a)), a)+ | el <- els+ , a <- elAttribs el+ , isNSDecl (attrKey a) ]+ where+ isNSDecl qn = qPrefix qn == Just "xmlns"+ || (qPrefix qn == Nothing && qName qn == "xmlns")++-- | Combine two @office:font-face-decls@ or @office:automatic-styles@+-- elements, preferring the first. Returns the merged element and the+-- declarations of the second that were dropped as duplicates.+mergeParts :: Maybe Element -> Maybe Element -> (Maybe Element, [Element])+mergeParts Nothing mbSecond = (mbSecond, [])+mergeParts mbFirst Nothing = (mbFirst, [])+mergeParts (Just first) (Just second) =+ (Just first{ elContent = elContent first ++ map Elem kept }, dropped)+ where+ keys = map partKey [ e | Elem e <- elContent first ]+ (dropped, kept) = partition ((`elem` keys) . partKey)+ [ e | Elem e <- elContent second ]++-- | Identify a font face or automatic style by its element name and+-- @style:name@.+partKey :: Element -> (T.Text, T.Text)+partKey e = (qName (elName e), partStyleName e)++partStyleName :: Element -> T.Text+partStyleName e = T.concat [ attrVal a+ | a <- elAttribs e+ , qName (attrKey a) == "name"+ , qPrefix (attrKey a) == Just "style" ]++-- | Replace references to other entries of the ODT archive by the+-- entries' contents, which is the only way a flat file can carry them:+-- images become base64 in an @office:binary-data@ child, formulas+-- become MathML inside the @draw:object@.+--+-- Note that this is applied to every copied part, not just the body, so+-- that images used by a reference document's headers, footers and master+-- styles are embedded too. References that do not resolve to an archive+-- entry are left alone; in particular, this is what makes+-- @--link-images@ work, since it writes no @Pictures/@ entry.+embedArchiveRefs :: Archive -> Element -> Element+embedArchiveRefs archive = go+ where+ go el =+ case qName (elName el) of+ "image"+ | Just entry <- flip findEntryByPath archive . T.unpack =<< xlinkHref el+ -> embedImage entry el+ "object"+ | Just path <- formulaPath =<< xlinkHref el+ , Just entry <- findEntryByPath path archive+ , Right mathml <- parseXMLElement (toTextLazy (fromEntry entry))+ -> (dropXlinkAttrs el){ elContent = [Elem mathml] }+ _ -> el{ elContent = map goContent (elContent el) }+ goContent (Elem e) = Elem (go e)+ goContent c = c++ embedImage entry el =+ case dropXlinkAttrs el of+ el' -> el'{ elAttribs = mimeAttrs ++ elAttribs el'+ , elContent = elContent el' ++ [Elem binaryData] }+ where+ mimeAttrs = [ Attr (QName "mime-type" Nothing (Just "draw")) m+ | Just m <- [getMimeType (eRelativePath entry)] ]+ binaryData = Element (officeQName "binary-data") []+ [XML.Text (CData CDataText (base64 (fromEntry entry)) Nothing)]+ Nothing++ base64 = T.intercalate "\n" . T.chunksOf 76 . toText . encode . B.toStrict++ -- the formula's MathML lives in content.xml of the directory the+ -- reference names+ formulaPath href =+ case fromMaybe href (T.stripPrefix "./" href) of+ "" -> Nothing+ h -> Just $ T.unpack $+ (if "/" `T.isSuffixOf` h then h else h <> "/") <> "content.xml"++xlinkHref :: Element -> Maybe T.Text+xlinkHref el = listToMaybe [ attrVal a+ | a <- elAttribs el+ , qName (attrKey a) == "href"+ , qPrefix (attrKey a) == Just "xlink" ]++-- | Drop the attributes with which the ODT writer refers to an archive+-- entry; the content is about to be embedded instead.+dropXlinkAttrs :: Element -> Element+dropXlinkAttrs el = el{ elAttribs = filter (not . isRef . attrKey) (elAttribs el) }+ where+ isRef qn = qPrefix qn == Just "xlink"+ && qName qn `elem` ["href", "type", "show", "actuate"]++-- | Produce an ODT archive from a Pandoc document. pandocToODT :: PandocMonad m => WriterOptions -- ^ Writer options -> Pandoc -- ^ Document to convert- -> O m B.ByteString+ -> O m Archive pandocToODT opts doc@(Pandoc meta _) = do let title = docTitle meta let authors = docAuthors meta@@ -97,15 +305,29 @@ lang <- toLang (getLang opts meta) refArchive <- case writerReferenceDoc opts of- Just f -> lift $ toArchive . B.fromStrict . fst <$>+ Just f -> do+ arch <- lift $ toArchiveOrFail . B.fromStrict . fst <$> (P.fetchItem (T.pack f))- Nothing -> lift $ toArchive . B.fromStrict <$>+ case arch of+ Left err -> throwError $ PandocParseError $+ "Could not parse reference-doc " <>+ tshow f <> ": " <> T.pack err+ Right x -> pure x+ Nothing -> do+ arch <- lift $ toArchiveOrFail . B.fromStrict <$> readDataFile "reference.odt"+ case arch of+ Left err -> throwError $ PandocParseError $+ "Could not parse reference.odt: " <> T.pack err+ Right x -> pure x -- handle formulas and pictures -- picEntriesRef <- P.newIORef ([] :: [Entry]) let refTextWidth = referenceTextWidthPt opts refArchive doc' <- walkM (transformPicMath opts refTextWidth) $ walk fixDisplayMath doc- newContents <- lift $ writeOpenDocument opts{writerWrapText = WrapNone} doc'+ tpl <- maybe (lift $ compileDefaultTemplate "opendocument") pure+ (writerTemplate opts)+ newContents <- lift $ writeOpenDocument+ opts{ writerWrapText = WrapNone, writerTemplate = Just tpl } doc' epochtime <- floor `fmap` lift P.getPOSIXTime let contentEntry = toEntry "content.xml" epochtime $ fromTextLazy $ TL.fromStrict newContents@@ -165,7 +387,7 @@ ,("office:version","1.3")] ( inTags True "office:meta" [] ( metaTag "meta:generator" ("Pandoc/" <> pandocVersionText) $$- metaTag "dc:title" (stringify title)+ metaTag "dc:title" (stringifyInlines title) $$ metaTag "dc:description" (T.intercalate "\n" (map stringify $@@ -184,7 +406,7 @@ $$ metaTag "meta:creation-date" d $$ metaTag "dc:date" d ) (T.pack $ formatTime defaultTimeLocale "%FT%XZ" utctime)- (T.intercalate "; " (map stringify authors))+ (T.intercalate "; " (map stringifyInlines authors)) $$ vcat userDefinedMeta )@@ -195,7 +417,7 @@ archive'' <- updateStyle opts lang $ addEntryToArchive mimetypeEntry $ addEntryToArchive metaEntry archive'- return $ fromArchive archive''+ return archive'' updateStyle :: forall m . PandocMonad m => WriterOptions -> Maybe Lang -> Archive -> O m Archive@@ -367,7 +589,13 @@ case T.unpack src of s | isURI s -> return src | isAbsolute s -> return src- | otherwise -> return $ T.pack $ ".." </> s+ | otherwise -> do+ -- see fixInternalLinks: relative references+ -- are resolved against the zip structure+ variant <- gets stVariant+ case variant of+ Zipped -> return $ T.pack $ ".." </> s+ Flat -> return $ T.pack s else do entries <- gets stEntries let extension = maybe (takeExtension $ takeWhile (/='?') $ T.unpack src) T.unpack
@@ -89,8 +89,8 @@ type NameSpaces = [(Text, Text)] --- | Scales the image to fit the page--- sizes are passed in emu+-- | Scales the image to fit the page if it would overflow.+-- Sizes are passed in emu. fitToPage :: (Double, Double) -> Integer -> (Integer, Integer) fitToPage (x, y) pageWidth -- Fixes width to the page width and scales the height
@@ -1,5 +1,4 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE CPP #-} {- | Module : Text.Pandoc.Writers.OPML Copyright : Copyright (C) 2013-2024 John MacFarlane@@ -30,9 +29,7 @@ -- | Convert Pandoc document to string in OPML format. writeOPML :: PandocMonad m => WriterOptions -> Pandoc -> m Text writeOPML opts (Pandoc meta blocks) = do- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing+ let colwidth = wrapColumns opts meta' = B.setMeta "date" (B.str $ convertDate $ docDate meta) meta metadata <- metaToContext opts (fmap literal . writeMarkdown' opts . Pandoc nullMeta)@@ -65,7 +62,8 @@ convertDate :: [Inline] -> Text convertDate ils = maybe "" showDateTimeRFC822 $- parseTimeM True defaultTimeLocale "%F" . T.unpack =<< normalizeDate (stringify ils)+ parseTimeM True defaultTimeLocale "%F" . T.unpack =<<+ normalizeDate (stringifyInlines ils) -- | Convert a Block to OPML. blockToOPML :: PandocMonad m => WriterOptions -> Block -> m (Doc Text)
@@ -14,11 +14,10 @@ Conversion of 'Pandoc' documents to OpenDocument XML. -} module Text.Pandoc.Writers.OpenDocument ( writeOpenDocument ) where-import Control.Arrow ((***), (>>>))-import Control.Monad (unless, liftM)+import Control.Monad (unless, liftM, when) import Control.Monad.State.Strict ( StateT(..), modify, gets, lift )-import Data.Char (chr, isDigit)-import Data.Foldable (find)+import Data.Char (chr, isDigit, isLetter, isMark, ord)+import Numeric (showHex) import Data.List (sortOn, sortBy) import qualified Data.List as L import qualified Data.Map as Map@@ -34,8 +33,7 @@ import Text.Pandoc.Logging import Text.Pandoc.Options import Text.DocLayout hiding (link)-import Text.Pandoc.Shared (linesToPara, tshow, blocksToInlines)-import Text.Pandoc.Templates (renderTemplate)+import Text.Pandoc.Shared (linesToPara, plainToPara, tshow, blocksToInlines) import qualified Text.Pandoc.Translations as Term (Term(Figure, Table)) import Text.Pandoc.Walk import Text.Pandoc.Writers.Math@@ -46,11 +44,6 @@ import Text.Pandoc.Highlighting (highlight) import Skylighting (FormatOptions(..), SourceLine, Token) --- | Auxiliary function to convert Plain block to Para.-plainToPara :: Block -> Block-plainToPara (Plain x) = Para x-plainToPara x = x- -- Names of predefined styles in the reference.odt; see data/odt/styles.xml. defaultBulletListStyleName, defaultNumberedListStyleName :: Text defaultBulletListStyleName = "List_20_1"@@ -89,63 +82,65 @@ deriving (Show, Eq, Ord) data WriterState =- WriterState { stNotes :: [Doc Text]+ WriterState { stNoteCount :: Int , stTableStyles :: [Doc Text]+ , stTableStyleCount :: Int , stParaStyles :: [Doc Text]+ , stParaStyleCount :: Int+ , stParaStyleCache :: Map.Map ([(Text,Text)],[(Text,Text)]) Text+ -- ^ cache of automatic paragraph styles, keyed on+ -- (style attributes, paragraph properties) , stListOverrides :: Map.Map (ListNumberStyle,ListNumberDelim) (Text, Doc Text) , stTextStyles :: Map.Map (Set.Set TextStyle)- (Text, Doc Text)+ (Int, Doc Text)+ -- ^ automatic text styles, with the number that+ -- makes up the style's @Tn@ name , stTextStyleAttr :: Set.Set TextStyle , stIndentPara :: Int , stInDefinition :: Bool- , stTight :: Bool , stFirstPara :: Bool , stImageId :: Int , stTableCaptionId :: Int , stImageCaptionId :: Int- , stIdentTypes :: [(Text,ReferenceType)]+ , stIdentTypes :: Map.Map Text ReferenceType , stDirection :: Maybe Direction -- ^ active writing mode- , stDirStyles :: Map.Map (Text, Direction) Text- -- ^ cache of direction-adjusted paragraph styles,- -- keyed on (parent style, writing mode) } defaultWriterState :: WriterState defaultWriterState =- WriterState { stNotes = []+ WriterState { stNoteCount = 0 , stTableStyles = []+ , stTableStyleCount = 0 , stParaStyles = []+ , stParaStyleCount = 0+ , stParaStyleCache = Map.empty , stListOverrides = Map.empty , stTextStyles = Map.empty , stTextStyleAttr = Set.empty , stIndentPara = 0 , stInDefinition = False- , stTight = False , stFirstPara = False , stImageId = 1 , stTableCaptionId = 1 , stImageCaptionId = 1- , stIdentTypes = []+ , stIdentTypes = Map.empty , stDirection = Nothing- , stDirStyles = Map.empty } -when :: Bool -> Doc Text -> Doc Text-when p a = if p then a else empty- addTableStyle :: PandocMonad m => Doc Text -> OD m ()-addTableStyle i = modify $ \s -> s { stTableStyles = i : stTableStyles s }--addNote :: PandocMonad m => Doc Text -> OD m ()-addNote i = modify $ \s -> s { stNotes = i : stNotes s }+addTableStyle i = modify $ \s ->+ s { stTableStyles = i : stTableStyles s+ , stTableStyleCount = stTableStyleCount s + 1 } addParaStyle :: PandocMonad m => Doc Text -> OD m ()-addParaStyle i = modify $ \s -> s { stParaStyles = i : stParaStyles s }+addParaStyle i = modify $ \s ->+ s { stParaStyles = i : stParaStyles s+ , stParaStyleCount = stParaStyleCount s + 1 } addTextStyle :: PandocMonad m- => Set.Set TextStyle -> (Text, Doc Text) -> OD m ()+ => Set.Set TextStyle -> (Int, Doc Text) -> OD m () addTextStyle attrs i = modify $ \s -> s { stTextStyles = Map.insert attrs i (stTextStyles s) } @@ -216,11 +211,13 @@ Nothing -> do styles <- gets stTextStyles case Map.lookup at styles of- Just (styleName, _) -> return $- inTags False "text:span" [("text:style-name",styleName)] d+ Just (num, _) -> return $+ inTags False "text:span"+ [("text:style-name", textStyleName num)] d Nothing -> do- let styleName = "T" <> tshow (Map.size styles + 1)- addTextStyle at (styleName,+ let num = Map.size styles + 1+ styleName = textStyleName num+ addTextStyle at (num, inTags False "style:style" [("style:name", styleName) ,("style:family", "text")]@@ -230,6 +227,10 @@ return $ inTags False "text:span" [("text:style-name",styleName)] d +-- | Name of the @n@th automatic text style.+textStyleName :: Int -> Text+textStyleName num = "T" <> tshow num+ formulaStyles :: [Doc Text] formulaStyles = [formulaStyle InlineMath, formulaStyle DisplayMath] @@ -274,30 +275,35 @@ handleSpaces :: Text -> Doc Text handleSpaces s = case T.uncons s of Just (' ', _) -> genTag s- Just ('\t',x) -> selfClosingTag "text:tab" [] <> rm x+ Just ('\t',x) -> tabTag <> rm x _ -> rm s where- genTag = T.span (==' ') >>> tag . T.length *** rm >>> uncurry (<>)- tag n = when (n /= 0) $ selfClosingTag "text:s" [("text:c", tshow n)]- rm t = case T.uncons t of- Just ( ' ',xs) -> char ' ' <> genTag xs- Just ('\t',xs) -> selfClosingTag "text:tab" [] <> genTag xs- Just ( x,xs) -> char x <> rm xs- Nothing -> empty+ tabTag = selfClosingTag "text:tab" []+ -- collapse a run of spaces into a single <text:s>+ genTag t = let (spaces, t') = T.span (== ' ') t+ in tag (T.length spaces) <> rm t'+ tag 0 = empty+ tag n = selfClosingTag "text:s" [("text:c", tshow n)]+ -- emit text verbatim up to the next space or tab; a lone space is+ -- kept literal and any further ones become a <text:s>+ rm t = let (chunk, t') = T.break (\c -> c == ' ' || c == '\t') t+ pre = if T.null chunk then empty else literal chunk+ in case T.uncons t' of+ Just (' ', xs) -> pre <> char ' ' <> genTag xs+ Just (_ , xs) -> pre <> tabTag <> genTag xs+ Nothing -> pre -- | Convert Pandoc document to string in OpenDocument format. writeOpenDocument :: PandocMonad m => WriterOptions -> Pandoc -> m Text writeOpenDocument opts (Pandoc meta blocks) = do setupTranslations meta- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing- let meta' = case lookupMetaBlocks "abstract" meta of- [] -> meta- xs -> B.setMeta "abstract"- (B.divWith ("",[],[("custom-style","Abstract")])- (B.fromList xs))- meta+ -- The abstract is the one template field that takes block-level+ -- content, so it is rendered separately; everything else in the+ -- metadata goes in a context where only inlines are allowed.+ let abstractBlocks = case lookupMetaBlocks "abstract" meta of+ [] -> []+ xs -> [Div ("",[],[("custom-style","Abstract")]) xs]+ let meta' = B.deleteMeta "abstract" meta -- Set the default writing direction from the "dir" metadata field; -- in its absence, a right-to-left main language implies RTL. let mbDir = case lookupMetaString "dir" meta of@@ -307,19 +313,30 @@ Just l | Right lang <- parseLang l , isRTLLang lang -> Just RTL _ -> Nothing- ((body, metadata),s) <- flip runStateT+ ((body, abstract, metadata),s) <- flip runStateT defaultWriterState{ stDirection = mbDir } $ do- let collectBlockIdent (Header _ (ident,_,_) _) = [(ident,HeaderRef)]- collectBlockIdent (Figure (ident,_,_) _ _ ) = [(ident,FigureRef)]- collectBlockIdent (Table (ident,_,_) _ _ _ _ _) = [(ident,TableRef)]- collectBlockIdent _ = []- modify $ \s -> s{ stIdentTypes = query collectBlockIdent blocks }+ let collectBlockIdent (Header _ attr _) = identRef HeaderRef attr+ collectBlockIdent (Figure attr _ _ ) = identRef FigureRef attr+ collectBlockIdent (Table attr _ _ _ _ _) = identRef TableRef attr+ collectBlockIdent _ = []+ identRef ty (ident,_,_)+ | T.null ident = []+ | otherwise = [(ident, ty)]+ -- only needed to resolve cross references+ when (isEnabled Ext_xrefs_name opts ||+ isEnabled Ext_xrefs_number opts) $+ modify $ \s -> s{ stIdentTypes =+ Map.fromListWith (\_ old -> old)+ (query collectBlockIdent blocks) } m <- metaToContext opts (inlinesToOpenDocument opts . blocksToInlines) (fmap chomp . inlinesToOpenDocument opts) meta'+ a <- blocksToOpenDocument opts abstractBlocks b <- blocksToOpenDocument opts blocks- return (b, m)+ return (b, a, m)+ -- 'styles' is reversed below, so the text styles are put in+ -- descending order here to come out as T1, T2, ..., T10 let styles = stTableStyles s ++ stParaStyles s ++ formulaStyles ++ map snd (sortBy (comparing (Down . fst)) ( Map.elems (stTextStyles s)))@@ -329,22 +346,25 @@ . defField "toc" (writerTableOfContents opts) . defField "toc-depth" (tshow $ writerTOCDepth opts) . defField "automatic-styles" automaticStyles+ . (if null abstractBlocks+ then id+ else defField "abstract" abstract) $ metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> body- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context body withParagraphStyle :: PandocMonad m => WriterOptions -> Text -> [Block] -> OD m (Doc Text) withParagraphStyle o s bs = do s' <- dirStyleFor s+ -- 'Plain' is treated like 'Para' so that the requested style is not+ -- lost (e.g. for metadata fields, figure bodies, or tight list items) let go (b:bs')- | Para l <- b = cont bs' =<<- inParagraphTagsWithStyle s' <$> inlinesToOpenDocument o l- | otherwise = cont bs' =<< blockToOpenDocument o b+ | Para l <- b = cont bs' =<< styled l+ | Plain l <- b = cont bs' =<< styled l+ | otherwise = cont bs' =<< blockToOpenDocument o b go [] = return empty cont bs' i = (<>) i <$> go bs'+ styled l = inParagraphTagsWithStyle s' <$> inlinesToOpenDocument o l go bs inPreformattedTags :: [Doc Text] -> Doc Text@@ -517,7 +537,7 @@ HorizontalRule -> setFirstPara >> return (selfClosingTag "text:p" [ ("text:style-name", "Horizontal_20_Line") ]) b@(RawBlock f s) -> if f == Format "opendocument"- then return $ text $ T.unpack s+ then return $ literal s else empty <$ report (BlockNotRendered b) Figure a capt b -> figure o a capt b where@@ -531,7 +551,7 @@ let (ident,_,kvs) = attr i = withDirFromAttr attr $ withLangFromAttr attr $ case lookup "custom-style" kvs of- Just sty -> withParagraphStyle o sty s+ Just sty -> withParagraphStyle o (encodeStyleName sty) s _ -> blocksToOpenDocument o s mkBookmarkedDiv = inTags False "text:section" [("text:name", ident)] if T.null ident@@ -550,9 +570,9 @@ table :: PandocMonad m => WriterOptions -> Ann.Table -> OD m (Doc Text) table opts (Ann.Table (ident, _, _) (Caption _ c) colspecs thead tbodies tfoot) = do- tn <- length <$> gets stTableStyles- pn <- length <$> gets stParaStyles- let genIds = map chr [65..]+ tn <- gets stTableStyleCount+ pn <- gets stParaStyleCount+ let genIds = map colLetters [0..] name = "Table" <> tshow (tn + 1) (aligns, mwidths) = unzip colspecs fromWidth (ColWidth w) | w > 0 = w@@ -560,7 +580,7 @@ widths = map fromWidth mwidths textWidth = sum widths columnIds = zip genIds widths- mkColumn n = selfClosingTag "table:table-column" [("table:style-name", name <> "." <> T.singleton (fst n))]+ mkColumn n = selfClosingTag "table:table-column" [("table:style-name", name <> "." <> fst n)] columns = map mkColumn columnIds paraHStyles = paraTableStyles "Heading" pn aligns paraStyles = paraTableStyles "Contents" (pn + length (newPara paraHStyles)) aligns@@ -589,8 +609,7 @@ [] -> withParagraphStyle o "Figure" body caption -> do- imageDoc <- withParagraphStyle o "FigureWithCaption" $- map (\case {Plain i -> Para i; b -> b}) body+ imageDoc <- withParagraphStyle o "FigureWithCaption" body captionDoc <- inlinesToOpenDocument o caption >>= if isEnabled Ext_native_numbering o then numberedFigureCaption ident@@ -619,7 +638,7 @@ numberedCaption :: Text -> Text -> Text -> Int -> Text -> Doc Text -> Doc Text numberedCaption style term name num ident caption =- let t = text $ T.unpack term+ let t = literal term r = num - 1 ident' = case ident of "" -> "ref" <> name <> tshow r@@ -776,7 +795,7 @@ inlinesToOpenDocument o Cite _ l -> inlinesToOpenDocument o l RawInline f s -> if f == Format "opendocument"- then return $ text $ T.unpack s+ then return $ literal s else do report $ InlineNotRendered ils return empty@@ -814,7 +833,7 @@ let (ident,_,kvs) = attr i = maybe id (\sty -> fmap (inTags False "text:span"- [ ("text:style-name", sty) ]))+ [ ("text:style-name", encodeStyleName sty) ])) (lookup "custom-style" kvs) . withLangFromAttr attr $ inlinesToOpenDocument o xs mkBookmarkedSpan b =@@ -825,16 +844,17 @@ then i else fmap mkBookmarkedSpan i mkNote l = do- n <- length <$> gets stNotes+ n <- gets stNoteCount+ -- bump the counter before rendering the body, so that a note+ -- nested inside this one does not reuse the same text:id+ modify $ \st -> st{ stNoteCount = n + 1 } let footNote t = inTags False "text:note" [ ("text:id" , "ftn" <> tshow n) , ("text:note-class", "footnote" )] $ inTagsSimple "text:note-citation" (text . show $ n + 1) <> inTagsSimple "text:note-body" t- nn <- footNote <$> withAlteredTextStyles (const mempty)- (withParagraphStyle o "Footnote" l)- addNote nn- return nn+ footNote <$> withAlteredTextStyles (const mempty)+ (withParagraphStyle o "Footnote" l) formatOpenDocument :: FormatOptions -> [SourceLine] -> [[Doc Text]] formatOpenDocument _fmtOpts = map (map toHlTok)@@ -845,12 +865,10 @@ preformatted :: Text -> Doc Text preformatted s = handleSpaces $ escapeStringForXML s -mkLink :: WriterOptions -> [(Text,ReferenceType)] -> Text -> Text -> Doc Text -> Doc Text+mkLink :: WriterOptions -> Map.Map Text ReferenceType -> Text -> Text+ -> Doc Text -> Doc Text mkLink o identTypes s t d =- let maybeIdentAndType = case T.uncons s of- Just ('#', ident) -> find ((ident ==) . fst) identTypes- _ -> Nothing- d' = inSpanTags "Definition" d+ let d' = inSpanTags "Definition" d ref refType format ident = inTags False refType [ ("text:reference-format", format ), ("text:ref-name", ident) ]@@ -875,16 +893,27 @@ , ("xlink:href" , s ) , ("office:name", t ) ] d'- linkOrReference = case maybeIdentAndType of- Just (ident, HeaderRef) -> bookmarkRef' ident- Just (ident, TableRef) -> sequenceRef' ident- Just (ident, FigureRef) -> sequenceRef' ident- _ -> link+ linkOrReference = case T.uncons s of+ Just ('#', ident) ->+ case Map.lookup ident identTypes of+ Just HeaderRef -> bookmarkRef' ident+ Just TableRef -> sequenceRef' ident+ Just FigureRef -> sequenceRef' ident+ Nothing -> link+ _ -> link in if isEnabled Ext_xrefs_name o || isEnabled Ext_xrefs_number o then linkOrReference else link -tableStyle :: Int -> Double -> [(Char,Double)] -> Doc Text+-- | Spreadsheet-style column names (@A@, @B@, ..., @Z@, @AA@, @AB@, ...).+-- Plain @chr@ arithmetic would run past @Z@ into characters like @[@+-- and @\\@, which are not legal in a @style:name@.+colLetters :: Int -> Text+colLetters n+ | n < 26 = T.singleton (chr (65 + n))+ | otherwise = colLetters (n `div` 26 - 1) <> T.singleton (chr (65 + n `mod` 26))++tableStyle :: Int -> Double -> [(Text,Double)] -> Doc Text tableStyle num textWidth wcs = let tableId = "Table" <> tshow (num + 1) tableWidthAttr :: [(Text,Text)]@@ -898,10 +927,10 @@ selfClosingTag "style:table-properties" (("table:align", "center") : tableWidthAttr) colStyle (c,0) = selfClosingTag "style:style"- [ ("style:name" , tableId <> "." <> T.singleton c)+ [ ("style:name" , tableId <> "." <> c) , ("style:family", "table-column" )] colStyle (c,w) = inTags True "style:style"- [ ("style:name" , tableId <> "." <> T.singleton c)+ [ ("style:name" , tableId <> "." <> c) , ("style:family", "table-column" )] $ selfClosingTag "style:table-column-properties" [("style:rel-column-width", T.pack $ printf "%d*" (floor $ w * 65535 :: Integer))]@@ -930,32 +959,18 @@ paraStyle attrs = do i <- (*) (0.5 :: Double) . fromIntegral <$> gets stIndentPara b <- gets stInDefinition- t <- gets stTight dirAttrs <- getDirAttrs let indentVal = flip (<>) "in" . tshow $ if b then max 0.5 i else i- tight = if t then [ ("fo:margin-top" , "0in" )- , ("fo:margin-bottom" , "0in" )]- else [] indent = if i /= 0 || b then [ ("fo:margin-left" , indentVal) , ("fo:margin-right" , "0in" ) , ("fo:text-indent" , "0in" ) , ("style:auto-text-indent" , "false" )] else []- attributes = indent <> tight <> dirAttrs+ attributes = indent <> dirAttrs case (attributes, attrs) of ([], [("style:parent-style-name", parent)]) -> return parent- _ -> do- pn <- (+) 1 . length <$> gets stParaStyles- let name = "P" <> tshow pn- styleAttr = [ ("style:name" , name)- , ("style:family", "paragraph") ]- paraProps = if null attributes- then mempty- else selfClosingTag- "style:paragraph-properties" attributes- addParaStyle $ inTags True "style:style" (styleAttr <> attrs) paraProps- return name+ _ -> mkParaStyle attrs attributes paraStyleFromParent :: PandocMonad m => Text -> [(Text,Text)] -> OD m Text paraStyleFromParent parent attrs = do@@ -963,14 +978,29 @@ let attrs' = attrs <> dirAttrs if null attrs' then return parent- else do- pn <- (+) 1 . length <$> gets stParaStyles- let name = "P" <> tshow pn- styleAttr = [ ("style:name" , name)- , ("style:family" , "paragraph")- , ("style:parent-style-name", parent)]- paraProps = selfClosingTag "style:paragraph-properties" attrs'+ else mkParaStyle [("style:parent-style-name", parent)] attrs'++-- | Create an automatic paragraph style with the given @style:style@+-- attributes and @style:paragraph-properties@, reusing an identical+-- style if one has already been created.+mkParaStyle :: PandocMonad m => [(Text,Text)] -> [(Text,Text)] -> OD m Text+mkParaStyle attrs props = do+ cache <- gets stParaStyleCache+ case Map.lookup (attrs, props) cache of+ Just name -> return name+ Nothing -> do+ name <- ("P" <>) . tshow . (+ 1) <$> gets stParaStyleCount+ let styleAttr = ("style:name" , name)+ : ("style:family", "paragraph")+ : attrs+ paraProps = if null props+ then mempty+ else selfClosingTag+ "style:paragraph-properties" props addParaStyle $ inTags True "style:style" styleAttr paraProps+ modify $ \st -> st{ stParaStyleCache =+ Map.insert (attrs, props) name+ (stParaStyleCache st) } return name getDirAttrs :: PandocMonad m => OD m [(Text, Text)]@@ -984,28 +1014,31 @@ Just LTR -> [("style:writing-mode", "lr-tb"), ("fo:text-align", "left")] +-- | Encode a style name as LibreOffice does: characters not allowed+-- in an XML name are replaced by their code point in hex between+-- underscores, so that, e.g., @Text body@ becomes @Text_20_body@.+encodeStyleName :: Text -> Text+encodeStyleName t = case T.uncons t of+ Nothing -> t+ Just (c, cs) -> enc isStartChar c <> T.concatMap (enc isNameChar) cs+ where+ enc ok c+ | ok c = T.singleton c+ | otherwise = "_" <> T.pack (showHex (ord c) "") <> "_"+ isStartChar c = isLetter c || c == '_'+ isNameChar c = isStartChar c || isDigit c || isMark c+ || c == '-' || c == '.'+ -- | Adjust a named paragraph style for the current writing direction. -- When a direction is active, an automatic style derived from the--- given style with the appropriate @style:writing-mode@ is created--- (and cached). Automatic style names (@P1@, @P2@, ...) pass through--- unchanged, since automatic styles are always created with the--- current direction included.+-- given style with the appropriate @style:writing-mode@ is created.+-- With no direction active the name passes through unchanged, as do+-- automatic style names (@P1@, @P2@, ...), since automatic styles are+-- always created with the current direction included. dirStyleFor :: PandocMonad m => Text -> OD m Text-dirStyleFor parent = do- mbDir <- gets stDirection- case mbDir of- Nothing -> return parent- Just d- | isAutoStyleName parent -> return parent- | otherwise -> do- cache <- gets stDirStyles- case Map.lookup (parent, d) cache of- Just name -> return name- Nothing -> do- name <- paraStyleFromParent parent []- modify $ \st -> st{ stDirStyles =- Map.insert (parent, d) name (stDirStyles st) }- return name+dirStyleFor parent+ | isAutoStyleName parent = return parent+ | otherwise = paraStyleFromParent parent [] where isAutoStyleName t = case T.uncons t of Just ('P', ds) -> not (T.null ds) && T.all isDigit ds
@@ -33,13 +33,13 @@ import Text.Pandoc.Options import Text.Pandoc.Shared import Text.Pandoc.URI-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Citeproc.Locator (parseLocator, LocatorMap(..), LocatorInfo(..)) import Text.Pandoc.Walk (query) import Text.Pandoc.Writers.Shared data WriterState = WriterState { stNotes :: [[Block]]+ , stNoteNum :: Int , stHasMath :: Bool , stOptions :: WriterOptions }@@ -50,6 +50,7 @@ writeOrg :: PandocMonad m => WriterOptions -> Pandoc -> m Text writeOrg opts document = do let st = WriterState { stNotes = [],+ stNoteNum = 0, stHasMath = False, stOptions = opts } evalStateT (pandocToOrg document) st@@ -58,15 +59,12 @@ pandocToOrg :: PandocMonad m => Pandoc -> Org m Text pandocToOrg (Pandoc meta blocks) = do opts <- gets stOptions- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing metadata <- metaToContext opts blockListToOrg (fmap chomp . inlineListToOrg) meta body <- blockListToOrg blocks- notes <- gets (reverse . stNotes) >>= notesToOrg+ notes <- notesToOrg hasMath <- gets stHasMath let main = body $+$ notes let context = defField "body" main@@ -83,15 +81,21 @@ . defField "option-special-strings" (isEnabled Ext_special_strings opts) $ metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main --- | Return Org representation of notes.-notesToOrg :: PandocMonad m => [[Block]] -> Org m (Doc Text)-notesToOrg notes =- vsep <$> zipWithM noteToOrg [1..] notes+-- | Return Org representation of the collected notes. Rendering a+-- note may add further notes to the state (notes nested inside+-- notes); keep going until all of them have been rendered.+notesToOrg :: PandocMonad m => Org m (Doc Text)+notesToOrg = vsep <$> go 0+ where+ go done = do+ notes <- gets (drop done . reverse . stNotes)+ if null notes+ then return []+ else do+ docs <- zipWithM noteToOrg [done + 1 ..] notes+ (docs ++) <$> go (done + length notes) -- | Return Org representation of a note. noteToOrg :: PandocMonad m => Int -> [Block] -> Org m (Doc Text)@@ -114,14 +118,19 @@ -- | Escape special characters for Org. escapeString :: Text -> Doc Text-escapeString t- | T.all isAlphaNum t = literal t- | otherwise = mconcat $ map escChar (T.unpack t)+escapeString t =+ case T.break isSpecial t of+ (_, "") -> literal t+ (pre, post) ->+ case T.uncons post of+ -- escape special chars with ZERO WIDTH SPACE as org manual+ -- suggests+ Just (c, rest) -> (if T.null pre then mempty else literal pre)+ <> afterBreak "\x200B" <> char c+ <> escapeString rest+ Nothing -> literal pre -- not reachable where- -- escape special chars with ZERO WIDTH SPACE as org manual suggests- escChar c = if c == '*' || c == '#' || c == '|'- then afterBreak "\x200B" <> char c- else char c+ isSpecial c = c == '*' || c == '#' || c == '|' isRawFormat :: Format -> Bool isRawFormat f =@@ -161,8 +170,8 @@ return $ blankline $$ "#+begin_verse" $$ nest 2 contents $$ "#+end_verse" <> blankline blockToOrg (RawBlock "html" str) =- return $ blankline $$ "#+begin_html" $$- nest 2 (literal str) $$ "#+end_html" $$ blankline+ return $ blankline $$ "#+begin_export html" $$+ nest 2 (literal str) $$ "#+end_export" $$ blankline blockToOrg b@(RawBlock f str) | isRawFormat f = return $ literal str | otherwise = do@@ -213,13 +222,18 @@ let (beg, end) = case lang of Nothing -> ("#+begin_example" <> numberlines, "#+end_example") Just x -> ("#+begin_src " <> x <> numberlines <> args, "#+end_src")- -- escape special lines+ -- Escape special lines by prepending a comma. Like Emacs'+ -- org-escape-code-in-region, escape all lines consisting of+ -- indentation, then any number of commas, then "*" or "#+"; this+ -- keeps lines that already start with commas intact when the block+ -- is unescaped again.+ let needsEscape t = let t' = T.dropWhile (== ',') t+ in T.isPrefixOf "#+" t' || T.isPrefixOf "*" t' let escape_line line = let (spaces, code) = T.span (\c -> c == ' ' || c == '\t') line- in spaces <>- (if T.isPrefixOf "#+" code || T.isPrefixOf "*" code- then T.cons ',' code- else code)+ in if needsEscape code+ then spaces <> T.cons ',' code+ else line let escaped = T.unlines . map escape_line . T.lines $ str return $ name $$ literal beg $$ literal escaped $$ literal end $$ blankline blockToOrg (BlockQuote blocks) = do@@ -246,8 +260,7 @@ middle = hcat $ intersperse sep' blocks let makeRow = hpipeBlocks . zipWith lblock widthsInChars let head' = makeRow headers'- rows' <- mapM (\row -> do cols <- mapM blockListToOrg row- return $ makeRow cols) rows+ let rows' = map makeRow rawRows let border ch = char '|' <> char ch <> (hcat . intersperse (char ch <> char '+' <> char ch) $ map (\l -> text $ replicate l ch) widthsInChars) <>@@ -476,13 +489,14 @@ shouldFix Note{} = True -- Prevent footnotes shouldFix (Str "-") = True -- Prevent bullet list items shouldFix (Str x) -- Prevent ordered list items- | Just (cs, c) <- T.unsnoc x = T.all isDigit cs &&+ | Just (cs, c) <- T.unsnoc x = not (T.null cs) &&+ T.all isDigit cs && (c == '.' || c == ')') shouldFix _ = False -- | Convert Pandoc inline element to Org. inlineToOrg :: PandocMonad m => Inline -> Org m (Doc Text)-inlineToOrg (Span (uid, [], []) []) =+inlineToOrg (Span (uid, [], []) []) | not (T.null uid) = return $ "<<" <> literal uid <> ">>" inlineToOrg (Span _ lst) = inlineListToOrg lst@@ -548,7 +562,13 @@ _ -> mempty return $ "[cite" <> sty <> ":" <> citeItems <> "]" else inlineListToOrg lst-inlineToOrg (Code _ str) = return $ "=" <> literal str <> "="+inlineToOrg (Code _ str) = return $+ -- Org offers no escape mechanism inside verbatim text; if the+ -- content contains the delimiter, fall back to the other verbatim+ -- delimiter.+ if "=" `T.isInfixOf` str && not ("~" `T.isInfixOf` str)+ then "~" <> literal str <> "~"+ else "=" <> literal str <> "=" inlineToOrg (Str str) = do opts <- gets stOptions let str' = if isEnabled Ext_smart opts || isEnabled Ext_special_strings opts@@ -578,17 +598,49 @@ inlineToOrg (Link _ txt (src, _)) = case txt of [Str x] | escapeURI x == src -> -- autolink- return $ "[[" <> literal (orgPath x) <> "]]"- _ -> do contents <- nowrap <$> inlineListToOrg txt- return $ "[[" <> literal (orgPath src) <> "][" <> contents <> "]]"+ return $ "[[" <> literal (escapeLinkTarget (orgPath x)) <> "]]"+ _ -> do descr <- render Nothing . nowrap <$> inlineListToOrg txt+ return $ "[[" <> literal (escapeLinkTarget (orgPath src)) <>+ "][" <> literal (escapeLinkDescription descr) <> "]]" inlineToOrg (Image _ _ (source, _)) =- return $ "[[" <> literal (orgPath source) <> "]]"+ return $ "[[" <> literal (escapeLinkTarget (orgPath source)) <> "]]" inlineToOrg (Note contents) = do -- add to notes in state- notes <- gets stNotes- modify $ \st -> st { stNotes = contents:notes }- let ref = tshow $ length notes + 1+ modify $ \st -> st { stNotes = contents : stNotes st+ , stNoteNum = stNoteNum st + 1 }+ ref <- gets (tshow . stNoteNum) return $ "[fn:" <> literal ref <> "]"++-- | Escape a link target like Emacs' @org-link-escape@:+-- backslash-escape square brackets, and double any run of backslashes+-- occurring directly before a bracket or at the end of the target.+escapeLinkTarget :: Text -> Text+escapeLinkTarget t =+ let (pre, rest) = T.break (\c -> c == '\\' || c == '[' || c == ']') t+ in pre <> case T.uncons rest of+ Nothing -> ""+ Just ('\\', _) ->+ let (bs, rest') = T.span (== '\\') rest+ in case T.uncons rest' of+ Nothing -> bs <> bs+ Just (c, rest'')+ | c == '[' || c == ']'+ -> bs <> bs <> "\\" <> T.cons c (escapeLinkTarget rest'')+ | otherwise -> bs <> T.cons c (escapeLinkTarget rest'')+ Just (c, rest') -> "\\" <> T.cons c (escapeLinkTarget rest')++-- | Make a link description safe: it must not contain @]]@ or end+-- with @]@. Like Emacs' @org-link-make-string@, insert a zero-width+-- space to break up the offending brackets.+escapeLinkDescription :: Text -> Text+escapeLinkDescription = fixEnd . fixDouble+ where+ fixDouble t | "]]" `T.isInfixOf` t+ = fixDouble $ T.replace "]]" "]\x200B]" t+ | otherwise = t+ fixEnd t = case T.unsnoc t of+ Just (t', ']') -> t' <> "\x200B]"+ _ -> t orgPath :: Text -> Text orgPath src = case T.uncons src of
@@ -28,6 +28,7 @@ import Control.Monad.State ( StateT, gets, modify, evalStateT ) import Codec.Archive.Zip+import Data.Containers.ListUtils (nubOrdOn) import Data.List (intercalate, stripPrefix, nub, union, isPrefixOf, intersperse) import Data.Bifunctor (bimap) import Data.CaseInsensitive (CI)@@ -57,6 +58,7 @@ import Text.Pandoc.Writers.Shared (metaToContext) import Text.Pandoc.Writers.OOXML import qualified Data.Map as M+import qualified Data.Set as Set import Data.Maybe (mapMaybe, listToMaybe, fromMaybe, maybeToList, catMaybes, isJust) import Text.Pandoc.ImageSize import Control.Applicative ((<|>))@@ -67,7 +69,7 @@ import Text.Pandoc.Logging (LogMessage(PowerpointTemplateWarning)) import Text.Pandoc.Writers.Math (convertMath) import Text.Pandoc.Writers.Powerpoint.Presentation-import Text.Pandoc.Shared (tshow, stringify)+import Text.Pandoc.Shared (tshow, stringify, stringifyInlines) import Skylighting (fromColor) -- |The 'EMU' type is used to specify sizes in English Metric Units.@@ -131,6 +133,9 @@ , envInSpeakerNotes :: Bool , envSlideLayouts :: Maybe SlideLayouts , envOtherStyleIndents :: Maybe Indents+ -- The parsed slide master, cached to avoid+ -- re-parsing it for every slide.+ , envMaster :: Maybe Element } deriving (Show) @@ -151,6 +156,7 @@ , envInSpeakerNotes = False , envSlideLayouts = Nothing , envOtherStyleIndents = Nothing+ , envMaster = Nothing } type SlideLayouts = SlideLayoutsOf SlideLayout@@ -391,16 +397,28 @@ mediaEntries <- makeMediaEntries contentTypesEntry <- presentationToContentTypes p >>= contentTypesToEntry -- fold everything into our inherited archive and return it.- return $ foldr addEntryToArchive newArch' $- slideEntries <>- slideRelEntries <>- spkNotesEntries <>- spkNotesRelEntries <>- mediaEntries <>- [updatedMasterEntry, updatedMasterRelEntry] <>- [contentTypesEntry, docPropsEntry, docCustomPropsEntry, relsEntry,- presEntry, presRelsEntry, viewPropsEntry]+ return $ addEntriesToArchive+ (slideEntries <>+ slideRelEntries <>+ spkNotesEntries <>+ spkNotesRelEntries <>+ mediaEntries <>+ [updatedMasterEntry, updatedMasterRelEntry] <>+ [contentTypesEntry, docPropsEntry, docCustomPropsEntry, relsEntry,+ presEntry, presRelsEntry, viewPropsEntry])+ newArch' +-- | Add entries to an archive in a single pass. Equivalent to (but+-- faster than) folding 'addEntryToArchive' over the list: for+-- duplicate paths the first entry in the list wins, and the new+-- entries precede (and replace) existing entries with the same paths.+addEntriesToArchive :: [Entry] -> Archive -> Archive+addEntriesToArchive entries archive =+ archive{ zEntries = nubOrdOn eRelativePath entries <>+ filter (\e -> eRelativePath e `Set.notMember` newPaths)+ (zEntries archive) }+ where newPaths = Set.fromList $ map eRelativePath entries+ updateMasterElems :: SlideLayouts -> Element -> Element -> (Element, Element) updateMasterElems layouts master masterRels = (updatedMaster, updatedMasterRels) where@@ -578,10 +596,21 @@ distArchive <- toArchive . BL.fromStrict <$> readDefaultDataFile "reference.pptx" refArchive <- case writerReferenceDoc opts of- Just f -> toArchive . BL.fromStrict . fst- <$> P.fetchItem (T.pack f)- Nothing -> toArchive . BL.fromStrict <$>- readDataFile "reference.pptx"+ Just f -> do+ arch <- toArchiveOrFail . BL.fromStrict . fst+ <$> P.fetchItem (T.pack f)+ case arch of+ Left err -> throwError $ PandocParseError $+ "Could not parse reference-doc " <>+ tshow f <> ": " <> T.pack err+ Right x -> pure x+ Nothing -> do+ arch <- toArchiveOrFail . BL.fromStrict <$>+ readDataFile "reference.pptx"+ case arch of+ Left err -> throwError $ PandocParseError $+ "Could not parse reference.pptx: " <> T.pack err+ Right x -> pure x let (referenceLayouts, defaultReferenceLayouts) = (getLayoutsFromArchive refArchive, getLayoutsFromArchive distArchive)@@ -661,7 +690,7 @@ context <- metaToContext opts{ writerTemplate = writerTemplate opts <|> Just mempty } (return . literal . stringify)- (return . literal . stringify) meta+ (return . literal . stringifyInlines) meta let env = def { envRefArchive = refArchive , envDistArchive = distArchive@@ -673,6 +702,7 @@ , envSpeakerNotesIdMap = makeSpeakerNotesMap pres , envSlideLayouts = Just layouts , envOtherStyleIndents = otherStyleIndents+ , envMaster = Just master } let st = def { stMediaGlobalIds = initialGlobalIds refArchive distArchive@@ -983,9 +1013,13 @@ getMaster :: PandocMonad m => P m Element getMaster = do- refArchive <- asks envRefArchive- distArchive <- asks envDistArchive- getMaster' refArchive distArchive+ mbMaster <- asks envMaster+ case mbMaster of+ Just master -> pure master+ Nothing -> do+ refArchive <- asks envRefArchive+ distArchive <- asks envDistArchive+ getMaster' refArchive distArchive getMaster' :: PandocMonad m => Archive -> Archive -> m Element getMaster' refArchive distArchive =@@ -2272,6 +2306,8 @@ , sldNumShape ] ]+ -- required by Keynote, see #11935+ , mknode "p:clrMapOvr" [] $ mknode "a:masterClrMapping" [] () ] -----------------------------------------------------------------------@@ -2745,6 +2781,7 @@ : ( [mknode "dc:subject" [] $ fromMaybe "" $ dcSubject docProps | isJust (dcSubject docProps)]) <> ( [mknode "dc:description" [] $ fromMaybe "" $ dcDescription docProps | isJust (dcDescription docProps)]) <> ( [mknode "cp:category" [] $ fromMaybe "" $ cpCategory docProps | isJust (cpCategory docProps)])+ <> ( [mknode "dc:language" [] $ fromMaybe "" $ dcLanguage docProps | isJust (dcLanguage docProps)]) <> (\x -> [ mknode "dcterms:created" [("xsi:type","dcterms:W3CDTF")] x , mknode "dcterms:modified" [("xsi:type","dcterms:W3CDTF")] x ]) (T.pack $ formatTime defaultTimeLocale "%FT%XZ" utctime)
@@ -63,6 +63,7 @@ import qualified Data.Map as M import qualified Data.Set as S import Data.Maybe (maybeToList, fromMaybe, listToMaybe, isNothing)+import Data.Monoid (Any(..)) import Text.Pandoc.Highlighting import qualified Data.Text as T import Control.Applicative ((<|>))@@ -187,6 +188,7 @@ , dcKeywords :: Maybe [T.Text] , dcDescription :: Maybe T.Text , cpCategory :: Maybe T.Text+ , dcLanguage :: Maybe T.Text , dcDate :: Maybe T.Text , customProperties :: Maybe [(T.Text, T.Text)] } deriving (Show, Eq)@@ -845,11 +847,14 @@ return $ filter (not . isNotesDiv) blks handleAndFilterSpeakerNotes :: [Block] -> Pres ([Block], SpeakerNotes)-handleAndFilterSpeakerNotes blks = do- modify $ \st -> st{stSpeakerNotes = mempty}- blks' <- walkM handleAndFilterSpeakerNotes' blks- spkNotes <- gets stSpeakerNotes- return (blks', spkNotes)+handleAndFilterSpeakerNotes blks+ -- avoid an expensive walk in the common case of no notes divs:+ | not (getAny (query (Any . isNotesDiv) blks)) = return (blks, mempty)+ | otherwise = do+ modify $ \st -> st{stSpeakerNotes = mempty}+ blks' <- walkM handleAndFilterSpeakerNotes' blks+ spkNotes <- gets stSpeakerNotes+ return (blks', spkNotes) blocksToSlide :: [Block] -> Pres Slide blocksToSlide blks = do@@ -1165,7 +1170,7 @@ Just (MetaList xs) -> Just $ map Shared.stringify xs _ -> Nothing - authors = case map Shared.stringify $ docAuthors meta of+ authors = case map Shared.stringifyInlines $ docAuthors meta of [] -> Nothing ss -> Just $ T.intercalate "; " ss @@ -1185,8 +1190,9 @@ , dcKeywords = keywords , dcDescription = description , cpCategory = Shared.stringify <$> lookupMeta "category" meta+ , dcLanguage = Shared.stringify <$> lookupMeta "lang" meta , dcDate =- let t = Shared.stringify (docDate meta)+ let t = Shared.stringifyInlines (docDate meta) in if T.null t then Nothing else Just t
@@ -35,7 +35,6 @@ import Text.DocLayout import Text.Pandoc.Shared import Text.Pandoc.URI-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Writers.Shared import Text.Pandoc.Walk import Safe (lastMay, headMay)@@ -68,9 +67,6 @@ pandocToRST :: PandocMonad m => Pandoc -> RST m Text pandocToRST (Pandoc meta blocks) = do opts <- gets stOptions- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing let subtit = lookupMetaInlines "subtitle" meta title <- titleToRST (docTitle meta) subtit metadata <- metaToContext opts@@ -80,7 +76,7 @@ body <- blockListToRST' True $ case writerTemplate opts of Just _ -> normalizeHeadings 1 blocks Nothing -> blocks- notes <- gets (reverse . stNotes) >>= notesToRST+ notes <- notesToRST -- note that the notes may contain refs, so we do them first refs <- gets (reverse . stLinks) >>= refsToRST pics <- gets (reverse . stImages) >>= pictRefsToRST@@ -95,10 +91,7 @@ $ defField "titleblock" (render Nothing title :: Text) $ defField "math" hasMath $ defField "rawtex" rawTeX metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main where normalizeHeadings lev (Header l a i:bs) = Header lev a i:normalizeHeadings (lev+1) cont ++ normalizeHeadings lev bs'@@ -116,23 +109,36 @@ -- | Return RST representation of a reference key. keyToRST :: PandocMonad m => ([Inline], (Text, Text)) -> RST m (Doc Text) keyToRST (label, (src, _)) = do- label' <- inlineListToRST label+ -- The stored label has already been normalized by the walk in+ -- inlineListToRST. Use writeInlines to avoid re-applying the+ -- (non-idempotent) transformations: the definition must render+ -- exactly like the inline reference, which uses writeInlines.+ label' <- writeInlines label let label'' = if (==':') `T.any` (render Nothing label' :: Text) then char '`' <> label' <> char '`' else label' return $ nowrap $ ".. _" <> label'' <> ": " <> literal src --- | Return RST representation of notes.-notesToRST :: PandocMonad m => [[Block]] -> RST m (Doc Text)-notesToRST notes =- vsep <$> zipWithM noteToRST [1..] notes+-- | Return RST representation of the collected notes. Rendering a+-- note may add further notes to the state (notes nested inside+-- notes); keep going until all of them have been rendered.+notesToRST :: PandocMonad m => RST m (Doc Text)+notesToRST = vsep <$> go 0+ where+ go done = do+ notes <- gets (drop done . reverse . stNotes)+ if null notes+ then return []+ else do+ docs <- zipWithM noteToRST [done + 1 ..] notes+ (docs ++) <$> go (done + length notes) -- | Return RST representation of a note. noteToRST :: PandocMonad m => Int -> [Block] -> RST m (Doc Text) noteToRST num note = do contents <- blockListToRST note let marker = ".. [" <> text (show num) <> "]"- return $ nowrap $ marker $$ nest 3 contents+ return $ nowrap marker $$ nest 3 contents -- | Return RST representation of picture reference table. pictRefsToRST :: PandocMonad m@@ -146,7 +152,8 @@ => ([Inline], (Attr, Text, Text, Maybe Text)) -> RST m (Doc Text) pictToRST (label, (attr, src, _, mbtarget)) = do- label' <- inlineListToRST label+ -- the stored label was already normalized; see keyToRST+ label' <- writeInlines label dims <- imageDimsToRST attr let (_, cls, _) = attr classes = case cls of@@ -900,7 +907,9 @@ modify $ \st -> st { stImages = (alt', (attr,src,tit, mbtarget)):stImages st } return alt'- inlineListToRST txt+ -- the alt text has already been normalized by the walk in+ -- inlineListToRST, so don't apply the transformations again+ writeInlines txt imageDimsToRST :: PandocMonad m => Attr -> RST m (Doc Text) imageDimsToRST attr = do
@@ -55,6 +55,8 @@ , insertCurrentSpansAtColumn , takePreviousSpansAtColumn , decrementTrailingRowSpans+ , wrapColumns+ , renderWithTemplate ) where import Safe (lastMay, maximumMay)@@ -77,12 +79,13 @@ import Text.Pandoc.Parsing (runParser, eof, defaultParserState, anyOrderedListMarker) import Text.DocLayout-import Text.Pandoc.Shared (stringify, makeSections, blocksToInlines)+import Text.Pandoc.Shared (stringify, stringifyInlines, makeSections,+ blocksToInlines) import Text.Pandoc.Walk (Walkable(..)) import qualified Text.Pandoc.UTF8 as UTF8 import Text.Pandoc.XML (escapeStringForXML, rdfaAttributes, html5Attributes) import Text.DocTemplates (Context(..), Val(..), TemplateTarget,- ToContext(..), FromContext(..))+ ToContext(..), FromContext(..), renderTemplate) import Text.Pandoc.Chunks (tocToList, toTOCTree) import Text.Collate.Lang (Lang (..)) import Text.Pandoc.Class (PandocMonad, toLang)@@ -110,6 +113,24 @@ Just _ -> addVariablesToContext opts <$> metaToContext' blockWriter inlineWriter meta +-- | The line length to use in rendering: 'writerColumns' if+-- 'writerWrapText' is 'WrapAuto', otherwise 'Nothing' (no wrapping).+wrapColumns :: WriterOptions -> Maybe Int+wrapColumns opts =+ if writerWrapText opts == WrapAuto+ then Just $ writerColumns opts+ else Nothing++-- | Render a document body, inserting it into 'writerTemplate'+-- (with the given context) if there is a template, and wrapping+-- lines according to 'wrapColumns'.+renderWithTemplate :: WriterOptions -> Context Text -> Doc Text -> Text+renderWithTemplate opts context main =+ render (wrapColumns opts) $+ case writerTemplate opts of+ Nothing -> main+ Just tpl -> renderTemplate tpl context+ -- | Like 'metaToContext, but does not include variables and is -- not sensitive to 'writerTemplate'. metaToContext' :: (Monad m, TemplateTarget a)@@ -195,24 +216,25 @@ _ -> Nothing -- | Produce an HTML tag with the given pandoc attributes.-tagWithAttrs :: HasChars a => a -> Attr -> Doc a+tagWithAttrs :: (HasChars a, FromText a) => a -> Attr -> Doc a tagWithAttrs tag attr = "<" <> literal tag <> (htmlAttrs attr) <> ">" -- | Produce HTML for the given pandoc attributes, to be used in HTML tags-htmlAttrs :: HasChars a => Attr -> Doc a+htmlAttrs :: (HasChars a, FromText a) => Attr -> Doc a htmlAttrs (ident, classes, kvs) = addSpaceIfNotEmpty (hsep [ if T.null ident then empty- else "id=" <> doubleQuotes (text $ T.unpack ident)+ else "id=" <> doubleQuotes (literal $ fromText (escapeStringForXML ident)) ,if null classes then empty- else "class=" <> doubleQuotes (text $ T.unpack (T.unwords classes))+ else "class=" <> doubleQuotes+ (literal $ fromText . escapeStringForXML $ T.unwords classes) ,hsep (map (\(k,v) -> formatKey k <> "=" <>- doubleQuotes (text $ T.unpack (escapeStringForXML v))) kvs)+ doubleQuotes (literal $ fromText (escapeStringForXML v))) kvs) ]) where- formatKey x = text . T.unpack $- if (x `Set.member` (html5Attributes <> rdfaAttributes)+ formatKey x = literal . fromText $+ if ((x `Set.member` html5Attributes || x `Set.member` rdfaAttributes) && x /= "label") -- #10048 || T.any (== ':') x -- e.g. epub: namespace || "data-" `T.isPrefixOf` x@@ -259,8 +281,8 @@ -- | Remove leading and trailing 'Space' and 'SoftBreak' elements. stripLeadingTrailingSpace :: [Inline] -> [Inline] stripLeadingTrailingSpace = go . reverse . go . reverse- where go (Space:xs) = xs- go (SoftBreak:xs) = xs+ where go (Space:xs) = go xs+ go (SoftBreak:xs) = go xs go xs = xs -- | Put display math in its own block (for ODT/DOCX).@@ -607,8 +629,8 @@ lookupMetaBool :: Text -> Meta -> Bool lookupMetaBool key meta = case lookupMeta key meta of- Just (MetaBlocks _) -> True- Just (MetaInlines _) -> True+ Just (MetaBlocks bs) -> not (null bs)+ Just (MetaInlines ils) -> not (null ils) Just (MetaString x) -> not (T.null x) Just (MetaBool True) -> True _ -> False@@ -646,7 +668,7 @@ lookupMetaString key meta = case lookupMeta key meta of Just (MetaString s) -> s- Just (MetaInlines ils) -> stringify ils+ Just (MetaInlines ils) -> stringifyInlines ils Just (MetaBlocks bs) -> stringify bs Just (MetaBool b) -> T.pack (show b) _ -> ""@@ -677,6 +699,7 @@ toSubscript '=' = Just '\x208C' toSubscript '(' = Just '\x208D' toSubscript ')' = Just '\x208E'+toSubscript '\x2212' = Just '\x208B' -- unicode minus toSubscript c | c >= '0' && c <= '9' = Just $ chr (0x2080 + (ord c - 48))@@ -716,6 +739,10 @@ Just Plain{} -> True Just (BulletList is) -> maybe False endsWithPlain (lastMay is) Just (OrderedList _ is) -> maybe False endsWithPlain (lastMay is)+ Just (DefinitionList defs) ->+ case lastMay defs of+ Just (_, ds) -> maybe False endsWithPlain (lastMay ds)+ Nothing -> False _ -> False -- | Convert the relevant components of a new-style table (with block@@ -799,12 +826,13 @@ isSentenceEnding t = case T.unsnoc t of Just (t',c)- | c == '.' || c == '!' || c == '?'+ | c == '.' , not (isInitial t') -> True+ | c == '!' || c == '?' -> True | c == ')' || c == ']' || c == '"' || c == '\x201D' -> case T.unsnoc t' of- Just (t'',d) -> d == '.' || d == '!' || d == '?' &&- not (isInitial t'')+ Just (t'',d) -> (d == '.' && not (isInitial t''))+ || d == '!' || d == '?' _ -> False _ -> False where@@ -814,44 +842,46 @@ -- and modify internal links accordingly. (Yes, XML allows an -- underscore, but HTML 4 doesn't, so we are more conservative.) ensureValidXmlIdentifiers :: Pandoc -> Pandoc-ensureValidXmlIdentifiers = walk fixLinks . walkAttr fixIdentifiers+ensureValidXmlIdentifiers = walk goInline . walk goBlock where- fixIdentifiers (ident, classes, kvs) =+ fixAttr (ident, classes, kvs) = (case T.uncons ident of Nothing -> ident Just (c, _) | isLetter c -> ident _ -> "id_" <> ident, classes, kvs)- needsFixing src =+ fixSrc src = case T.uncons src of Just ('#',t) -> case T.uncons t of- Just (c,_) | not (isLetter c) -> Just ("#id_" <> t)- _ -> Nothing- _ -> Nothing- fixLinks (Link attr ils (src, tit))- | Just src' <- needsFixing src = Link attr ils (src', tit)- fixLinks (Image attr ils (src, tit))- | Just src' <- needsFixing src = Image attr ils (src', tit)- fixLinks x = x+ Just (c,_) | not (isLetter c) -> "#id_" <> t+ _ -> src+ _ -> src --- | Walk Pandoc document, modifying attributes.-walkAttr :: (Attr -> Attr) -> Pandoc -> Pandoc-walkAttr f = walk goInline . walk goBlock- where- goInline (Span attr ils) = Span (f attr) ils- goInline (Link attr ils target) = Link (f attr) ils target- goInline (Image attr ils target) = Image (f attr) ils target- goInline (Code attr txt) = Code (f attr) txt+ goInline (Span attr ils) = Span (fixAttr attr) ils+ goInline (Link attr ils (src, tit)) = Link (fixAttr attr) ils (fixSrc src, tit)+ goInline (Image attr ils (src, tit)) =+ Image (fixAttr attr) ils (fixSrc src, tit)+ goInline (Code attr txt) = Code (fixAttr attr) txt goInline x = x - goBlock (Header lev attr ils) = Header lev (f attr) ils- goBlock (CodeBlock attr txt) = CodeBlock (f attr) txt+ goBlock (Header lev attr ils) = Header lev (fixAttr attr) ils+ goBlock (CodeBlock attr txt) = CodeBlock (fixAttr attr) txt goBlock (Table attr cap colspecs thead tbodies tfoot) =- Table (f attr) cap colspecs thead tbodies tfoot- goBlock (Div attr bs) = Div (f attr) bs+ Table (fixAttr attr) cap colspecs+ (goTableHead thead) (map goTableBody tbodies) (goTableFoot tfoot)+ goBlock (Div attr bs) = Div (fixAttr attr) bs+ goBlock (Figure attr cap bs) = Figure (fixAttr attr) cap bs goBlock x = x + goTableHead (TableHead attr rows) = TableHead (fixAttr attr) (map goRow rows)+ goTableBody (TableBody attr rhc hd bd) =+ TableBody (fixAttr attr) rhc (map goRow hd) (map goRow bd)+ goTableFoot (TableFoot attr rows) = TableFoot (fixAttr attr) (map goRow rows)+ goRow (Row attr cells) = Row (fixAttr attr) (map goCell cells)+ goCell (Cell attr align rowspan colspan bs) =+ Cell (fixAttr attr) align rowspan colspan bs+ -- | Convert links to spans; most useful when writing elements that must not -- contain links, e.g. to avoid nested links. removeLinks :: [Inline] -> [Inline]@@ -878,8 +908,12 @@ toTaskListItem :: MonadPlus m => [Block] -> m (Bool, [Block]) toTaskListItem (Plain (Str "☐":Space:ils):xs) = pure (False, Plain ils:xs) toTaskListItem (Plain (Str "☒":Space:ils):xs) = pure (True, Plain ils:xs)+toTaskListItem (Plain [Str "☐"]:xs) = pure (False, Plain []:xs)+toTaskListItem (Plain [Str "☒"]:xs) = pure (True, Plain []:xs) toTaskListItem (Para (Str "☐":Space:ils):xs) = pure (False, Para ils:xs) toTaskListItem (Para (Str "☒":Space:ils):xs) = pure (True, Para ils:xs)+toTaskListItem (Para [Str "☐"]:xs) = pure (False, Para []:xs)+toTaskListItem (Para [Str "☒"]:xs) = pure (True, Para []:xs) toTaskListItem _ = mzero -- | Add an opener and closer to a Doc. If the Doc begins or ends@@ -960,9 +994,11 @@ -- For handling previous row spans that are next to the end of a row's cells -- that were previously added with 'insertCurrentSpansAtColumn'. decrementTrailingRowSpans :: Int -> M.Map Int (RowSpan, ColSpan) -> M.Map Int (RowSpan, ColSpan)-decrementTrailingRowSpans columnPosition = M.mapWithKey decrementTrailing+decrementTrailingRowSpans columnPosition = M.mapMaybeWithKey decrementTrailing where- decrementTrailing previousColumnPosition previousSpan@(RowSpan rowSpan, colSpan) =- if previousColumnPosition >= columnPosition && rowSpan >= 1- then (RowSpan rowSpan - 1, colSpan)- else previousSpan+ decrementTrailing previousColumnPosition previousSpan@(RowSpan rowSpan, colSpan)+ | previousColumnPosition >= columnPosition && rowSpan >= 1 =+ if rowSpan > 1+ then Just (RowSpan rowSpan - 1, colSpan)+ else Nothing -- span is used up; drop it instead of keeping a 0 entry+ | otherwise = Just previousSpan
@@ -23,7 +23,6 @@ import Text.DocLayout import Text.Pandoc.Shared import Text.Pandoc.URI-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Writers.Shared import Text.Pandoc.XML @@ -31,9 +30,6 @@ writeTEI :: PandocMonad m => WriterOptions -> Pandoc -> m Text writeTEI opts doc = do let Pandoc meta blocks = ensureValidXmlIdentifiers doc- let colwidth = if writerWrapText opts == WrapAuto- then Just $ writerColumns opts- else Nothing let startLvl = case writerTopLevelDivision opts of TopLevelPart -> -1 TopLevelChapter -> 0@@ -49,20 +45,12 @@ $ defField "mathml" (case writerMathMethod opts of MathML -> True _ -> False) metadata- return $ render colwidth $- case writerTemplate opts of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate opts context main -- | Convert a list of Pandoc blocks to TEI. blocksToTEI :: PandocMonad m => WriterOptions -> [Block] -> m (Doc Text) blocksToTEI opts bs = vcat <$> mapM (blockToTEI opts) bs --- | Auxiliary function to convert Plain block to Para.-plainToPara :: Block -> Block-plainToPara (Plain x) = Para x-plainToPara x = x- -- | Convert a list of pairs of terms and definitions into a TEI -- list with labels and items. deflistItemsToTEI :: PandocMonad m@@ -179,7 +167,7 @@ blockToTEI _ HorizontalRule = return $ selfClosingTag "milestone" [("unit","undefined") ,("type","separator")- ,("rendition","line")]+ ,("rend","line")] blockToTEI opts (Figure attr capt bs) = blockToTEI opts (figureDiv attr capt bs) @@ -275,8 +263,12 @@ linktext <- inlinesToTEI opts txt return $ linktext <+> char '(' <> emailLink <> char ')' | otherwise =- inTags False "ref" (("target", src) : idFromAttr opts attr)+ inTags False "ref" (("target", src') : idFromAttr opts attr) <$> inlinesToTEI opts txt+ where+ src' = case T.stripPrefix "#" src of+ Just ident -> "#" <> writerIdentifierPrefix opts <> ident+ Nothing -> src inlineToTEI opts (Image attr description (src, tit)) = do let titleDoc = if T.null tit then empty
@@ -36,7 +36,6 @@ import Text.DocLayout import Text.Pandoc.Shared import Text.Pandoc.URI-import Text.Pandoc.Templates (renderTemplate) import Text.Pandoc.Writers.Shared import Text.Printf (printf) @@ -105,9 +104,6 @@ blocks <- walkM addNodeText blocks' let titlePage = not $ all null $ docTitle meta : docDate meta : docAuthors meta- let colwidth = if writerWrapText options == WrapAuto- then Just $ writerColumns options- else Nothing metadata <- metaToContext options blockListToTexinfo (fmap chomp .inlineListToTexinfo)@@ -118,10 +114,7 @@ $ defField "toc" (writerTableOfContents options) $ defField "titlepage" titlePage $ defField "strikeout" (stStrikeout st) metadata- return $ render colwidth $- case writerTemplate options of- Nothing -> body- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate options context body -- | Escape things as needed for Texinfo. stringToTexinfo :: PandocMonad m => Text -> TI m Text
@@ -28,12 +28,14 @@ import Data.Bifunctor (first, second) import Network.URI (unEscapeString) import qualified Data.Text as T-import Control.Monad (unless)+import Control.Monad (unless, when) import Control.Monad.State ( StateT, evalStateT, gets, modify ) import Text.Pandoc.Writers.Shared ( lookupMetaInlines, lookupMetaString, metaToContext, defField, resetField,- setupTranslations )-import Text.Pandoc.Shared (isTightList, orderedListMarkers, tshow, stringify)+ setupTranslations, getLang,+ renderWithTemplate )+import Text.Pandoc.Shared (isTightList, orderedListMarkers, tshow,+ stringifyInlines) import Text.Pandoc.Highlighting (highlight, formatTypstBlock, formatTypstInline, styleToTypst) import Text.Pandoc.Translations (Term(Abstract), translateTerm)@@ -42,12 +44,12 @@ import Text.Pandoc.Writers.Math (convertMath) import qualified Text.TeXMath as TM import Text.DocLayout-import Text.DocTemplates (renderTemplate) import Text.Pandoc.Extensions (Extension(..)) import Text.Pandoc.Logging (LogMessage(..)) import qualified Text.Pandoc.UTF8 as UTF8 import Text.Collate.Lang (Lang(..), parseLang) import Text.Printf (printf)+import Text.Read (readMaybe) import Data.Char (isDigit) import Data.Maybe (fromMaybe) import Unicode.Char (isXIDContinue)@@ -59,7 +61,11 @@ evalStateT (pandocToTypst options document) WriterState{ stOptions = options, stEscapeContext = NormalContext,- stHighlighting = False }+ stHighlighting = False,+ stCsl = False,+ stCslHangingIndent = Nothing,+ stCslEntrySpacing = Nothing+ } data EscapeContext = NormalContext | TermContext deriving (Show, Eq)@@ -68,7 +74,10 @@ WriterState { stOptions :: WriterOptions, stEscapeContext :: EscapeContext,- stHighlighting :: Bool+ stHighlighting :: Bool,+ stCsl :: Bool,+ stCslHangingIndent :: Maybe Text,+ stCslEntrySpacing :: Maybe Text } type TW m = StateT WriterState m@@ -76,9 +85,6 @@ pandocToTypst :: PandocMonad m => WriterOptions -> Pandoc -> TW m Text pandocToTypst options (Pandoc meta blocks) = do- let colwidth = if writerWrapText options == WrapAuto- then Just $ writerColumns options- else Nothing setupTranslations meta metadata <- metaToContext options blocksToTypst@@ -94,7 +100,9 @@ _ -> []) $ lookupMetaInlines "nocite" meta hasHighlighting <- gets stHighlighting-+ hasCslSettings <- gets stCsl+ cslHangingIndent <- gets stCslHangingIndent+ cslEntrySpacing <- gets stCslEntrySpacing let context = defField "body" main $ defField "toc" (writerTableOfContents options) $ (if isEnabled Ext_citations options@@ -102,15 +110,18 @@ . defField "nocite-ids" (filter (/= "*") nociteIds) . defField "full-bibliography" ("*" `elem` nociteIds) else id)- $ (case lookupMetaString "lang" meta of- "" -> id- lang ->+ $ (case getLang options meta of+ Nothing -> id+ Just lang -> case parseLang lang of Left _ -> id Right l -> resetField "lang" (langLanguage l) . maybe id (resetField "region") (langRegion l)) $ defField "csl" (lookupMetaString "citation-style" meta) -- #10661+ $ defField "csl-settings" hasCslSettings+ $ maybe id (defField "csl-hanging-indent") cslHangingIndent+ $ maybe id (defField "csl-entry-spacing") cslEntrySpacing $ defField "smart" (isEnabled Ext_smart options) $ defField "abstract-title" abstractTitle $ defField "toc-depth" (tshow $ writerTOCDepth options)@@ -130,10 +141,7 @@ then defField "section-numbering" ("1.1.1.1.1" :: Text) else id) metadata- return $ render colwidth $- case writerTemplate options of- Nothing -> main- Just tpl -> renderTemplate tpl context+ return $ renderWithTemplate options context main pickTypstAttrs :: [(Text, Text)] -> ([(Text, Text)],[(Text, Text)]) pickTypstAttrs = foldr go ([],[])@@ -206,7 +214,7 @@ RawBlock fmt str -> case fmt of Format "typst" -> return $ literal str- _ -> return mempty+ _ -> mempty <$ report (BlockNotRendered block) CodeBlock (ident,cls,kvs) code -> do let go :: Char -> (Int, Int) -> (Int, Int) go '`' (longest, current) =@@ -362,17 +370,18 @@ $$ footer ) $$ ")"- return $ if "typst:no-figure" `elem` tabclasses- then toTypstBracesSetText typstTextAttrs table- else "#figure("- $$- nest 2- ("align(center)[" <> toTypstPoundSetText typstTextAttrs <> "#" <> table <> "]"- $$ capt'- $$ typstFigureKind- $$ ")")- $$ lab- $$ blankline+ return $+ (if "typst:no-figure" `elem` tabclasses+ then toTypstBracesSetText typstTextAttrs table+ else "#figure("+ $$+ nest 2+ ("align(center)[" <> toTypstPoundSetText typstTextAttrs <> "#" <> table <> "]"+ $$ capt'+ $$ typstFigureKind+ $$ ")"))+ $$ lab+ $$ blankline Figure (ident,_,kvs) (Caption _mbshort capt) blocks -> do caption <- blocksToTypst capt opts <- gets stOptions@@ -394,11 +403,22 @@ $$ ")" $$ lab $$ blankline Div (ident,_,_) (Header lev ("",cls,kvs) ils:rest) -> blocksToTypst (Header lev (ident,cls,kvs) ils:rest)- Div (ident,_,kvs) blocks -> do+ Div (ident,cls,kvs) blocks -> do let lab = case lookup "typst-label" kvs of Just l -> toLabel FreestandingLabel l Nothing -> toLabel FreestandingLabel ident let (typstAttrs,typstTextAttrs) = pickTypstAttrs kvs+ when ("csl-bib-body" `elem` cls) $+ modify $ \st -> st{ stCsl = True+ , stCslEntrySpacing =+ case lookup "entry-spacing" kvs >>=+ readMaybe . T.unpack of+ Just (n :: Int) -> Just $ tshow n <> "em"+ Nothing -> Nothing+ , stCslHangingIndent =+ if "hanging-indent" `elem` cls+ then Just "1.5em"+ else Nothing } contents <- blocksToTypst blocks return $ "#block" <> toTypstPropsListParens typstAttrs <> "[" $$ toTypstPoundSetText typstTextAttrs@@ -496,7 +516,7 @@ RawInline fmt str -> case fmt of Format "typst" -> return $ literal str- _ -> return mempty+ _ -> mempty <$ report (InlineNotRendered inline) Strikeout inlines -> textstyle "#strike" inlines Emph inlines -> textstyle "#emph" inlines Underline inlines -> textstyle "#underline" inlines@@ -598,7 +618,7 @@ Just alt -> Just alt Nothing -> case imgInlines of [] -> Nothing- _ -> Just (stringify imgInlines)+ _ -> Just (stringifyInlines imgInlines) textstyle :: PandocMonad m => Doc Text -> [Inline] -> TW m (Doc Text) textstyle s inlines = do@@ -633,6 +653,8 @@ go (lastc, t') c | needsEscape c = (c, t' <> escapeChar c) | c == '-', lastc == '-', smart = (c, t' <> T.pack ['\\',c])+ | c == '?', lastc == '-', smart = (c, t' <> T.pack ['\\',c])+ | c == '.', lastc == '.', smart = (c, t' <> T.pack ['\\',c]) | c == '/', lastc == '/' = (c, t' <> T.pack ['\\',c]) | otherwise = (c, T.snoc t' c) escapeChar c
@@ -1,6 +1,8 @@ {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeOperators #-} -- | -- Module : Text.Pandoc.Writers.XML@@ -18,19 +20,19 @@ import Data.Maybe (mapMaybe) import qualified Data.Text as T import Data.Version (versionBranch)+import GHC.Generics import Text.Pandoc.Class.PandocMonad (PandocMonad) import Text.Pandoc.Definition import Text.Pandoc.Options (WriterOptions (..)) import Text.Pandoc.XML.Light import qualified Text.Pandoc.XML.Light as XML import Text.Pandoc.XMLFormat-import Text.XML.Light (xml_header) type PandocAttr = Text.Pandoc.Definition.Attr writeXML :: (PandocMonad m) => WriterOptions -> Pandoc -> m T.Text-writeXML _ doc = do- return $ pandocToXmlText doc+writeXML opts doc = do+ return $ pandocToXmlText opts doc text_node :: T.Text -> Content text_node text = Text (CData CDataText text Nothing)@@ -65,40 +67,85 @@ elementWithAttrAndContents :: T.Text -> PandocAttr -> [Content] -> Element elementWithAttrAndContents tag attr contents = addAttrAttributes attr $ elementWithContents tag contents -asBlockOfInlines :: Element -> [Content]-asBlockOfInlines el = [Elem el, text_node "\n"]+-- | Extract the name of a value's constructor via GHC.Generics.+class GConName f where+ gConName :: f p -> String -asBlockOfBlocks :: Element -> [Content]-asBlockOfBlocks el = [Elem newline_before_first, newline]- where- newline = text_node "\n"- newline_before_first = if null (elContent el) then el else prependContents [newline] el+instance (GConName f) => GConName (M1 D d f) where+ gConName (M1 x) = gConName x -itemName :: (Show a) => a -> T.Text-itemName a = T.pack $ takeWhile (/= ' ') (show a)+instance (GConName f, GConName g) => GConName (f :+: g) where+ gConName (L1 x) = gConName x+ gConName (R1 x) = gConName x +instance (Constructor c) => GConName (M1 C c f) where+ gConName = conName++itemName :: (Generic a, GConName (Rep a)) => a -> T.Text+itemName = T.pack . gConName . from+ intAsText :: Int -> T.Text intAsText i = T.pack $ show i -itemAsEmptyElement :: (Show a) => a -> Element+itemAsEmptyElement :: (Generic a, GConName (Rep a)) => a -> Element itemAsEmptyElement item = emptyElement $ itemName item -pandocToXmlText :: Pandoc -> T.Text-pandocToXmlText (Pandoc (Meta meta) blocks) = with_header . with_blocks . with_meta . with_version $ el- where- el = prependContents [text_node "\n"] $ emptyElement "Pandoc"- with_version = addAttribute atNameApiVersion (T.intercalate "," $ map (T.pack . show) $ versionBranch pandocTypesVersion)- with_meta = appendContents (metaMapToXML meta "meta")- with_blocks = appendContents (asBlockOfBlocks $ elementWithContents "blocks" $ blocksToXML blocks)- with_header :: Element -> T.Text- with_header e = T.concat [T.pack xml_header, "\n", showElement e]+pandocToXmlText :: WriterOptions -> Pandoc -> T.Text+pandocToXmlText opts (Pandoc (Meta meta) blocks) =+ case writerTemplate opts of+ Just _ -> -- standalone document; include Pandoc and Meta+ ppcTopElement configPP . with_blocks . with_meta . with_version $ el+ Nothing -> -- fragment; just include blocks, as native writer does+ mconcat $ map (ppcContent configPP) block_contents+ where+ el = emptyElement "Pandoc"+ with_version = addAttribute atNameApiVersion version+ version = (T.intercalate "," $ map (T.pack . show)+ $ versionBranch pandocTypesVersion)+ with_meta = appendContents (metaMapToXML meta "meta")+ with_blocks = appendContents $ asContents block_element+ block_element = elementWithContents "blocks" block_contents+ block_contents = blocksToXML blocks +-- | Pretty-printing configuration: the contents of elements that+-- contain inline content are kept on a single line, so that no+-- significant whitespace is added inside them.+configPP :: ConfigPP+configPP = useInlineTags (isInlineTag . qName) prettyConfigPP++-- | Check whether a tag is for an element with inline content.+isInlineTag :: T.Text -> Bool+isInlineTag t =+ case t of+ "Para" -> True+ "Plain" -> True+ "Header" -> True+ "MetaInlines" -> True+ "Emph" -> True+ "Strong" -> True+ "Strikeout" -> True+ "Superscript" -> True+ "Subscript" -> True+ "SmallCaps" -> True+ "Underline" -> True+ "Quoted" -> True+ "Cite" -> True+ "Link" -> True+ "Image" -> True+ "Span" -> True+ _ ->+ t == tgNameLineItem+ || t == tgNameDefListTerm+ || t == tgNameCitationPrefix+ || t == tgNameCitationSuffix+ || t == tgNameShortCaption+ metaMapToXML :: Map T.Text MetaValue -> T.Text -> [Content]-metaMapToXML mmap tag = asBlockOfBlocks $ elementWithContents tag entries+metaMapToXML mmap tag = asContents $ elementWithContents tag entries where entries = concatMap to_entry $ toList mmap to_entry :: (T.Text, MetaValue) -> [Content]- to_entry (text, metavalue) = asBlockOfBlocks with_key+ to_entry (text, metavalue) = asContents with_key where entry = elementWithContents tgNameMetaMapEntry $ metaValueToXML metavalue with_key = addAttribute atNameMetaMapEntryKey text entry@@ -108,21 +155,60 @@ let name = itemName value el = itemAsEmptyElement value in case (value) of- MetaBool b -> asBlockOfInlines $ addAttribute atNameMetaBoolValue bool_value el+ MetaBool b -> asContents $ addAttribute atNameMetaBoolValue bool_value el where bool_value = if b then "true" else "false"- MetaString s -> asBlockOfInlines $ appendContents [text_node s] el- MetaInlines inlines -> asBlockOfInlines $ appendContents (inlinesToXML inlines) el- MetaBlocks blocks -> asBlockOfBlocks $ appendContents (blocksToXML blocks) el- MetaList items -> asBlockOfBlocks $ appendContents (concatMap metaValueToXML items) el+ MetaString s -> asContents $ appendContents [text_node s] el+ MetaInlines inlines -> asContents $ appendContents (inlinesToXML inlines) el+ MetaBlocks blocks -> asContents $ appendContents (blocksToXML blocks) el+ MetaList items -> asContents $ appendContents (concatMap metaValueToXML items) el MetaMap mm -> metaMapToXML mm name blocksToXML :: [Block] -> [Content] blocksToXML blocks = concatMap blockToXML blocks inlinesToXML :: [Inline] -> [Content]-inlinesToXML inlines = concatMap inlineContentToContents (ilsToIlsContent inlines [])+inlinesToXML inlines = concatMap wsRunsAsElements $ mergeTextNodes $ concatMap inlineContentToContents (ilsToIlsContent inlines []) +-- | Merge consecutive text nodes into a single text node; otherwise+-- the pretty-printer would render each one on a line of its own.+mergeTextNodes :: [Content] -> [Content]+mergeTextNodes (Text (CData CDataText t _) : rest) =+ text_node (T.concat (t : ts)) : mergeTextNodes rest'+ where+ (ts, rest') = textRun rest+ textRun (Text (CData CDataText t' _) : cs) =+ let (ts', cs') = textRun cs in (t' : ts', cs')+ textRun cs = ([], cs)+mergeTextNodes (c : rest) = c : mergeTextNodes rest+mergeTextNodes [] = []++-- | A whitespace run in a text node is read back as a single Space+-- (a run of spaces) or a single SoftBreak (a run containing a+-- newline), so runs like " \n" or "\n\n" would not roundtrip: encode+-- them as sequences of Space and SoftBreak elements instead.+wsRunsAsElements :: Content -> [Content]+wsRunsAsElements (Text (CData CDataText t _))+ | hasLongWsRun = go [] (T.groupBy (\a b -> isWs a == isWs b) t)+ where+ isWs ch = ch == ' ' || ch == '\n'+ -- a whitespace run needs encoding only if it is longer than one+ -- character (single-character runs are " " or "\n", which are+ -- kept); the common case of no such run avoids the work below+ hasLongWsRun = fst $ T.foldl' adjacent (False, False) t+ adjacent (found, prevWs) ch =+ let ws = isWs ch in (found || (prevWs && ws), ws)+ keep r = r == " " || r == "\n" || not (T.any isWs r)+ go acc (r : rs)+ | keep r = go (r : acc) rs+ | otherwise = flush acc ++ map toElem (T.unpack r) ++ go [] rs+ go acc [] = flush acc+ flush [] = []+ flush acc = [text_node $ T.concat $ reverse acc]+ toElem '\n' = Elem $ emptyElement "SoftBreak"+ toElem _ = Elem $ emptyElement "Space"+wsRunsAsElements c = [c]+ data InlineContent = NormalInline Inline | ElSpace Int@@ -159,28 +245,28 @@ asContents el = [Elem el] wrapBlocks :: T.Text -> [Block] -> [Content]-wrapBlocks tag blocks = asBlockOfBlocks $ elementWithContents tag $ blocksToXML blocks+wrapBlocks tag blocks = asContents $ elementWithContents tag $ blocksToXML blocks wrapArrayOfBlocks :: T.Text -> [[Block]] -> [Content] wrapArrayOfBlocks tag array = concatMap (wrapBlocks tag) array -- wrapInlines :: T.Text -> [Inline] -> [Content]--- wrapInlines tag inlines = asBlockOfInlines $ element_with_contents tag $ inlinesToXML inlines+-- wrapInlines tag inlines = asContents $ element_with_contents tag $ inlinesToXML inlines blockToXML :: Block -> [Content] blockToXML block = let el = itemAsEmptyElement block in case (block) of- Para inlines -> asBlockOfInlines $ appendContents (inlinesToXML inlines) el- Header level (idn, cls, attrs) inlines -> asBlockOfInlines $ appendContents (inlinesToXML inlines) with_attr+ Para inlines -> asContents $ appendContents (inlinesToXML inlines) el+ Header level (idn, cls, attrs) inlines -> asContents $ appendContents (inlinesToXML inlines) with_attr where with_attr = addAttrAttributes (idn, cls, attrs ++ [(atNameLevel, intAsText level)]) el- Plain inlines -> asBlockOfInlines $ appendContents (inlinesToXML inlines) el- Div attr blocks -> asBlockOfBlocks $ appendContents (blocksToXML blocks) with_attr+ Plain inlines -> asContents $ appendContents (inlinesToXML inlines) el+ Div attr blocks -> asContents $ appendContents (blocksToXML blocks) with_attr where with_attr = addAttrAttributes attr el- BulletList items -> asBlockOfBlocks $ appendContents (wrapArrayOfBlocks tgNameListItem items) el- OrderedList (start, style, delim) items -> asBlockOfBlocks $ with_contents . with_attrs $ el+ BulletList items -> asContents $ appendContents (wrapArrayOfBlocks tgNameListItem items) el+ OrderedList (start, style, delim) items -> asContents $ with_contents . with_attrs $ el where with_attrs = addAttributes@@ -191,16 +277,16 @@ ] ) with_contents = appendContents (wrapArrayOfBlocks tgNameListItem items)- BlockQuote blocks -> asBlockOfBlocks $ appendContents (blocksToXML blocks) el- HorizontalRule -> asBlockOfInlines el- CodeBlock attr text -> asBlockOfInlines $ with_contents . with_attr $ el+ BlockQuote blocks -> asContents $ appendContents (blocksToXML blocks) el+ HorizontalRule -> asContents el+ CodeBlock attr text -> asContents $ with_contents . with_attr $ el where with_contents = appendContents [text_node text] with_attr = addAttrAttributes attr- LineBlock lins -> asBlockOfBlocks $ appendContents (concatMap wrapInlines lins) el+ LineBlock lins -> asContents $ appendContents (concatMap wrapInlines lins) el where wrapInlines inlines = asContents $ appendContents (inlinesToXML inlines) $ emptyElement tgNameLineItem- Table attr caption colspecs thead tbodies tfoot -> asBlockOfBlocks $ with_foot . with_bodies . with_head . with_colspecs . with_caption . with_attr $ el+ Table attr caption colspecs thead tbodies tfoot -> asContents $ with_foot . with_bodies . with_head . with_colspecs . with_caption . with_attr $ el where with_attr = addAttrAttributes attr with_caption = appendContents (captionToXML caption)@@ -208,7 +294,7 @@ with_head = appendContents (tableHeadToXML thead) with_bodies = appendContents (concatMap tableBodyToXML tbodies) with_foot = appendContents (tableFootToXML tfoot)- Figure attr caption blocks -> asBlockOfBlocks $ with_contents . with_caption . with_attr $ el+ Figure attr caption blocks -> asContents $ with_contents . with_caption . with_attr $ el where with_attr = addAttrAttributes attr with_caption = appendContents (captionToXML caption)@@ -216,7 +302,7 @@ RawBlock (Format format) text -> asContents $ appendContents [text_node text] raw where raw = addAttribute atNameFormat format el- DefinitionList items -> asBlockOfBlocks $ appendContents (map definitionListItemToXML items) el+ DefinitionList items -> asContents $ appendContents (map definitionListItemToXML items) el inlineToXML :: Inline -> [Content] inlineToXML inline =@@ -235,17 +321,17 @@ SmallCaps inlines -> wrapInlines inlines Superscript inlines -> wrapInlines inlines Subscript inlines -> wrapInlines inlines- SoftBreak -> asContents el+ SoftBreak -> [text_node "\n"] LineBreak -> asContents el Span attr inlines -> asContents $ appendContents (inlinesToXML inlines) with_attr where with_attr = addAttrAttributes attr el Link (idn, cls, attrs) inlines (url, title) -> asContents $ appendContents (inlinesToXML inlines) with_attr where- with_attr = addAttrAttributes (idn, cls, attrs ++ [(atNameLinkUrl, url), (atNameTitle, title)]) el+ with_attr = addAttrAttributes (idn, cls, attrs ++ optionalAttribute atNameLinkUrl url ++ optionalAttribute atNameTitle title) el Image (idn, cls, attrs) inlines (url, title) -> asContents $ appendContents (inlinesToXML inlines) with_attr where- with_attr = addAttrAttributes (idn, cls, attrs ++ [(atNameImageUrl, url), (atNameTitle, title)]) el+ with_attr = addAttrAttributes (idn, cls, attrs ++ optionalAttribute atNameImageUrl url ++ optionalAttribute atNameTitle title) el RawInline (Format format) text -> asContents $ appendContents [text_node text] raw where raw = addAttribute atNameFormat format el@@ -262,10 +348,15 @@ -- TODO: don't let an attribute overwrite id or class maybeAttribute :: (T.Text, T.Text) -> Maybe XML.Attr-maybeAttribute (_, "") = Nothing maybeAttribute ("", _) = Nothing-maybeAttribute (name, value) = Just $ XML.Attr (unqual name) value+maybeAttribute (name, value) = Just $ XML.Attr (unqual $ encodeAttrName name) value +-- | An optional attribute, omitted when its value is empty (the+-- reader treats a missing attribute as an empty value).+optionalAttribute :: T.Text -> T.Text -> [(T.Text, T.Text)]+optionalAttribute _ "" = []+optionalAttribute name value = [(name, value)]+ validAttributes :: [(T.Text, T.Text)] -> [XML.Attr] validAttributes pairs = mapMaybe maybeAttribute pairs @@ -286,13 +377,17 @@ addAttrAttributes :: PandocAttr -> Element -> Element addAttrAttributes (identifier, classes, attributes) el = addAttributes attrs' el where- attrs' = mapMaybe maybeAttribute (("id", identifier) : ("class", T.intercalate " " classes) : attributes)+ attrs' =+ mapMaybe maybeAttribute $+ optionalAttribute "id" identifier+ ++ optionalAttribute "class" (T.intercalate " " classes)+ ++ attributes addCitations :: [Citation] -> Element -> Element-addCitations citations el = appendContents [Elem $ elementWithContents tgNameCitations $ (text_node "\n") : concatMap citation_to_elem citations] el+addCitations citations el = appendContents [Elem $ elementWithContents tgNameCitations $ concatMap citation_to_elem citations] el where citation_to_elem :: Citation -> [Content]- citation_to_elem citation = asBlockOfInlines with_suffix+ citation_to_elem citation = asContents with_suffix where cit_elem = elementWithAttributes (itemName citation) attrs prefix = citationPrefix citation@@ -309,7 +404,7 @@ map (\(n, v) -> XML.Attr (unqual n) v) [ ("id", citationId citation),- (atNameCitationMode, T.pack $ show $ citationMode citation),+ (atNameCitationMode, itemName $ citationMode citation), (atNameCitationNoteNum, intAsText $ citationNoteNum citation), (atNameCitationHash, intAsText $ citationHash citation) ]@@ -317,18 +412,18 @@ definitionListItemToXML :: ([Inline], [[Block]]) -> Content definitionListItemToXML (inlines, defs) = Elem $ elementWithContents tgNameDefListItem $ term ++ wrapArrayOfBlocks tgNameDefListDef defs where- term = asBlockOfInlines $ appendContents (inlinesToXML inlines) $ emptyElement tgNameDefListTerm+ term = asContents $ appendContents (inlinesToXML inlines) $ emptyElement tgNameDefListTerm captionToXML :: Caption -> [Content]-captionToXML (Caption short blocks) = asBlockOfBlocks with_short_caption+captionToXML (Caption short blocks) = asContents with_short_caption where el = elementWithContents "Caption" $ blocksToXML blocks with_short_caption = case (short) of- Just inlines -> prependContents (asBlockOfInlines $ elementWithContents tgNameShortCaption $ inlinesToXML inlines) el+ Just inlines -> prependContents (asContents $ elementWithContents tgNameShortCaption $ inlinesToXML inlines) el _ -> el colSpecToXML :: (Alignment, ColWidth) -> [Content]-colSpecToXML (align, cw) = asBlockOfInlines colspec+colSpecToXML (align, cw) = asContents colspec where colspec = elementWithAttributes "ColSpec" $ validAttributes [(atNameAlignment, itemName align), (atNameColWidth, colwidth)] colwidth = case (cw) of@@ -336,27 +431,27 @@ ColWidthDefault -> "0" colSpecsToXML :: [(Alignment, ColWidth)] -> [Content]-colSpecsToXML colspecs = asBlockOfBlocks $ elementWithContents tgNameColspecs $ concatMap colSpecToXML colspecs+colSpecsToXML colspecs = asContents $ elementWithContents tgNameColspecs $ concatMap colSpecToXML colspecs tableHeadToXML :: TableHead -> [Content]-tableHeadToXML (TableHead attr rows) = asBlockOfBlocks $ elementWithAttrAndContents "TableHead" attr $ concatMap rowToXML rows+tableHeadToXML (TableHead attr rows) = asContents $ elementWithAttrAndContents "TableHead" attr $ concatMap rowToXML rows tableBodyToXML :: TableBody -> [Content]-tableBodyToXML (TableBody (idn, cls, attrs) (RowHeadColumns headcols) hrows brows) = asBlockOfBlocks $ elementWithAttrAndContents "TableBody" attr children+tableBodyToXML (TableBody (idn, cls, attrs) (RowHeadColumns headcols) hrows brows) = asContents $ elementWithAttrAndContents "TableBody" attr children where attr = (idn, cls, (atNameRowHeadColumns, intAsText headcols) : attrs)- header_rows = asBlockOfBlocks $ elementWithContents tgNameBodyHeader $ concatMap rowToXML hrows- body_rows = asBlockOfBlocks $ elementWithContents tgNameBodyBody $ concatMap rowToXML brows+ header_rows = asContents $ elementWithContents tgNameBodyHeader $ concatMap rowToXML hrows+ body_rows = asContents $ elementWithContents tgNameBodyBody $ concatMap rowToXML brows children = header_rows ++ body_rows tableFootToXML :: TableFoot -> [Content]-tableFootToXML (TableFoot attr rows) = asBlockOfBlocks $ elementWithAttrAndContents "TableFoot" attr $ concatMap rowToXML rows+tableFootToXML (TableFoot attr rows) = asContents $ elementWithAttrAndContents "TableFoot" attr $ concatMap rowToXML rows rowToXML :: Row -> [Content]-rowToXML (Row attr cells) = asBlockOfBlocks $ elementWithAttrAndContents "Row" attr $ concatMap cellToXML cells+rowToXML (Row attr cells) = asContents $ elementWithAttrAndContents "Row" attr $ concatMap cellToXML cells cellToXML :: Cell -> [Content]-cellToXML (Cell (idn, cls, attrs) alignment (RowSpan rowspan) (ColSpan colspan) blocks) = asBlockOfBlocks $ elementWithAttrAndContents "Cell" attr $ blocksToXML blocks+cellToXML (Cell (idn, cls, attrs) alignment (RowSpan rowspan) (ColSpan colspan) blocks) = asContents $ elementWithAttrAndContents "Cell" attr $ blocksToXML blocks where with_alignment a = (atNameAlignment, itemName alignment) : a with_rowspan a = if rowspan > 1 then (atNameRowspan, intAsText rowspan) : a else a
@@ -32,7 +32,7 @@ import Commonmark.Entity (lookupEntity) import Text.HTML.TagSoup.Entity (htmlEntities) import Text.DocLayout- ( ($$), char, hcat, nest, text, Doc, HasChars )+ ( ($$), nest, text, Doc, HasChars ) import Text.Printf (printf) import qualified Data.Map as M import Data.String ( IsString )@@ -47,35 +47,63 @@ '"' -> """ c -> T.singleton c +-- | Is this character legal in XML at all?+-- See <https://www.w3.org/TR/xml/#charsets>.+isLegalXMLChar :: Char -> Bool+isLegalXMLChar c = c == '\t' || c == '\n' || c == '\r' ||+ (c >= '\x20' && c <= '\xD7FF') ||+ (c >= '\xE000' && c <= '\xFFFD') ||+ (c >= '\x10000' && c <= '\x10FFFF')++-- | Does this character need to be replaced or dropped by+-- 'escapeStringForXML'?+isSpecialXMLChar :: Char -> Bool+isSpecialXMLChar = \case+ '&' -> True+ '<' -> True+ '>' -> True+ '"' -> True+ c -> not (isLegalXMLChar c)+ -- | Escape string as needed for XML. Entity references are not preserved. escapeStringForXML :: Text -> Text-escapeStringForXML = T.concatMap escapeCharForXML . T.filter isLegalXMLChar- where isLegalXMLChar c = c == '\t' || c == '\n' || c == '\r' ||- (c >= '\x20' && c <= '\xD7FF') ||- (c >= '\xE000' && c <= '\xFFFD') ||- (c >= '\x10000' && c <= '\x10FFFF')- -- see https://www.w3.org/TR/xml/#charsets+escapeStringForXML t+ | T.null rest = t -- nothing to do, so return the input without copying+ | otherwise = T.concat (chunk : go rest)+ where+ (chunk, rest) = T.break isSpecialXMLChar t+ -- 'go' is always given text starting with a special character. Text+ -- between special characters is copied in runs rather than character+ -- by character.+ go t' = let (specials, t'') = T.span isSpecialXMLChar t'+ (chunk', rest') = T.break isSpecialXMLChar t''+ in T.concatMap escape specials :+ if T.null rest' then [chunk'] else chunk' : go rest'+ -- a special character is either one of & < > " or else not+ -- representable in XML, in which case it is dropped+ escape c | isLegalXMLChar c = escapeCharForXML c+ | otherwise = T.empty -- | Escape newline characters as escapeNls :: Text -> Text-escapeNls = T.concatMap $ \case- '\n' -> " "- c -> T.singleton c+escapeNls t+ | T.elem '\n' t = T.replace "\n" " " t+ | otherwise = t --- | Return a text object with a string of formatted XML attributes.-attributeList :: (HasChars a, IsString a) => [(Text, Text)] -> Doc a-attributeList = hcat . map- (\(a, b) -> text (T.unpack $ " " <> escapeStringForXML a <> "=\"" <>- escapeNls (escapeStringForXML b) <> "\""))+-- | The pieces of a string of formatted XML attributes, each attribute+-- preceded by a space.+attributeList :: [(Text, Text)] -> [Text]+attributeList = concatMap+ (\(a, b) -> [" ", escapeStringForXML a, "=\"",+ escapeNls (escapeStringForXML b), "\""]) -- | Put the supplied contents between start and end tags of tagType, -- with specified attributes and (if specified) indentation. inTags :: (HasChars a, IsString a) => Bool -> Text -> [(Text, Text)] -> Doc a -> Doc a inTags isIndented tagType attribs contents =- let openTag = char '<' <> text (T.unpack tagType) <> attributeList attribs <>- char '>'- closeTag = text "</" <> text (T.unpack tagType) <> char '>'+ let openTag = tagDoc ("<" : tagType : attributeList attribs ++ [">"])+ closeTag = tagDoc ["</", tagType, ">"] in if isIndented then openTag $$ nest 2 contents $$ closeTag else openTag <> contents <> closeTag@@ -84,7 +112,13 @@ selfClosingTag :: (HasChars a, IsString a) => Text -> [(Text, Text)] -> Doc a selfClosingTag tagType attribs =- char '<' <> text (T.unpack tagType) <> attributeList attribs <> text " />"+ tagDoc ("<" : tagType : attributeList attribs ++ [" />"])++-- | Assemble the pieces of a tag into a single 'Doc', so that a tag+-- costs one 'Text' allocation and one 'Doc' node rather than one of+-- each per tag name and per attribute.+tagDoc :: (HasChars a, IsString a) => [Text] -> Doc a+tagDoc = text . T.unpack . T.concat -- | Put the supplied contents between start and end tags of tagType. inTagsSimple :: (HasChars a, IsString a)
@@ -2,7 +2,9 @@ {-# LANGUAGE ScopedTypeVariables #-} module Text.Pandoc.XMLFormat- ( atNameAlignment,+ ( decodeAttrName,+ encodeAttrName,+ atNameAlignment, atNameApiVersion, atNameCitationHash, atNameCitationMode,@@ -41,7 +43,93 @@ ) where +import Data.Char (chr, digitToInt, isAsciiLower, isAsciiUpper, isDigit, isHexDigit, ord, toUpper) import Data.Text (Text)+import qualified Data.Text as T+import Numeric (showHex)++-- | Encode an attribute name so that it is a valid XML name.+-- Characters that are not allowed in XML names -- and colons, which+-- XML parsers treat as namespace separators -- are encoded as+-- _xHHHH_, where HHHH is the hexadecimal code of the character+-- (uppercase, at least four digits). An underscore that introduces+-- a literal "_x" is encoded as _x005F_, so that decoding is+-- unambiguous. For example, "typst:property" is encoded as+-- "typst_x003A_property".+encodeAttrName :: Text -> Text+encodeAttrName name =+ case T.unpack name of+ [] -> name+ c : cs+ | isNameStartChar c && all isNameChar cs && not ("_x" `T.isInfixOf` name) ->+ name+ | otherwise ->+ T.pack $ concat $ go isNameStartChar (c : cs)+ where+ go _ [] = []+ go ok (c : cs)+ | c == '_' && take 1 cs == "x" = encodeChar '_' : go isNameChar cs+ | ok c = [c] : go isNameChar cs+ | otherwise = encodeChar c : go isNameChar cs+ encodeChar c = "_x" ++ replicate (4 - length h) '0' ++ h ++ "_"+ where+ h = map toUpper $ showHex (ord c) ""++-- | Decode an attribute name encoded by 'encodeAttrName': _xHHHH_+-- sequences (four to six hexadecimal digits) are decoded to the+-- character with the given code.+decodeAttrName :: Text -> Text+decodeAttrName name+ | "_x" `T.isInfixOf` name = T.concat $ go name+ | otherwise = name+ where+ go t =+ case T.breakOn "_x" t of+ (pre, rest)+ | T.null rest -> [pre]+ | otherwise ->+ let body = T.drop 2 rest+ digits = T.takeWhile isHexDigit body+ n = T.length digits+ code = T.foldl' (\acc d -> 16 * acc + digitToInt d) 0 digits+ in if n >= 4+ && n <= 6+ && "_" `T.isPrefixOf` T.drop n body+ && validChar code+ then pre : T.singleton (chr code) : go (T.drop (n + 1) body)+ else pre : "_x" : go body+ validChar code = code <= 0x10FFFF && not (code >= 0xD800 && code <= 0xDFFF)++-- the XML NameStartChar production, without the colon (which XML+-- parsers treat as a namespace separator)+isNameStartChar :: Char -> Bool+isNameStartChar c =+ isAsciiUpper c+ || isAsciiLower c+ || c == '_'+ || (c >= '\xC0' && c <= '\xD6')+ || (c >= '\xD8' && c <= '\xF6')+ || (c >= '\xF8' && c <= '\x2FF')+ || (c >= '\x370' && c <= '\x37D')+ || (c >= '\x37F' && c <= '\x1FFF')+ || (c >= '\x200C' && c <= '\x200D')+ || (c >= '\x2070' && c <= '\x218F')+ || (c >= '\x2C00' && c <= '\x2FEF')+ || (c >= '\x3001' && c <= '\xD7FF')+ || (c >= '\xF900' && c <= '\xFDCF')+ || (c >= '\xFDF0' && c <= '\xFFFD')+ || (c >= '\x10000' && c <= '\xEFFFF')++-- the XML NameChar production, without the colon+isNameChar :: Char -> Bool+isNameChar c =+ isNameStartChar c+ || isDigit c+ || c == '-'+ || c == '.'+ || c == '\xB7'+ || (c >= '\x300' && c <= '\x36F')+ || (c >= '\x203F' && c <= '\x2040') -- the attribute carrying the API version of pandoc types in the main Pandoc element atNameApiVersion :: Text
@@ -0,0 +1,198 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+{- |+ Module : Tests.Data+ Copyright : © 2026 John MacFarlane+ License : GNU GPL, version 2 or above++ Maintainer : John MacFarlane <jgm@berkeley.edu>+ Stability : alpha+ Portability : portable++Tests for data file lookup, including lookup in @PANDOC_DATA_DIRS@.+-}+module Tests.Data (tests) where++import Codec.Archive.Zip (Archive, filesInArchive,+#ifdef EMBED_DATA_FILES+ findEntryByPath, fromEntry,+#endif+ toArchive)+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import Data.List (intercalate)+import qualified Data.Text as T+import System.FilePath ((</>), searchPathSeparator)+import System.Info (os)+import Test.Tasty+import Test.Tasty.HUnit+import Text.Pandoc.Class (PandocPure, FileInfo(..), FileTree,+ findFileWithDataFallback, getDataDirs,+ getPandocDataDirs, insertInFileTree,+ modifyPureState, PureState(..), runIOorExplode,+ runPure, setUserDataDir)+import Text.Pandoc.Data (readDataFile, readDefaultDataFile)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)++-- | An absolute path that is valid on the current platform.+absPath :: FilePath -> FilePath+absPath d = if os == "mingw32" then "C:\\" </> d else "/" </> d++userDir, dirA, dirB :: FilePath+userDir = absPath "user"+dirA = absPath "dirA"+dirB = absPath "dirB"++pandocDataDirs :: [FilePath] -> T.Text+pandocDataDirs = T.pack . intercalate [searchPathSeparator]++-- | Run a 'PandocPure' action with the given user data directory,+-- value of @PANDOC_DATA_DIRS@, and files.+runWith :: Maybe FilePath -> Maybe T.Text -> FileTree -> [(FilePath, B.ByteString)]+ -> PandocPure a -> Either String a+runWith mbUserDir mbDataDirs defaultFiles files action =+ either (Left . show) Right $ runPure $ do+ modifyPureState $ \st ->+ st{ stEnv = maybe [] (\x -> [("PANDOC_DATA_DIRS", x)]) mbDataDirs+ , stFiles = foldr (\(fp, bs) -> insertInFileTree fp (fileInfo bs))+ defaultFiles files }+ setUserDataDir mbUserDir+ action++fileInfo :: B.ByteString -> FileInfo+fileInfo bs = FileInfo{ infoFileMTime = posixSecondsToUTCTime 0+ , infoFileContents = bs }++-- | Default data files that are needed by the tests, placed where+-- 'PandocPure' looks for them if data files are not embedded.+getDefaultFiles :: IO (FileTree, B.ByteString, Archive)+getDefaultFiles = runIOorExplode $ do+ abbrevs <- readDefaultDataFile "abbreviations"+ refdocx <- toArchive . BL.fromStrict <$>+ readDefaultDataFile "reference.docx"+ parts <- mapM (\p -> (p,) <$> readDefaultDataFile ("docx" </> p))+ (filesInArchive refdocx)+ let tree = foldr (\(fp, bs) -> insertInFileTree ("data" </> fp) (fileInfo bs))+ mempty+ (("abbreviations", abbrevs) :+ [("docx" </> p, bs) | (p, bs) <- parts])+ return (tree, abbrevs, refdocx)++#ifdef EMBED_DATA_FILES+-- | Contents of a file in a docx archive.+docxPart :: FilePath -> B.ByteString -> Maybe BL.ByteString+docxPart p = fmap fromEntry . findEntryByPath p . toArchive . BL.fromStrict+#endif++tests :: [TestTree]+tests =+ [ testGroup "getPandocDataDirs"+ [ testCase "PANDOC_DATA_DIRS unset" $+ runWith Nothing Nothing mempty [] getPandocDataDirs @?= Right []+ , testCase "ignoring relative and empty entries" $+ runWith Nothing (Just (pandocDataDirs [dirA, "relative", "", dirB]))+ mempty [] getPandocDataDirs+ @?= Right [dirA, dirB]+ ]+ , testGroup "getDataDirs"+ [ testCase "user data dir comes first" $+ runWith (Just userDir) (Just (pandocDataDirs [dirA, dirB])) mempty []+ (getDataDirs "templates/default.html5")+ @?= Right [userDir, dirA, dirB]+ , testCase "no user data dir" $+ runWith Nothing (Just (pandocDataDirs [dirA])) mempty []+ (getDataDirs "abbreviations")+ @?= Right [dirA]+ , testCase "absolute path" $+ runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty []+ (getDataDirs (absPath "abbreviations"))+ @?= Right []+ , testCase "path relative to parent directory" $+ runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty []+ (getDataDirs (".." </> "abbreviations"))+ @?= Right []+ ]+ , withResource getDefaultFiles (const (return ())) $ \getDefaults ->+ testGroup "readDataFile"+ [ testCase "user data dir takes precedence" $ do+ (defs, _, _) <- getDefaults+ runWith (Just userDir) (Just (pandocDataDirs [dirA])) defs+ [ (userDir </> "abbreviations", "user")+ , (dirA </> "abbreviations", "dirA") ]+ (readDataFile "abbreviations")+ @?= Right "user"+ , testCase "earlier PANDOC_DATA_DIRS entries take precedence" $ do+ (defs, _, _) <- getDefaults+ runWith (Just userDir) (Just (pandocDataDirs [dirA, dirB])) defs+ [ (dirA </> "abbreviations", "dirA")+ , (dirB </> "abbreviations", "dirB") ]+ (readDataFile "abbreviations")+ @?= Right "dirA"+ , testCase "later PANDOC_DATA_DIRS entries are used" $ do+ (defs, _, _) <- getDefaults+ runWith Nothing (Just (pandocDataDirs [dirA, dirB])) defs+ [ (dirB </> "abbreviations", "dirB") ]+ (readDataFile "abbreviations")+ @?= Right "dirB"+#ifdef EMBED_DATA_FILES+ , testCase "falls back to default data files" $ do+ (defs, abbrevs, _) <- getDefaults+ runWith (Just userDir) (Just (pandocDataDirs [dirA])) defs []+ (readDataFile "abbreviations")+ @?= Right abbrevs+ , testCase "readDefaultDataFile ignores data dirs" $ do+ (defs, abbrevs, _) <- getDefaults+ runWith (Just userDir) (Just (pandocDataDirs [dirA])) defs+ [ (userDir </> "abbreviations", "user")+ , (dirA </> "abbreviations", "dirA") ]+ (readDefaultDataFile "abbreviations")+ @?= Right abbrevs+ , testCase "reference.docx from PANDOC_DATA_DIRS" $ do+ (defs, _, _) <- getDefaults+ runWith Nothing (Just (pandocDataDirs [dirA])) defs+ [ (dirA </> "reference.docx", "dirA") ]+ (readDataFile "reference.docx")+ @?= Right "dirA"+ , testCase "reference.docx component from PANDOC_DATA_DIRS" $ do+ (defs, _, _) <- getDefaults+ let styles = "word" </> "styles.xml"+ runWith Nothing (Just (pandocDataDirs [dirA])) defs+ [ (dirA </> "docx" </> styles, "dirA") ]+ (docxPart styles <$> readDataFile "reference.docx")+ @?= Right (Just "dirA")+ , testCase "readDefaultDataFile ignores reference.docx components" $ do+ (defs, _, refdocx) <- getDefaults+ let styles = "word" </> "styles.xml"+ let expected = fromEntry <$> findEntryByPath styles refdocx+ runWith (Just userDir) (Just (pandocDataDirs [dirA])) defs+ [ (userDir </> "docx" </> styles, "user")+ , (dirA </> "docx" </> styles, "dirA") ]+ (docxPart styles <$> readDefaultDataFile "reference.docx")+ @?= Right expected+#endif+ ]+ , testGroup "findFileWithDataFallback"+ [ testCase "file in working directory takes precedence" $+ runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty+ [ ("f.lua", "")+ , (dirA </> "filters" </> "f.lua", "") ]+ (findFileWithDataFallback "filters" "f.lua")+ @?= Right (Just "f.lua")+ , testCase "user data dir takes precedence" $+ runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty+ [ (userDir </> "filters" </> "f.lua", "")+ , (dirA </> "filters" </> "f.lua", "") ]+ (findFileWithDataFallback "filters" "f.lua")+ @?= Right (Just (userDir </> "filters" </> "f.lua"))+ , testCase "file in PANDOC_DATA_DIRS" $+ runWith (Just userDir) (Just (pandocDataDirs [dirA, dirB])) mempty+ [ (dirB </> "filters" </> "f.lua", "") ]+ (findFileWithDataFallback "filters" "f.lua")+ @?= Right (Just (dirB </> "filters" </> "f.lua"))+ , testCase "file not found" $+ runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty []+ (findFileWithDataFallback "filters" "f.lua")+ @?= Right Nothing+ ]+ ]
@@ -0,0 +1,335 @@+{-# LANGUAGE OverloadedStrings #-}+{- |+ Module : Tests.ImageSize+ Copyright : © 2025 John MacFarlane+ License : GNU GPL, version 2 or above++ Maintainer : John MacFarlane <jgm@berkeley.edu>+ Stability : alpha+ Portability : portable++Tests for image type and size detection.+-}+module Tests.ImageSize (tests) where++import Data.Bits (shiftR)+import qualified Data.ByteString as B+import Data.Word (Word8)+import Test.Tasty+import Test.Tasty.HUnit+import Text.Pandoc.ImageSize+import Text.Pandoc.Options (def)++-- helpers to construct binary test data:++le32, be32 :: Int -> B.ByteString+le32 n = B.pack $ map (fromIntegral . (n `shiftR`) . (8 *)) [0,1,2,3]+be32 n = B.pack $ map (fromIntegral . (n `shiftR`) . (8 *)) [3,2,1,0]++be16 :: Int -> B.ByteString+be16 n = B.pack $ map (fromIntegral . (n `shiftR`) . (8 *)) [1,0]++-- | An ISO BMFF box with the given type and contents.+box :: B.ByteString -> B.ByteString -> B.ByteString+box name body = be32 (8 + B.length body) <> name <> body++be64 :: Int -> B.ByteString+be64 n = B.pack $ map (fromIntegral . (n `shiftR`) . (8 *)) [7,6..0]++-- | An ISO BMFF box using the 64-bit "largesize" field (size == 1).+largeBox :: B.ByteString -> B.ByteString -> B.ByteString+largeBox name body = be32 1 <> name <> be64 (16 + B.length body) <> body++-- | An ISO BMFF box with size == 0 (extends to the end of the file).+zeroBox :: B.ByteString -> B.ByteString -> B.ByteString+zeroBox name body = be32 0 <> name <> body++-- | An EMF header with the given frame bounds (1/100 mm), reference+-- device size in pixels, and reference device size in mm.+emfFile :: [Int] -> [Int] -> [Int] -> B.ByteString+emfFile frame device mm = B.concat $+ [le32 1, B.replicate 20 0] <> map le32 frame <>+ [" EMF", B.replicate 28 0] <> map le32 (device <> mm)++-- | A WebP RIFF container with the given chunk.+webpFile :: B.ByteString -> B.ByteString+webpFile chunk = "RIFF\0\0\0\0WEBP" <> chunk++svgFile :: B.ByteString -> B.ByteString+svgFile attrs =+ "<svg xmlns=\"http://www.w3.org/2000/svg\" " <> attrs <> "></svg>"++jpegBare, jpegApp0 :: B.ByteString+jpegBare = B.pack [0xff, 0xd8, 0xff, 0xdb] <> "rest"+jpegApp0 = B.pack [0xff, 0xd8, 0xff, 0xe0] <> "rest"++-- | A PNG chunk with the given type and body (and a dummy CRC).+pngChunk :: B.ByteString -> B.ByteString -> B.ByteString+pngChunk name body = be32 (B.length body) <> name <> body <> B.replicate 4 0++-- | A PNG header with the given extra chunks after IHDR (and no+-- image data).+pngFile :: [B.ByteString] -> Int -> Int -> B.ByteString+pngFile chunks w h = B.concat $+ [ "\x89PNG\r\n\x1a\n"+ , pngChunk "IHDR" (be32 w <> be32 h <> B.pack [8, 3, 0, 0, 0]) ]+ <> chunks++-- | A pHYs chunk with the given unit (1 = pixels per meter) and+-- pixel densities.+physChunk :: Word8 -> Int -> Int -> B.ByteString+physChunk unit x y = pngChunk "pHYs" (be32 x <> be32 y <> B.pack [unit])++-- | A JPEG marker segment with the given marker and body.+jpegSeg :: Word8 -> B.ByteString -> B.ByteString+jpegSeg m body = B.pack [0xff, m] <> be16 (B.length body + 2) <> body++-- | A JPEG header: SOI marker, the given segments, and a baseline+-- SOF segment with the given width and height (and no image data).+jpegFile :: [B.ByteString] -> Int -> Int -> B.ByteString+jpegFile segs w h = B.concat $ [B.pack [0xff, 0xd8]] <> segs <>+ [jpegSeg 0xc0 (B.pack [8] <> be16 h <> be16 w <> B.pack [3])]++-- | A JFIF APP0 segment with the given density units (0 = aspect+-- ratio only, 1 = dots per inch, 2 = dots per cm) and densities.+jfifSeg :: Word8 -> Int -> Int -> B.ByteString+jfifSeg units x y = jpegSeg 0xe0 $+ "JFIF\0" <> B.pack [1, 2, units] <> be16 x <> be16 y <> "\0\0"++-- | An Exif APP1 segment giving 300x200 dpi resolution.+exifSeg :: B.ByteString+exifSeg = jpegSeg 0xe1 $ "Exif\0\0"+ <> "MM" <> be16 42 <> be32 8 -- TIFF header (big-endian)+ <> be16 3 -- IFD entry count+ <> entry 0x011a 5 (be32 50) -- XResolution at offset 50+ <> entry 0x011b 5 (be32 58) -- YResolution at offset 58+ <> entry 0x0128 3 (be16 2 <> be16 0) -- ResolutionUnit: inches+ <> be32 0 -- next IFD offset+ <> be32 300 <> be32 1 -- XResolution = 300/1+ <> be32 200 <> be32 1 -- YResolution = 200/1+ where entry tag typ val = be16 tag <> be16 typ <> be32 1 <> val++-- the contents of a meta box locating an ispe (image spatial+-- extents) box giving dimensions 640x480:+avifMeta :: B.ByteString+avifMeta = B.replicate 4 0 <> -- version/flags+ box "iprp" (box "ipco"+ (box "ispe" (B.replicate 4 0 <> be32 640 <> be32 480)))++-- an AVIF image with an ispe (image spatial extents) box:+avifIspe :: B.ByteString+avifIspe = box "ftyp" ("avif" <> B.replicate 4 0) <> box "meta" avifMeta++-- an (animated) AVIF image with dimensions only in a tkhd box:+avisTkhd :: B.ByteString+avisTkhd = box "ftyp" ("avis" <> B.replicate 4 0)+ <> box "moov" (box "trak" (box "tkhd"+ (B.replicate 4 0 <> -- version/flags+ B.replicate 72 0 <> -- times, track id, duration, etc.+ be32 (640 * 65536) <> be32 (480 * 65536)))) -- 16.16++-- lossless webp, 100x200: width - 1 in bits 0-13, height - 1 in+-- bits 14-27 of the little-endian word after the 0x2f signature+webpLossless :: B.ByteString+webpLossless = webpFile $ "VP8L\0\0\0\0"+ <> B.pack [0x2f, 0x63, 0xc0, 0x31, 0x00]++-- lossy webp, 320x240: frame tag, then 9d 01 2a keyframe signature,+-- then 14-bit width and height as little-endian 16-bit words+webpLossy :: B.ByteString+webpLossy = webpFile $ "VP8 \0\0\0\0"+ <> B.pack [0x00, 0x00, 0x00, 0x9d, 0x01, 0x2a, 0x40, 0x01, 0xf0, 0x00]++-- extended webp, 1000x500: canvas width and height - 1 as 24-bit+-- little-endian words after the flags+webpExtended :: B.ByteString+webpExtended = webpFile $ "VP8X" <> B.replicate 8 0+ <> B.pack [0xe7, 0x03, 0x00, 0xf3, 0x01, 0x00]++-- zlib-compressed "<</MediaBox [0 0 100 200]>>"+compressedMediaBox :: B.ByteString+compressedMediaBox = B.pack+ [ 120, 156, 179, 177, 209, 247, 77, 77, 201, 76, 116, 202, 175, 80+ , 136, 54, 80, 48, 80, 48, 52, 48, 80, 48, 50, 48, 136, 181, 179+ , 3, 0, 104, 186, 6, 232 ]++tests :: [TestTree]+tests =+ [ testGroup "imageType"+ [ testCase "png" $ imageType "\x89PNG\r\n\x1a\n" @?= Just Png+ , testCase "gif" $ imageType "GIF89a" @?= Just Gif+ , testCase "tiff (little-endian)" $ imageType "II\x2a\0" @?= Just Tiff+ , testCase "tiff (big-endian)" $ imageType "MM\0\x2a" @?= Just Tiff+ , testCase "jpeg with app segment" $ imageType jpegApp0 @?= Just Jpeg+ , testCase "jpeg without app segment" $ imageType jpegBare @?= Just Jpeg+ , testCase "pdf" $ imageType "%PDF-1.5" @?= Just Pdf+ , testCase "eps" $ imageType "%!PS-Adobe-3.0 EPSF-3.0" @?= Just Eps+ , testCase "svg" $ imageType (svgFile "") @?= Just Svg+ , testCase "svg with xml declaration" $+ imageType ("<?xml version=\"1.0\"?>\n" <> svgFile "") @?= Just Svg+ , testCase "uppercase svg with xml declaration" $+ imageType "<?xml version=\"1.0\"?>\n<SVG></SVG>" @?= Just Svg+ , testCase "xml that is not svg" $+ imageType "<?xml version=\"1.0\"?>\n<html><p>svg</p></html>"+ @?= Nothing+ , testCase "svg with BOM" $+ imageType ("\xef\xbb\xbf" <> svgFile "") @?= Just Svg+ , testCase "emf" $+ imageType (emfFile [0,0,1,1] [1,1] [1,1]) @?= Just Emf+ , testCase "webp" $ imageType webpLossless @?= Just Webp+ , testCase "avif brand" $ imageType avifIspe @?= Just Avif+ , testCase "avis brand" $ imageType avisTkhd @?= Just Avif+ , testCase "other ISO media is not avif" $+ imageType (box "ftyp" ("isom" <> B.replicate 4 0)) @?= Nothing+ , testCase "garbage" $ imageType "garbage!" @?= Nothing+ ]+ , testGroup "numUnit"+ [ testCase "number and unit" $ numUnit "3cm" @?= Just (3.0, "cm")+ , testCase "space between number and unit" $+ numUnit "3 cm" @?= Just (3.0, "cm")+ , testCase "bare number" $ numUnit "3.5" @?= Just (3.5, "")+ , testCase "no number" $ numUnit "cm" @?= Nothing+ , testCase "lengthToDim with space" $+ lengthToDim "3 cm" @?= Just (Centimeter 3)+ ]+ , testGroup "imageSize"+ [ testGroup "eps"+ [ testCase "zero origin" $+ imageSize def "%!PS EPSF\n%%BoundingBox: 0 0 612 792\n"+ @?= Right (ImageSize 612 792 72 72)+ , testCase "nonzero origin" $+ imageSize def "%!PS EPSF\n%%BoundingBox: 10 20 110 220\n"+ @?= Right (ImageSize 100 200 72 72)+ , testCase "negative origin" $+ imageSize def "%!PS EPSF\n%%BoundingBox: -10 -10 90 190\n"+ @?= Right (ImageSize 100 200 72 72)+ ]+ , testGroup "pdf"+ [ testCase "MediaBox" $+ imageSize def "%PDF-1.4\n<</MediaBox [0 0 612 792]>>"+ @?= Right (ImageSize 612 792 72 72)+ , testCase "MediaBox in compressed object stream" $+ imageSize def ("%PDF-1.5\n<</Type /ObjStm>>\nstream\n"+ <> compressedMediaBox <> "\nendstream\n")+ @?= Right (ImageSize 100 200 72 72)+ , testCase "corrupt compressed object stream" $+ imageSize def ("%PDF-1.5\n<</Type /ObjStm>>\nstream\n"+ <> "NOT ZLIB DATA\nendstream\n")+ @?= Left "could not determine PDF size"+ , testCase "MediaBox after corrupt object stream" $+ imageSize def ("%PDF-1.5\n<</Type /ObjStm>>\nstream\n"+ <> "NOT ZLIB DATA\nendstream\n"+ <> "<</MediaBox [0 0 300 400]>>")+ @?= Right (ImageSize 300 400 72 72)+ , testCase "MediaBox after empty object stream" $+ imageSize def ("%PDF-1.5\n<</Type /ObjStm>>\nstream\n"+ <> "endstream<</MediaBox [0 0 25 50]>>")+ @?= Right (ImageSize 25 50 72 72)+ ]+ , testGroup "svg"+ [ testCase "width and height attributes" $+ imageSize def (svgFile "width=\"50\" height=\"60\"")+ @?= Right (ImageSize 50 60 96 96)+ , testCase "viewBox fallback" $+ imageSize def (svgFile "viewBox=\"0 0 100 200\"")+ @?= Right (ImageSize 100 200 96 96)+ , testCase "viewBox with commas" $+ imageSize def (svgFile "viewBox=\"0,0,100,200\"")+ @?= Right (ImageSize 100 200 96 96)+ , testCase "viewBox with fractional values" $+ imageSize def (svgFile "viewBox=\"0, 0, 210.5, 297.3\"")+ @?= Right (ImageSize 210 297 96 96)+ , testCase "no size information" $+ imageSize def (svgFile "")+ @?= Left "could not determine SVG size"+ ]+ , testGroup "emf"+ [ testCase "size and dpi from header" $+ imageSize def (emfFile [0,0,10000,5000] [1024,768] [320,240])+ @?= Right (ImageSize 320 160 81 81)+ , testCase "nonzero frame origin" $+ imageSize def (emfFile [2000,1000,12000,6000] [1024,768] [320,240])+ @?= Right (ImageSize 320 160 81 81)+ , testCase "zero-size reference device" $+ imageSize def (emfFile [0,0,10000,5000] [1024,768] [0,240])+ @?= Left "could not determine EMF size"+ ]+ , testGroup "webp"+ [ testCase "lossless (VP8L)" $+ imageSize def webpLossless @?= Right (ImageSize 100 200 96 96)+ , testCase "lossy (VP8)" $+ imageSize def webpLossy @?= Right (ImageSize 320 240 96 96)+ , testCase "extended (VP8X)" $+ imageSize def webpExtended @?= Right (ImageSize 1000 500 96 96)+ ]+ , testGroup "avif"+ [ testCase "ispe box" $+ imageSize def avifIspe @?= Right (ImageSize 640 480 96 96)+ , testCase "tkhd box" $+ imageSize def avisTkhd @?= Right (ImageSize 640 480 96 96)+ , testCase "meta box with 64-bit largesize" $+ imageSize def (box "ftyp" ("avif" <> B.replicate 4 0)+ <> largeBox "meta" avifMeta)+ @?= Right (ImageSize 640 480 96 96)+ , testCase "meta box with size 0 (extends to end of file)" $+ imageSize def (box "ftyp" ("avif" <> B.replicate 4 0)+ <> zeroBox "meta" avifMeta)+ @?= Right (ImageSize 640 480 96 96)+ , testCase "unknown box before meta box" $+ imageSize def (box "ftyp" ("avif" <> B.replicate 4 0)+ <> box "free" "junk" <> box "meta" avifMeta)+ @?= Right (ImageSize 640 480 96 96)+ ]+ , testGroup "png" -- headers without image data, so these only+ -- succeed if no decoding is attempted+ [ testCase "no pHYs chunk" $+ imageSize def (pngFile [] 640 480)+ @?= Right (ImageSize 640 480 72 72)+ , testCase "pHYs in pixels per meter" $+ imageSize def (pngFile [physChunk 1 3937 3937] 640 480)+ @?= Right (ImageSize 640 480 99 99)+ , testCase "pHYs with unknown unit" $+ imageSize def (pngFile [physChunk 0 4 3] 640 480)+ @?= Right (ImageSize 640 480 72 72)+ , testCase "pHYs after another chunk" $+ imageSize def (pngFile [ pngChunk "tEXt" "k\0v"+ , physChunk 1 3937 3937 ] 640 480)+ @?= Right (ImageSize 640 480 99 99)+ ]+ , testGroup "jpeg" -- headers without image data, so these only+ -- succeed if no decoding is attempted+ [ testCase "jfif dpi" $+ imageSize def (jpegFile [jfifSeg 1 96 96] 640 480)+ @?= Right (ImageSize 640 480 96 96)+ , testCase "jfif density in dots per cm" $+ imageSize def (jpegFile [jfifSeg 2 100 100] 640 480)+ @?= Right (ImageSize 640 480 254 254)+ , testCase "jfif aspect ratio only" $+ imageSize def (jpegFile [jfifSeg 0 1 1] 640 480)+ @?= Right (ImageSize 640 480 72 72)+ , testCase "exif resolution" $+ imageSize def (jpegFile [exifSeg] 640 480)+ @?= Right (ImageSize 640 480 300 200)+ , testCase "exif overrides jfif" $+ imageSize def (jpegFile [jfifSeg 1 96 96, exifSeg] 640 480)+ @?= Right (ImageSize 640 480 300 200)+ , testCase "no app segments" $+ imageSize def (jpegFile [jpegSeg 0xdb (B.replicate 65 0)] 640 480)+ @?= Right (ImageSize 640 480 72 72)+ , testCase "defective zero density defaults to 72 (#6880)" $+ imageSize def (jpegFile [jfifSeg 1 0 0] 640 480)+ @?= Right (ImageSize 640 480 72 72)+ ]+ , testGroup "fixtures"+ [ testCase "lalune.jpg" $ do+ img <- B.readFile "lalune.jpg"+ imageSize def img @?= Right (ImageSize 250 250 120 120)+ , testCase "fb2/test-small.png" $ do+ img <- B.readFile "fb2/test-small.png"+ imageSize def img @?= Right (ImageSize 48 32 71 71)+ , testCase "bodybg.gif" $ do+ img <- B.readFile "bodybg.gif"+ imageSize def img @?= Right (ImageSize 230 334 72 72)+ ]+ ]+ ]
@@ -6,6 +6,7 @@ -- import Tests.Helpers import Text.Pandoc.Class.IO (extractMedia) import Text.Pandoc.Class (fillMediaBag, runIOorExplode)+import Text.Pandoc.MediaBag (insertMedia, lookupMedia, mediaPath) import System.IO.Temp (withTempDirectory) import System.FilePath import Text.Pandoc.Builder as B@@ -14,6 +15,34 @@ tests :: [TestTree] tests = [+ testCase "insertMedia mediaPath sanitization" $ do+ -- a ".." substring that is not a path component is harmless+ -- and should not cause the file to be renamed:+ let bag = insertMedia "foo..bar.png" Nothing "contents" mempty+ (mediaPath <$> lookupMedia "foo..bar.png" bag) @?= Just "foo..bar.png"+ -- a ".." path component must not survive into mediaPath:+ let bag2 = insertMedia "../evil.png" Nothing "contents" mempty+ case lookupMedia "../evil.png" bag2 of+ Nothing -> assertFailure "item not found in media bag"+ Just item -> assertBool "mediaPath contains a .. component"+ (".." `notElem` splitDirectories (mediaPath item)),+ testCase "path canonicalization" $ do+ -- redundant . and .. components are collapsed, so equivalent+ -- spellings of a path refer to the same item:+ let bag = insertMedia "img/../sub/./lalune.png" Nothing "contents" mempty+ (mediaPath <$> lookupMedia "sub/lalune.png" bag) @?= Just "sub/lalune.png"+ (mediaPath <$> lookupMedia "img/../sub/lalune.png" bag)+ @?= Just "sub/lalune.png",+ testCase "no mediaPath collisions between escaped and literal keys" $ do+ -- "a%20b.png" used to unescape to the same mediaPath as the+ -- literal "a b.png", so one clobbered the other on extraction:+ let bag = insertMedia "a%20b.png" Nothing "contents1" $+ insertMedia "a b.png" Nothing "contents2" mempty+ case (lookupMedia "a%20b.png" bag, lookupMedia "a b.png" bag) of+ (Just i1, Just i2) -> assertBool+ "escaped and literal keys share a mediaPath"+ (mediaPath i1 /= mediaPath i2)+ _ -> assertFailure "items not found in media bag", testCase "test fillMediaBag & extractMedia" $ withTempDirectory "." "extractMediaTest" $ \tmpdir -> do -- Use absolute paths so the test does not need to change@@ -27,7 +56,9 @@ -- absolute path -> extracted with hashed name B.para (B.image (T.pack absLalune) "" mempty) <> B.para (B.image "data:image/png;base64,cHJpbnQgImhlbGxvIgo=;.lua+%2f%2e%2e%2f%2e%2e%2fa%2elua" "" mempty) <>- B.para (B.image "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" "" mempty)+ B.para (B.image "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" "" mempty) <>+ -- the data: scheme is case-insensitive+ B.para (B.image "DATA:image/gif;base64,dXBwZXJjYXNlIGRhdGEgdXJpIHRlc3QK" "" mempty) let fooDir = absTmpdir </> "foo" runIOorExplode $ do fillMediaBag d@@ -42,6 +73,9 @@ (exists3 && not exists4) exists5 <- doesFileExist (fooDir </> "d5fceb6532643d0d84ffe09c40c481ecdf59e15a.gif") assertBool "data uri with gif is not properly decoded" exists5+ exists5a <- doesFileExist+ (fooDir </> "81c7546d23179ce1b344a763aa9038c3a8ff85d0.gif")+ assertBool "data uri with uppercase scheme is not extracted" exists5a -- double-encoded version: let e = B.doc $ B.para (B.image "data:image/png;base64,cHJpbnQgInB3bmVkIgo=;.lua+%252f%252e%252e%252f%252e%252e%252fb%252elua" "" mempty)
@@ -112,10 +112,18 @@ "docx/char_styles.docx" "docx/char_styles.native" , testCompare+ "double strikethrough"+ "docx/double_strikeout.docx"+ "docx/double_strikeout.native"+ , testCompare "hyperlinks" "docx/links.docx" "docx/links.native" , testCompare+ "hyperlinks with ScreenTips"+ "docx/link_tooltips.docx"+ "docx/link_tooltips.native"+ , testCompare "hyperlinks in <w:instrText> tag" "docx/instrText_hyperlink.docx" "docx/instrText_hyperlink.native"@@ -128,6 +136,18 @@ "docx/empty_field.docx" "docx/empty_field.native" , testCompare+ "field without <w:instrText> does not corrupt later lists"+ "docx/field_no_instrText.docx"+ "docx/field_no_instrText.native"+ , testCompare+ "field with <w:instrText> in tracked change does not corrupt later lists"+ "docx/field_tracked_instrText.docx"+ "docx/field_tracked_instrText.native"+ , testCompare+ "field without <w:instrText> in a code block keeps its text"+ "docx/field_no_instrText_codeblock.docx"+ "docx/field_no_instrText_codeblock.native"+ , testCompare "pageref hyperlinks in <w:instrText> tag" "docx/pageref.docx" "docx/pageref.native"@@ -200,6 +220,10 @@ "docx/nested_smart_tags.docx" "docx/nested_smart_tags.native" , testCompare+ "inline customXml elements"+ "docx/custom_xml.docx"+ "docx/custom_xml.native"+ , testCompare "remove anchor spans with nothing pointing to them" "docx/unused_anchors.docx" "docx/unused_anchors.native"@@ -421,8 +445,8 @@ "docx/track_changes_deletion_reject.native" , testCompareWithOpts def{readerTrackChanges=AllChanges} "keep insertion (all)"- "docx/track_changes_deletion.docx"- "docx/track_changes_deletion_all.native"+ "docx/track_changes_insertion.docx"+ "docx/track_changes_insertion_all.native" , testCompareWithOpts def{readerTrackChanges=AllChanges} "keep deletion (all)" "docx/track_changes_deletion.docx"@@ -439,6 +463,26 @@ "move text (all)" "docx/track_changes_move.docx" "docx/track_changes_move_all.native"+ , testCompareWithOpts def{readerTrackChanges=AllChanges}+ "spaces at the edges of changes stay in the span (all)"+ "docx/track_changes_edge_spaces.docx"+ "docx/track_changes_edge_spaces_all.native"+ , testCompareWithOpts def{readerTrackChanges=AcceptChanges}+ "spaces at the edges of changes (accept)"+ "docx/track_changes_edge_spaces.docx"+ "docx/track_changes_edge_spaces_accept.native"+ , testCompareWithOpts def{readerTrackChanges=RejectChanges}+ "spaces at the edges of changes (reject)"+ "docx/track_changes_edge_spaces.docx"+ "docx/track_changes_edge_spaces_reject.native"+ , testCompareWithOpts def{readerTrackChanges=AcceptChanges}+ "paragraph marks in notes stay in their notes (accept)"+ "docx/track_changes_paragraph_mark_in_note.docx"+ "docx/track_changes_paragraph_mark_in_note_accept.native"+ , testCompareWithOpts def{readerTrackChanges=RejectChanges}+ "paragraph marks in notes stay in their notes (reject)"+ "docx/track_changes_paragraph_mark_in_note.docx"+ "docx/track_changes_paragraph_mark_in_note_reject.native" , testCompareWithOpts def{readerTrackChanges=AcceptChanges} "comments (accept -- no comments)" "docx/comments.docx"
@@ -32,7 +32,48 @@ (=:) = test dokuwiki tests :: [TestTree]-tests = [ testGroup "inlines"+tests = [ testGroup "math groups and comments"+ [ testGroup name+ [ test reader "nested math in a color box" $+ wrap "\\colorbox{aqua}{$F=ma$}" =?>+ para (makeMath "\\colorbox{aqua}{$F=ma$}")+ , test reader "unpaired escaped opening brace" $+ wrap "{\\{x}" =?> para (makeMath "{\\{x}")+ , test reader "unpaired escaped closing brace" $+ wrap escapedClosing =?> para (makeMath escapedClosing)+ , test reader "commented delimiters and braces" $+ wrap commented =?> para (makeMath commented)+ , test reader "commented closing brace inside a group" $+ wrap ("{x% } " <> close <> "\ny}") =?>+ para (makeMath $ "{x% } " <> close <> "\ny}")+ , test reader "commented opening brace inside a group" $+ wrap "{x% {\ny}" =?> para (makeMath "{x% {\ny}")+ , test reader "escaped percent is literal" $+ wrap "x\\%" =?> para (makeMath "x\\%")+ , test reader "percent after an escaped backslash is a comment" $+ wrap ("x\\\\% " <> close <> "\ny") =?>+ para (makeMath $ "x\\\\% " <> close <> "\ny")+ ]+ | (name, ext, open, close, makeMath) <-+ [ ("dollars", Ext_tex_math_dollars, "$", "$", math)+ , ("double dollars", Ext_tex_math_dollars,+ "$$", "$$", displayMath)+ , ("parentheses", Ext_tex_math_single_backslash,+ "\\(", "\\)", math)+ , ("brackets", Ext_tex_math_single_backslash,+ "\\[", "\\]", displayMath)+ , ("double-backslash parentheses", Ext_tex_math_double_backslash,+ "\\\\(", "\\\\)", math)+ , ("double-backslash brackets", Ext_tex_math_double_backslash,+ "\\\\[", "\\\\]", displayMath)+ ]+ , let reader = purely $ readDokuWiki def+ { readerExtensions = extensionsFromList [ext] }+ wrap s = open <> s <> close+ escapedClosing = "{\\} hi " <> open <> "x" <> close <> " bye}"+ commented = "x% " <> close <> " { } \\%\ny"+ ]+ , testGroup "inlines" [ "Bold" =: "**bold**" =?> para (strong "bold")
@@ -57,7 +57,48 @@ { writerWrapText = WrapPreserve }) tests :: [TestTree]-tests = [ testGroup "base tag"+tests = [ testGroup "math groups and comments"+ [ testGroup name+ [ test reader "nested math in a color box" $+ wrap "\\colorbox{aqua}{$F=ma$}" =?>+ para (makeMath "\\colorbox{aqua}{$F=ma$}")+ , test reader "unpaired escaped opening brace" $+ wrap "{\\{x}" =?> para (makeMath "{\\{x}")+ , test reader "unpaired escaped closing brace" $+ wrap escapedClosing =?> para (makeMath escapedClosing)+ , test reader "commented delimiters and braces" $+ wrap commented =?> para (makeMath commented)+ , test reader "commented closing brace inside a group" $+ wrap ("{x% } " <> close <> "\ny}") =?>+ para (makeMath $ "{x% } " <> close <> "\ny}")+ , test reader "commented opening brace inside a group" $+ wrap "{x% {\ny}" =?> para (makeMath "{x% {\ny}")+ , test reader "escaped percent is literal" $+ wrap "x\\%" =?> para (makeMath "x\\%")+ , test reader "percent after an escaped backslash is a comment" $+ wrap ("x\\\\% " <> close <> "\ny") =?>+ para (makeMath $ "x\\\\% " <> close <> "\ny")+ ]+ | (name, ext, open, close, makeMath) <-+ [ ("dollars", Ext_tex_math_dollars, "$", "$", math)+ , ("double dollars", Ext_tex_math_dollars,+ "$$", "$$", displayMath)+ , ("parentheses", Ext_tex_math_single_backslash,+ "\\(", "\\)", math)+ , ("brackets", Ext_tex_math_single_backslash,+ "\\[", "\\]", displayMath)+ , ("double-backslash parentheses", Ext_tex_math_double_backslash,+ "\\\\(", "\\\\)", math)+ , ("double-backslash brackets", Ext_tex_math_double_backslash,+ "\\\\[", "\\\\]", displayMath)+ ]+ , let reader = purely $ readHtml def+ { readerExtensions = extensionsFromList [ext] }+ wrap s = "<p>" <> open <> s <> close <> "</p>"+ escapedClosing = "{\\} hi " <> open <> "x" <> close <> " bye}"+ commented = "x% " <> close <> " { } \\%\ny"+ ]+ , testGroup "base tag" [ test html "simple" $ "<head><base href=\"http://www.w3schools.com/images/foo\" ></head><body><img src=\"stickman.gif\" alt=\"Stickman\"></head>" =?> plain (image "http://www.w3schools.com/images/stickman.gif" "" (text "Stickman"))
@@ -250,6 +250,51 @@ mconcat ["^^" <> T.pack [i] | i <- hex] =?> para (str $ T.pack $ ['p'..'y']++['!'..'&']) ]+ , testGroup "symbol commands"+ [ "qed" =:+ "A\\qed" =?> para (str "A\xa0\x25FB")+ ]+ , testGroup "newif"+ [ "newif defines conditional" =:+ "\\newif\\iffoo\\footrue\\iffoo yes\\fi" =?>+ para (str "yes")+ , "newif requires name starting with if" =:+ "\\newif\\foobar\\foobar hi\\fi" =?>+ para (str "hi")+ ]+ , testGroup "conditionals"+ [ "unknown conditional nested in skipped branch" =:+ "\\iffalse X\\ifdim\\wd0>0pt Y\\fi Z\\fi W" =?>+ para "W"+ , "nested conditionals with else branches" =:+ "\\iftrue A\\iffalse B\\else C\\fi D\\else E\\fi F" =?>+ para "ACDF"+ , "newif conditional nested in skipped branch" =:+ "\\newif\\iffoo\\iffalse x\\iffoo y\\fi z\\fi w" =?>+ para "w"+ , "nested conditional in else branch" =:+ "\\iffalse a\\else b\\iffalse c\\else d\\fi e\\fi f" =?>+ para "bdef"+ ]+ , testGroup "ligatures"+ [ "quote ligatures in normal text" =:+ "it's ``x'' `y'" =?>+ para ("it\8217s " <> doubleQuoted "x" <> " " <> singleQuoted "y")+ , "apostrophe in texttt stays ASCII" =:+ "\\texttt{it's}" =?> para (code "it's")+ , "backtick in texttt stays ASCII" =:+ "\\texttt{`x'}" =?> para (code "`x'")+ ]+ , testGroup "urls"+ [ "url with escaped %" =:+ "\\url{http://example.com/a\\%b}" =?>+ para (linkWith ("",["uri"],[]) "http://example.com/a%b" ""+ (str "http://example.com/a%b"))+ , "url with escaped backslash" =:+ "\\url{http://example.com/a\\\\b}" =?>+ para (linkWith ("",["uri"],[]) "http://example.com/a\\b" ""+ (str "http://example.com/a\\b"))+ ] , testGroup "memoir scene breaks" [ "plainbreak" =: "hello\\plainbreak{2}goodbye" =?>
@@ -162,7 +162,91 @@ -} tests :: [TestTree]-tests = [ testGroup "inline code"+tests = [ testGroup "math groups"+ [ testGroup name+ [ test reader "nested math in text" $+ wrap "\\text{hi $x$ bye}" =?>+ para (makeMath "\\text{hi $x$ bye}")+ , test reader "nested math in a color box" $+ wrap "\\colorbox{aqua}{$F=ma$}" =?>+ para (makeMath "\\colorbox{aqua}{$F=ma$}")+ , test reader "nested groups" $+ wrap "\\fcolorbox{red}{aqua}{\\text{hi $x$ bye}}" =?>+ para (makeMath+ "\\fcolorbox{red}{aqua}{\\text{hi $x$ bye}}")+ , test reader "matching delimiters inside a group" $+ wrap grouped =?> para (makeMath grouped)+ , test reader "unpaired escaped opening brace inside a group" $+ wrap "{\\{x}" =?> para (makeMath "{\\{x}")+ , test reader "unpaired escaped closing brace inside a group" $+ wrap escapedClosing =?> para (makeMath escapedClosing)+ , test reader "unpaired escaped opening brace outside a group" $+ wrap "\\{x" =?> para (makeMath "\\{x")+ , test reader "unpaired escaped closing brace outside a group" $+ wrap "x\\}" =?> para (makeMath "x\\}")+ , test reader "escaped backslash before closing brace" $+ wrap "{x\\\\}" =?> para (makeMath "{x\\\\}")+ , test reader "multiline group" $+ wrap "{a\nb}" =?> para (makeMath "{a\nb}")+ , test reader "commented delimiters and braces" $+ wrap commented =?> para (makeMath commented)+ , test reader "commented closing brace inside a group" $+ wrap ("{x% } " <> close <> "\ny}") =?>+ para (makeMath $ "{x% } " <> close <> "\ny}")+ , test reader "commented opening brace inside a group" $+ wrap "{x% {\ny}" =?> para (makeMath "{x% {\ny}")+ , test reader "escaped percent is literal" $+ wrap "x\\%" =?> para (makeMath "x\\%")+ , test reader "escaped percent does not hide a closing brace" $+ wrap "{x\\%}" =?> para (makeMath "{x\\%}")+ , test reader "escaped percent does not hide an opening brace" $+ wrap ("x\\%" <> grouped) =?>+ para (makeMath $ "x\\%" <> grouped)+ , test reader "percent after an escaped backslash starts a comment" $+ wrap ("x\\\\% " <> close <> "\ny") =?>+ para (makeMath $ "x\\\\% " <> close <> "\ny")+ , test reader "escaped backslash before a comment inside a group" $+ wrap "{x\\\\% }\ny}" =?>+ para (makeMath "{x\\\\% }\ny}")+ , test reader "comment cannot supply the closing delimiter" $+ wrap "x%" =?>+ para (text $ literalOpen <> "x%" <> literalClose)+ , test reader "consecutive comments" $+ wrap ("x%\n% } $ \\" <> "\ny") =?>+ para (makeMath $ "x%\n% } $ \\" <> "\ny")+ , test reader "following math remains separate" $+ (wrap grouped <> " and " <> wrap "y") =?>+ para (makeMath grouped <> " and " <> makeMath "y")+ , test reader "unclosed group falls back to text" $+ wrap "{x" =?> para (text $ literalOpen <> "{x" <>+ literalClose)+ , test reader "unclosed group does not hide later math" $+ (wrap "{x" <> "\n\n" <> wrap "y") =?>+ (para (text $ literalOpen <> "{x" <> literalClose) <>+ para (makeMath "y"))+ ]+ | (name, ext, open, close, literalOpen, literalClose, makeMath) <-+ [ ("dollars", Ext_tex_math_dollars,+ "$", "$", "$", "$", math)+ , ("double dollars", Ext_tex_math_dollars,+ "$$", "$$", "$$", "$$", displayMath)+ , ("parentheses", Ext_tex_math_single_backslash,+ "\\(", "\\)", "(", ")", math)+ , ("brackets", Ext_tex_math_single_backslash,+ "\\[", "\\]", "[", "]", displayMath)+ , ("double-backslash parentheses", Ext_tex_math_double_backslash,+ "\\\\(", "\\\\)", "\\(", "\\)", math)+ , ("double-backslash brackets", Ext_tex_math_double_backslash,+ "\\\\[", "\\\\]", "\\[", "\\]", displayMath)+ ]+ , let reader = purely $ readMarkdown def+ { readerExtensions = extensionsFromList [ext] }+ wrap s = open <> s <> close+ grouped = "\\text{hi " <> open <> "x" <> close <> " bye}"+ escapedClosing = "{\\} hi " <> open <> "x" <> close <> " bye}"+ commented = "x% " <> close <> " { } \\%\ny"+ ]+ , testGroup "inline code" [ "with attribute" =: "`document.write(\"Hello\");`{.javascript}" =?> para
@@ -28,7 +28,22 @@ defopts = def{ readerExtensions = getDefaultExtensions "odt" } tests :: [TestTree]-tests = testsComparingToMarkdown ++ testsComparingToNative+tests = testsComparingToMarkdown ++ testsComparingToNative +++ [ testGroup "fodt" (fodtTestsComparingToMarkdown +++ fodtTestsComparingToNative +++ fodtTestsComparingToOwnNative)+ , testGroup "styles extension"+ [ unsafePerformIO $ liftM (test id "enabled")+ (compareFODTToNative stylesopts "odt/fodt/styles.fodt"+ "odt/fodt-native/styles.native")+ , unsafePerformIO $ liftM (test id "disabled")+ (compareFODTToNative defopts "odt/fodt/styles.fodt"+ "odt/fodt-native/styles-off.native")+ ]+ ]+ where+ stylesopts = defopts{ readerExtensions = enableExtension Ext_styles+ (readerExtensions defopts) } testsComparingToMarkdown :: [TestTree] testsComparingToMarkdown = map nameToTest namesOfTestsComparingToMarkdown@@ -50,7 +65,73 @@ toODTPath name = "odt/odt/" ++ name ++ ".odt" toNativePath name = "odt/native/" ++ name ++ ".native" +-- The flat files are LibreOffice's conversions of the zipped files, and are+-- compared against the very same expectations, so that flat and zipped input+-- are asserted to converge on identical documents. office:settings,+-- office:scripts and office:master-styles have been removed from them: they+-- are view state that the reader never looks at, and they were a quarter of+-- the bytes.+--+-- Four fixtures are enough, because a flat file shares nearly all of the+-- reader with a zipped one: once readFODT has handed the root element over,+-- the converter is the very same code. Only two paths are reached by a flat+-- file alone, and each has a fixture that reaches it:+--+-- * "image" -- an image with no xlink:href, so the data must be decoded+-- from office:binary-data and the type taken from+-- draw:mime-type rather than found in the archive+-- * "formula" -- a draw:object with no xlink:href, so the MathML is read+-- from the element itself rather than from a sub-document+--+-- The other two guard against whitespace, which is the one hazard that is+-- peculiar to this format without being a separate branch of the reader:+-- LibreOffice pretty-prints a flat file, and that indentation is in the XML+-- the reader sees. It has already been read as content once, as the alt text+-- of an image. "preformattedText" is a CodeBlock, whose whitespace is+-- significant and therefore compared exactly, and "textMixedStyles" is a run+-- of nested emphasis, where stray indentation would show up between inlines.+--+-- Three of the zipped fixtures *cannot* be used, rather than merely being+-- surplus: LibreOffice's flat export renames the automatic style that marks+-- inlined code (inlinedCode), drops the cached text of a chapter reference+-- field (referenceToChapter), and resolves a relative reference to a+-- non-embedded image against the local filesystem (imageRelative). +fodtTestsComparingToMarkdown :: [TestTree]+fodtTestsComparingToMarkdown = map nameToTest namesOfFODTTestsComparingToMarkdown+ where nameToTest name = createTest+ compareFODTToMarkdown+ name+ (toFODTPath name)+ (toMarkdownPath name)+ toFODTPath name = "odt/fodt/" ++ name ++ ".fodt"+ toMarkdownPath name = "odt/markdown/" ++ name ++ ".md"++fodtTestsComparingToNative :: [TestTree]+fodtTestsComparingToNative = map nameToTest namesOfFODTTestsComparingToNative+ where nameToTest name = createTest+ compareFODTToNative+ name+ (toFODTPath name)+ (toNativePath name)+ toFODTPath name = "odt/fodt/" ++ name ++ ".fodt"+ toNativePath name = "odt/native/" ++ name ++ ".native"++-- Images need their own expectations, because the media bag paths are+-- pandoc's own when the image data is embedded, and because LibreOffice+-- converts the frame dimensions to inches on export.++fodtTestsComparingToOwnNative :: [TestTree]+fodtTestsComparingToOwnNative = map nameToTest namesOfFODTTestsComparingToOwnNative+ where nameToTest name = createTest+ compareFODTToNative+ name+ (toFODTPath name)+ (toNativePath name)+ toFODTPath name = "odt/fodt/" ++ name ++ ".fodt"+ toNativePath name = "odt/fodt-native/" ++ name ++ ".native"++ newtype NoNormPandoc = NoNormPandoc {unNoNorm :: Pandoc} deriving ( Show ) @@ -91,7 +172,25 @@ odt <- getNoNormVia id "odt" <$> runIO (readODT opts odtFile) return (odt,markdown) +compareFODTToNative :: TestCreator+compareFODTToNative opts fodtPath nativePath = do+ nativeFile <- UTF8.toText <$> BS.readFile nativePath+ fodtFile <- UTF8.toText <$> BS.readFile fodtPath+ native <- getNoNormVia id "native" <$> runIO (readNative def nativeFile)+ fodt <- getNoNormVia id "fodt" <$> runIO (readFODT opts fodtFile)+ return (fodt,native) +compareFODTToMarkdown :: TestCreator+compareFODTToMarkdown opts fodtPath markdownPath = do+ markdownFile <- UTF8.toText <$> BS.readFile markdownPath+ fodtFile <- UTF8.toText <$> BS.readFile fodtPath+ markdown <- getNoNormVia id "markdown" <$>+ runIO (readMarkdown def{ readerExtensions = pandocExtensions }+ markdownFile)+ fodt <- getNoNormVia id "fodt" <$> runIO (readFODT opts fodtFile)+ return (fodt,markdown)++ createTest :: TestCreator -> TestName -> FilePath -> FilePath@@ -172,6 +271,7 @@ , "inlinedCode" , "listContinueNumbering" , "listContinueNumbering2"+ , "listContinueNumbering3" , "orderedListMixed" , "orderedListRoman" , "orderedListSimple"@@ -194,3 +294,14 @@ , "unorderedList" , "unorderedListHeader" ]++namesOfFODTTestsComparingToMarkdown :: [ String ]+namesOfFODTTestsComparingToMarkdown = [ "formula" ]++namesOfFODTTestsComparingToNative :: [ String ]+namesOfFODTTestsComparingToNative = [ "preformattedText"+ , "textMixedStyles"+ ]++namesOfFODTTestsComparingToOwnNative :: [ String ]+namesOfFODTTestsComparingToOwnNative = [ "image" ]
@@ -52,6 +52,18 @@ ] =?> para "a^b" + , "disable braced sub/superscript syntax" =:+ T.unlines [ "#+OPTIONS: ^:nil"+ , "a^{b} c_{d}"+ ] =?>+ para "a^{b} c_{d}"++ , "interpret only braced sub/superscripts" =:+ T.unlines [ "#+OPTIONS: ^:{}"+ , "a^b a^{b} a^(b)"+ ] =?>+ para ("a^b a" <> superscript "b" <> " a^(b)")+ , "directly select drawers to be exported" =: T.unlines [ "#+OPTIONS: d:(\"IMPORTANT\")" , ":IMPORTANT:"
@@ -112,6 +112,14 @@ "line \\\\ \nbreak" =?> para ("line" <> linebreak <> "break") + , "Bare backslash" =:+ "a \\ b" =?>+ para ("a" <> space <> "\\" <> space <> "b")++ , "Unclosed LaTeX environment" =:+ "\\begin{align} more text" =?>+ para ("\\begin{align}" <> space <> "more" <> space <> "text")+ , "Inline note" =: "[fn::Schreib mir eine E-Mail]" =?> para (note $ para "Schreib mir eine E-Mail")
@@ -82,6 +82,13 @@ , headerWith ("headline", [], []) 2 "Headline" ] + , "Inline note with hyphen and underscore in label" =:+ "Some text[fn:my_note-1: the note]" =?>+ para (mconcat+ [ "Some", space, "text"+ , note . para $ "the" <> space <> "note"+ ])+ , "Footnote followed by two blank lines" =: T.unlines [ "footnote[fn:blanklines]" , ""
@@ -41,8 +41,62 @@ tests :: [TestTree] tests = [ testGroup "basic" [ testCompare+ "charts"+ "pptx-reader/charts.pptx"+ "pptx-reader/charts.native"+ , testCompare+ "run formatting and hyperlinks"+ "pptx-reader/formatting.pptx"+ "pptx-reader/formatting.native"+ , testCompare+ "group shapes"+ "pptx-reader/groups.pptx"+ "pptx-reader/groups.native"+ , testCompare+ "slide number placeholders"+ "pptx-reader/slide-numbers.pptx"+ "pptx-reader/slide-numbers.native"+ , testCompare "text extraction" "pptx-reader/basic.pptx" "pptx-reader/basic.native"+ , testCompare+ "text runs and line breaks"+ "pptx-reader/runs.pptx"+ "pptx-reader/runs.native"+ ]+ , testGroup "slides"+ [ testCompare+ "hidden slides"+ "pptx-reader/hidden.pptx"+ "pptx-reader/hidden.native"+ ]+ , testGroup "tables"+ [ testCompare+ "merged cells"+ "pptx-reader/table-spans.pptx"+ "pptx-reader/table-spans.native"+ ]+ , testGroup "markup compatibility"+ [ testCompare+ "equations and alternate content"+ "pptx-reader/equations.pptx"+ "pptx-reader/equations.native"+ ]+ , testGroup "lists"+ [ testCompare+ "inherited bullets, nesting and numbering"+ "pptx-reader/lists.pptx"+ "pptx-reader/lists.native"+ , testCompare+ "lists written by the pptx writer"+ "pptx-reader/lists-writer.pptx"+ "pptx-reader/lists-writer.native"+ ]+ , testGroup "notes"+ [ testCompare+ "speaker notes"+ "pptx-reader/notes.pptx"+ "pptx-reader/notes.native" ] ]
@@ -217,6 +217,19 @@ , "bare URI parsing disabled inside emphasis (#4561)" =: "*http://location*" =?> para (emph (text "http://location"))+ , "bare URI and email address in angle brackets" =:+ "See <https://example.com> or <jane-doe@example.com>." =?>+ para ("See <" <> link "https://example.com" "" "https://example.com" <>+ "> or <" <>+ link "mailto:jane-doe@example.com" "" "jane-doe@example.com" <>+ ">.")+ , "inline markup after a colon, a slash or a quote" =:+ "key:*a* path/*b* \"*c*\"" =?>+ para ("key:" <> emph "a" <> " path/" <> emph "b" <> " \"" <>+ emph "c" <> "\"")+ , "quoted start-strings are not inline markup" =:+ "(*) and (*), [*] and [*], \"*\" or \"x*\"" =?>+ para (text "(*) and (*), [*] and [*], \"*\" or \"x*\"") , "include newlines" =: "**before\nafter**" =?> para (strong (text "before\nafter"))
@@ -41,8 +41,48 @@ tests :: [TestTree] tests = [ testGroup "basic" [ testCompare+ "cell fonts"+ "xlsx-reader/cell-fonts.xlsx"+ "xlsx-reader/cell-fonts.native"+ , testCompare+ "inline and rich-text strings"+ "xlsx-reader/strings.xlsx"+ "xlsx-reader/strings.native"+ , testCompare "sheet extraction" "xlsx-reader/basic.xlsx" "xlsx-reader/basic.native"+ , testCompare+ "package-absolute relationship targets"+ "xlsx-reader/absolute-targets.xlsx"+ "xlsx-reader/absolute-targets.native"+ ]+ , testGroup "values"+ [ testCompare+ "numbers, booleans and errors"+ "xlsx-reader/numbers.xlsx"+ "xlsx-reader/numbers.native"+ ]+ , testGroup "dates"+ [ testCompare+ "date and time formats"+ "xlsx-reader/dates.xlsx"+ "xlsx-reader/dates.native"+ , testCompare+ "1904 date system"+ "xlsx-reader/dates-1904.xlsx"+ "xlsx-reader/dates-1904.native"+ ]+ , testGroup "merged cells"+ [ testCompare+ "merged ranges as spans"+ "xlsx-reader/merged.xlsx"+ "xlsx-reader/merged.native"+ ]+ , testGroup "charts"+ [ testCompare+ "charts on a sheet and on a chart sheet"+ "xlsx-reader/charts.xlsx"+ "xlsx-reader/charts.native" ] ]
@@ -34,6 +34,10 @@ , testGroup "collapseFilePath" testCollapse , testGroup "toLegacyTable" testLegacyTable , testGroup "table of contents" testTOC+ , testGroup "stringifyInlines"+ [ testProperty "stringifyInlines matches stringify"+ stringifyInlinesMatchesStringify+ ] , testGroup "makeSections" [ testProperty "makeSections is idempotent" makeSectionsIsIdempotent , testCase "makeSections is idempotent for test case" $@@ -44,6 +48,10 @@ (makeSections False Nothing d' == d') ] ]++stringifyInlinesMatchesStringify :: [Inline] -> Bool+stringifyInlinesMatchesStringify ils =+ stringifyInlines ils == stringify ils makeSectionsIsIdempotent :: [Block] -> Bool makeSectionsIsIdempotent d =
@@ -64,6 +64,22 @@ def "docx/links.native" "docx/golden/links.docx"+ , testCase "link titles become ScreenTips (#11869)" $ do+ doc <- documentXml def $ Pandoc mempty+ [ Para [ Link ("", [], []) [Str "external"]+ ("https://example.org/r.pdf", "Annual report")+ , Space+ , Link ("", [], []) [Str "internal"]+ ("#methods", "Jump to Methods")+ , Space+ , Link ("", [], []) [Str "untitled"]+ ("https://example.org", "")+ ]+ , Header 1 ("methods", [], []) [Str "Methods"]+ ]+ map (findAttr (wmlName "tooltip"))+ (findElements (wmlName "hyperlink") doc)+ @?= [Just "Annual report", Just "Jump to Methods", Nothing] , docxTest "inline image" def{ writerExtensions =@@ -108,6 +124,11 @@ "docx/lists.native" "docx/golden/lists.docx" , docxTest+ "CSL bibliography (hanging indent and spacing)"+ def+ "docx/csl_bibliography.native"+ "docx/golden/csl_bibliography.docx"+ , docxTest "lists continuing after interruption" def "docx/lists_continuing.native"@@ -197,6 +218,11 @@ def "docx/raw-bookmarks.native" "docx/golden/raw-bookmarks.docx"+ , docxTest+ "mark math"+ def+ "docx/mark_math.native"+ "docx/golden/mark_math.docx" ] , testGroup "track changes" [ docxTest
@@ -70,7 +70,7 @@ "\\begin{description}\n\\item[foo] ~ \n\\subsection{bar}\n\nbaz\n\\end{description}" , "containing image" =: header 1 (image "imgs/foo.jpg" "" (text "Alt text")) =?>- "\\section{\\texorpdfstring{\\protect\\pandocbounded{\\includegraphics[keepaspectratio,alt={Alt text}]{imgs/foo.jpg}}}{Alt text}}"+ "\\section{\\texorpdfstring{\\protect\\pandocbounded{\\includegraphics[keepaspectratio,alt={Alt\ntext}]{imgs/foo.jpg}}}{Alt text}}" ] , testGroup "inline code" [ "struck out and highlighted" =:@@ -249,7 +249,7 @@ (plain $ image (pack "img.jpg") (pack "") (text "alt text")) =?> "\\begin{figure}[htbp]\n\\centering\n"- <> "\\pandocbounded{\\includegraphics[keepaspectratio,alt={alt text}]{img.jpg}}\n"+ <> "\\pandocbounded{\\includegraphics[keepaspectratio,alt={alt\ntext}]{img.jpg}}\n" <> "\\caption{caption}\n\\end{figure}" ] ]
@@ -49,6 +49,26 @@ , "emph/strong with spaces (#10696)" =: emph (str "f" <> strong (space <> str "d" <> space)) <> str "l" =?> "*f **d*** l"+ , "image destination with space"+ =: image "home/image folder/image.png" "title" "pic"+ =?> ""+ , "link destination with space and pointy brackets"+ =: link "a b<c>.pdf" "" "link"+ =?> "[link](<a b\\<c\\>.pdf>)"+ , "bullet marker after linebreak in list item"+ =: orderedList [ plain (str "Lead:" <> linebreak <>+ str "-" <> space <> str "alpha") ]+ =?> "1. Lead:\\\n \\- alpha\n"+ , "ordered marker after linebreak in list item"+ =: orderedList [ plain (str "Lead:" <> linebreak <>+ str "3." <> space <> str "alpha") ]+ =?> "1. Lead:\\\n 3\\. alpha\n"+ , "lone bullet marker after linebreak"+ =: plain (str "Lead:" <> linebreak <> str "-")+ =?> "Lead:\\\n\\-"+ , "lone ordered marker after linebreak in list item"+ =: orderedList [ plain (str "Lead:" <> linebreak <> str "3.") ]+ =?> "1. Lead:\\\n 3\\." ] ++ [noteTests] ++ [shortcutLinkRefsTests] {-@@ -227,6 +247,12 @@ , " [foo]: /url1" , " [1]: /url2" , " [2]: /url3"+ ]+ , "Reference link destination with space"+ =: para (link "my file.pdf" "" "link")+ =?> unlines [ "[link]"+ , ""+ , " [link]: <my file.pdf>" ] , "Reference link is followed by text in brackets" =: para (link "/url" "" "link" <> "[text in brackets]")
@@ -0,0 +1,54 @@+{-# LANGUAGE OverloadedStrings #-}+{- |+ Module : Tests.Writers.ODT+ Copyright : © 2026 John MacFarlane+ License : GNU GPL, version 2 or above++ Maintainer : John MacFarlane <jgm@berkeley.edu>+ Stability : alpha+ Portability : portable++Tests for the ODT writers.+-}+module Tests.Writers.ODT (tests) where++import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import Test.Tasty+import Test.Tasty.HUnit+import Text.Pandoc+import Text.Pandoc.Builder+import qualified Text.Pandoc.XML.Light as XML++-- | The @xlink:href@ of every image in a flat OpenDocument file. The+-- local name is enough to match on, since the prefix is an artifact of+-- the namespace declarations we happen to write.+imageHrefs :: Text -> IO [Text]+imageHrefs txt =+ case XML.parseXMLElement (TL.fromStrict txt) of+ Left msg -> assertFailure $+ "flat OpenDocument output is not valid XML: "+ ++ T.unpack msg+ Right root -> return+ [ href+ | el <- XML.filterElementsName ((== "image") . XML.qName) root+ , Just href <- [XML.findAttrBy ((== "href") . XML.qName) el]+ ]++tests :: [TestTree]+tests =+ -- This cannot be a command test, because extracting the reference+ -- from the output needs a pipe into grep, and the command tests run+ -- under cmd.exe on Windows.+ [ testCase "fodt --link-images leaves a relative reference alone" $ do+ -- An ODT resolves relative references against its zip structure,+ -- so the writer prefixes them with "../" (see fixInternalLinks).+ -- A flat file is a single file, and must not have one.+ fodt <- runIOorExplode $ setVerbosity ERROR >>+ writeFODT def{ writerLinkImages = True } doc'+ hrefs <- imageHrefs fodt+ ["lalune.jpg"] @=? hrefs+ ]+ where+ doc' = doc $ para $ image "lalune.jpg" "" mempty
@@ -34,6 +34,13 @@ │ │ │ Nested block quote +Numbered list item beginning with a block quote (regression test for+issue #11804, where the leading bar was dropped on the block quote’s+first line because it shared a line with the list marker):++1. + │ List item block quote+ Multiline table with caption: Centered Left Right Default aligned
@@ -38,6 +38,12 @@ > > > Nested block quote +Numbered list item beginning with a block quote (regression test for+issue #11804, where the leading bar was dropped on the block quote's+first line because it shared a line with the list marker):++1. > List item block quote+ Multiline table with caption: : Here's the caption.
@@ -256,3855 +256,3837 @@ ] , Para [ Image- ( "" , [ "red icon" ] , [] )- []- ( "./images/icons/heart.png" , "" )- ]- , Para [ Str "anchor:tiger" ]- , Para [ Strong [ Str "*bold*" ] ]- , Para- [ Link- ( "" , [] , [] )- [ Str "Get" , Space , Str "Report" ]- ( "downloads/report.pdf" , "" )- ]- , Para- [ Link- ( "" , [] , [] )- [ Str "tools.html#editors" ]- ( "tools.html#editors" , "" )- ]- , Para- [ Link- ( "" , [] , [] )- [ Str "Your" , Space , Str "files" ]- ( "file:///home/username" , "" )- ]- , Para [ Str "Tricky" , Space , Str "cases:" ]- , Para- [ Link- ( "" , [] , [] )- [ Str "Get" , Space , Str "Report" ]- ( "My Documents/report.pdf" , "" )- ]- , Para- [ Link- ( "" , [] , [] )- [ Str "Get" , Space , Str "Report" ]- ( "My Documents/report.pdf" , "" )- ]- , Para- [ Link- ( "" , [] , [] )- [ Str "Get" , Space , Str "Report" ]- ( "My%20Documents/report.pdf" , "" )- ]- , Para- [ Link- ( "" , [] , [] )- [ Str "https://example.org/now_this__link_works.html" ]- ( "https://example.org/now_this__link_works.html" , "" )- ]- , Para- [ Link- ( "" , [] , [] )- [ Str "Subscribe" ]- ( "join@discuss.example.org" , "" )- ]- , Para- [ Link- ( "" , [ "mail" ] , [] )- [ Str "Click,"- , Space- , Str "subscribe,"- , Space- , Str "and"- , Space- , Str "participate!"- ]- ( "join@discuss.example.org" , "" )- ]- , Para- [ Link- ( "" , [ "cross-reference" ] , [] )- [ Str "use"- , Space- , Str "attributes"- , Space- , Str "within"- , Space- , Str "the"- , Space- , Str "link"- , Space- , Str "macro"- ]- ( "#link-macro-attributes" , "" )- ]- , Figure- ( "" , [] , [] )- (Caption Nothing [])- [ Plain- [ Image- ( "" , [] , [] ) [ Str "Sunset" ] ( "sunset.jpg" , "" )- ]- ]- , Figure- ( "" , [] , [] )- (Caption Nothing [])- [ Plain [ Image ( "" , [] , [] ) [] ( "name.png" , "" ) ] ]- , Figure- ( "" , [] , [] )- (Caption Nothing [])- [ Plain- [ Image- ( ""- , []- , [ ( "width" , "300px" ) , ( "height" , "400px" ) ]- )- [ Str "Sunset" ]- ( "sunset.jpg" , "" )- ]- ]- , Div- ( ""- , []- , [ ( "wrapper" , "1" )- , ( "alt" , "Sunset" )- , ( "height" , "400" )- , ( "width" , "300" )- ]- )- [ Figure- ( "" , [] , [] )- (Caption Nothing [])- [ Plain [ Image ( "" , [] , [] ) [] ( "sunset.jpg" , "" ) ]- ]- ]- , Para [ Math DisplayMath "e=mc^2\n" ]- , Para [ Math DisplayMath "sin n / 3\n" ]- , Para [ Math DisplayMath "e^i\n" ]- , Header- 2- ( "_attribute_substitutions" , [] , [] )- [ Str "Attribute" , Space , Str "substitutions" ]- , Para [ Str "Foo" , Space , Str "bar" , Space , Str "baz" ]- , Para [ Str "{nonexistent}" ]- , Para- [ Str "Built"- , Space- , Str "in:"- , Space- , Str "xyz"- , Space- , Str "a\160b\8203c'd\8216"- ]- , Header- 2- ( "_bold_and_italic" , [] , [] )- [ Str "Bold" , Space , Str "and" , Space , Str "italic" ]- , Para- [ Str "Constrained:"- , Space- , Strong- [ Str "this"- , Space- , Str "is"- , Space- , Str "bold"- , Space- , Emph [ Str "and" , Space , Str "italic" ]- ]- , Str "."- ]- , Para- [ Str "Unconstrained:"- , Space- , Str "wild"- , Strong- [ Str "content"- , Emph [ Str "with" , Space , Str "italic" ]- , Str "stuff"- ]- , Str "."- ]- , Header 2 ( "_monospace" , [] , [] ) [ Str "Monospace" ]- , Para [ Code ( "" , [] , [] ) "simple" ]- , Para- [ Code ( "" , [] , [] ) "complex"- , Space- , Strong- [ Code ( "" , [] , [] ) "with"- , Space- , Code ( "" , [] , [] ) "bold"- ]- , Space- , Code ( "" , [] , [] ) "text"- , Space- , Code ( "" , [] , [] ) "and"- , Space- , Code ( "" , [] , [] ) "a"- , Space- , Link- ( "" , [] , [] )- [ Code ( "" , [] , [] ) "foo.html" ]- ( "foo.html" , "" )- ]- , Para- [ Str "unconstrained"- , Code ( "" , [] , [] ) "wwow"- , Str "okay"- ]- , Header- 2- ( "_span_and_inline_attributes" , [] , [] )- [ Str "Span"- , Space- , Str "and"- , Space- , Str "inline"- , Space- , Str "attributes"- ]- , Para- [ Span- ( "" , [ "red" ] , [] )- [ Str "Bonjour" , Space , Strong [ Str "monsieur" ] ]- ]- , Para- [ Str "Un"- , Span ( "" , [ "red" ] , [] ) [ Str "constrained" ]- , Str "content"- ]- , Para- [ Str "With"- , Space- , Span- ( "" , [ "mark" ] , [] )- [ Str "no" , Space , Str "attribute" ]- , Space- , Str "it\8217s"- , Space- , Str "highlighted."- ]- , Header- 2- ( "_sub_and_superscript" , [] , [] )- [ Str "Sub"- , Space- , Str "and"- , Space- , Str "superscript"- ]- , Para [ Str "H" , Subscript [ Str "2" ] , Str "O" ]- , Para- [ Str "H"- , Subscript [ Str "a" , Space , Str "b" ]- , Str "O"- ]- , Para- [ Str "Not"- , Space- , Str "subscript:"- , Space- , Str "H~a"- , Space- , Str "b~O."- ]- , Para [ Str "H^2&O" ]- , Para- [ Str "H"- , Superscript [ Str "a" , Space , Str "b" ]- , Str "O"- ]- , Para- [ Str "Not"- , Space- , Str "subscript:"- , Space- , Str "H^a"- , Space- , Str "b^O."- ]- , Header- 2 ( "_passthrough" , [] , [] ) [ Str "Passthrough" ]- , Para- [ Str "Here"- , Space- , Str "the"- , Space- , Str "special"- , Space- , Str "characters"- , Space- , Str "just"- , Space- , Str "come"- , Space- , Str "through"- , Space- , Str "as"- , Space- , Str "literal:"- ]- , Para [ Str "<b>*test*</b>" ]- , Para [ Str "xx<b>*test*</b>xx" ]- , Para- [ Str "But"- , Space- , Str "here"- , Space- , Str "they"- , Space- , Str "are"- , Space- , Str "passed"- , Space- , Str "through:"- ]- , Para [ Str "xx" , Strong [ Str "*test*" ] , Str "xx" ]- , Header 2 ( "_quoted" , [] , [] ) [ Str "Quoted" ]- , Para- [ Quoted DoubleQuote [ Str "double" , Space , Str "quoted" ]- ]- , Para- [ Quoted SingleQuote [ Str "single" , Space , Str "quoted" ]- ]- , Header 2 ( "_footnotes" , [] , [] ) [ Str "Footnotes" ]- , Para- [ Str "Double"- , Note- [ Para- [ Str "The"- , Space- , Str "double"- , Space- , Str "hail-and-rainbow"- , Space- , Str "level"- , Space- , Str "makes"- , Space- , Str "my"- , Space- , Str "toes"- , Space- , Str "tingle."- ]- ]- ]- , Para- [ Str "A"- , Space- , Str "bold"- , Space- , Str "statement!"- , Note- [ Para- [ Str "Opinions"- , Space- , Str "are"- , Space- , Str "my"- , Space- , Str "own."- ]- ]- , SoftBreak- , Str "Another"- , Space- , Str "outrageous"- , Space- , Str "statement."- , Note- [ Para- [ Str "Opinions"- , Space- , Str "are"- , Space- , Str "my"- , Space- , Str "own."- ]- ]- ]- , Header- 1- ( "_block_markup" , [] , [] )- [ Str "Block" , Space , Str "markup" ]- , Header 2 ( "_sections" , [] , [] ) [ Str "Sections" ]- , Header- 3- ( "_another_level" , [] , [] )- [ Str "Another" , Space , Str "level" ]- , Header- 4 ( "_level_5" , [] , [] ) [ Str "Level" , Space , Str "5" ]- , Header- 3- ( "_markdown_style" , [] , [] )- [ Str "Markdown" , Space , Str "style" ]- , Header- 4- ( "_level_5_2" , [] , [] )- [ Str "Level" , Space , Str "5" ]- , Header- 2- ( "_discrete_heading" , [] , [] )- [ Str "Discrete" , Space , Str "heading" ]- , Header- 3- ( "" , [] , [] )- [ Str "A"- , Space- , Str "discrete"- , Space- , Str "heading,"- , Space- , Str "not"- , Space- , Str "a"- , Space- , Str "section"- ]- , Header 2 ( "_paragraph" , [] , [] ) [ Str "Paragraph" ]- , Para- [ Str "This"- , Space- , Str "is"- , Space- , Str "a"- , Space- , Str "paragraph"- , SoftBreak- , Str "whose"- , Space- , Str "source"- , Space- , Str "fits"- , Space- , Str "on"- , Space- , Str "two"- , Space- , Str "lines."- ]- , Para- [ Str "{.This"- , Space- , Str "is"- , Space- , Str "my"- , Space- , Str "title}"- , SoftBreak- , Str "A"- , Space- , Str "paragraph"- , Space- , Str "with"- , Space- , Str "a"- , Space- , Str "title."- ]- , Header- 2- ( "_example_block" , [] , [] )- [ Str "Example" , Space , Str "block" ]- , Div- ( "" , [] , [] )- [ Div- ( "" , [ "title" ] , [] )- [ Para [ Str "Optional" , Space , Str "title" ] ]- , Para- [ Str "This"- , Space- , Str "is"- , Space- , Str "an"- , Space- , Str "example"- , Space- , Str "of"- , Space- , Str "an"- , Space- , Str "example"- , Space- , Str "block."- ]- ]- , Div- ( "" , [ "example" ] , [] )- [ Div- ( "" , [ "title" ] , [] )- [ Para [ Str "Optional" , Space , Str "title" ] ]- , Para- [ Str "Paragraph" , Space , Strong [ Str "one" ] , Str "." ]- , Para- [ Str "Paragraph" , Space , Strong [ Str "two" ] , Str "." ]- ]- , Header 2 ( "_admonition" , [] , [] ) [ Str "Admonition" ]- , Para [ Str "Simple" , Space , Str "form:" ]- , Div- ( "" , [ "warning" ] , [] )- [ Div ( "" , [ "title" ] , [] ) [ Para [ Str "Warning" ] ]- , Para- [ Str "This"- , Space- , Str "is"- , Space- , Str "very"- , Space- , Str "dangerous."- , SoftBreak- , Str "Don\8217t"- , Space- , Str "do"- , Space- , Str "it"- , Space- , Str "unless"- , Space- , Str "you"- , Space- , Str "understand"- , Space- , Str "the"- , Space- , Str "risks."- ]- ]- , Div- ( "" , [ "important" ] , [] )- [ Div- ( "" , [ "title" ] , [] )- [ Para- [ Str "Title"- , Space- , Str "of"- , Space- , Str "the"- , Space- , Str "admonition"- ]- ]- , Para [ Str "Remember:" ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para- [ Str "Don\8217t"- , Space- , Str "do"- , Space- , Str "this."- ]- ]- , [ Para- [ Str "And"- , Space- , Str "don\8217t"- , Space- , Str "do"- , Space- , Str "that."- ]- ]- ]- ]- , Header 2 ( "_sidebar" , [] , [] ) [ Str "Sidebar" ]- , Para- [ Str "A" , Space , Str "simple" , Space , Str "sidebar." ]- , Div- ( "" , [ "sidebar" ] , [] )- [ Div- ( "" , [ "title" ] , [] )- [ Para- [ Str "Optional"- , Space- , Str "Title"- , Space- , Strong- [ Str "with"- , Space- , Str "strong"- , Space- , Str "emphasis"- ]- ]- ]- , Para- [ Str "Here"- , Space- , Str "is"- , Space- , Str "a"- , Space- , Str "sidebar."- ]- , Div- ( "" , [ "tip" ] , [] )- [ Div ( "" , [ "title" ] , [] ) [ Para [ Str "Tip" ] ]- , Para- [ Str "It"- , Space- , Str "can"- , Space- , Str "contain"- , Space- , Str "any"- , Space- , Str "type"- , Space- , Str "of"- , Space- , Str "content."- ]- ]- ]- , Header- 2- ( "_literal_block" , [] , [] )- [ Str "Literal" , Space , Str "block" ]- , Para- [ Str "Short"- , Space- , Str "indented"- , Space- , Str "code:"- ]- , CodeBlock- ( "" , [] , [] )- "$ ls -a\n$ cat /foo/bar/baz \\\n /bi/bim/bop\n"- , CodeBlock- ( "" , [] , [] ) "This is\n a literal block too.\n"- , CodeBlock- ( "" , [] , [] )- " Fenced\n $+ *a* literal\n\n****\nnot a sidebar\n****\n"- , Header 2 ( "_listing" , [] , [] ) [ Str "Listing" ]- , CodeBlock- ( "" , [ "ruby" ] , [] )- "require 'sinatra'\n\nget '/hi' do\n \"Hello World!\"\nend"- , Para [ Str "Implied:" ]- , CodeBlock- ( "" , [ "ruby" ] , [] )- "require 'sinatra'\n\nget '/hi' do\n \"Hello World!\"\nend"- , CodeBlock- ( "" , [ "ruby" ] , [] )- "# A function\ndef foo\n return 42\nend"- , CodeBlock- ( "hello" , [ "haskell" ] , [] )- "putStrLn $ unwords [\"Hello\", \"world\"]"- , Para [ Str "Line" , Space , Str "numbering:" ]- , CodeBlock- ( "" , [] , [ ( "options" , "linenums" ) ] )- "puts 1\nputs 2\nputs 3"- , CodeBlock- ( "" , [] , [] ) "This doesn't have a language.\n +=\"hi\""- , Para- [ Str "And"- , Space- , Str "with"- , Space- , Str "a"- , Space- , Str "callout"- , Space- , Str "list:"- ]- , CodeBlock- ( "" , [ "ruby" ] , [] )- "require 'sinatra' \9312\n\nget '/hi' do \9313 \9314\n \"Hello World!\"\nend"- , Div- ( "" , [ "callout-list" ] , [] )- [ OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Library" , Space , Str "import" ] ]- , [ Para [ Str "URL" , Space , Str "mapping" ] ]- , [ Para [ Str "Response" , Space , Str "block" ] ]- ]- ]- , Para [ Str "Markdown-style" , Space , Str "fenced:" ]- , CodeBlock- ( "" , [ "ruby" ] , [] ) "def foo\n return 5\nend"- , Header 2 ( "_verse" , [] , [] ) [ Str "Verse" ]- , BlockQuote- [ Para- [ Str "The"- , Space- , Str "fog"- , Space- , Str "comes"- , LineBreak- , Str "on"- , Space- , Str "little"- , Space- , Str "cat"- , Space- , Str "feet."- ]- , Para- [ Str "\8212"- , Space- , Str "Carl"- , Space- , Str "Sandburg,"- , Space- , Str "two"- , Space- , Str "lines"- , Space- , Str "from"- , Space- , Str "the"- , Space- , Str "poem"- , Space- , Str "Fog"- ]- ]- , BlockQuote- [ Para- [ Str "The"- , Space- , Str "fog"- , Space- , Str "comes"- , LineBreak- , Str "on"- , Space- , Str "little"- , Space- , Str "cat"- , Space- , Str "feet."- , LineBreak- , Str "It"- , Space- , Str "sits"- , Space- , Str "looking"- , LineBreak- , Str "over"- , Space- , Str "harbor"- , Space- , Str "and"- , Space- , Str "city"- , LineBreak- , Str "on"- , Space- , Str "silent"- , Space- , Str "haunches"- , LineBreak- , Str "and"- , Space- , Str "then"- , Space- , Str "moves"- , Space- , Str "on."- ]- , Para- [ Str "\8212"- , Space- , Str "Carl"- , Space- , Str "Sandburg,"- , Space- , Str "Fog"- ]- ]- , Header- 2 ( "_collapsible" , [] , [] ) [ Str "Collapsible" ]- , Para- [ Str "Click"- , Space- , Str "here"- , Space- , Str "for"- , Space- , Str "more."- ]- , Div- ( ""- , [ "example" ]- , [ ( "options" , "collapsible,open" ) ]- )- [ Para- [ Str "This"- , Space- , Str "is"- , Space- , Str "collapsible."- ]- , Para- [ Str "It"- , Space- , Str "can"- , Space- , Str "be"- , Space- , Str "hidden."- ]- ]- , Div- ( "" , [] , [ ( "options" , "collapsible" ) ] )- [ Div- ( "" , [ "title" ] , [] )- [ Para [ Str "Click" , Space , Str "me!" ] ]- , Para- [ Str "This"- , Space- , Str "paragraph"- , Space- , Str "is"- , SoftBreak- , Str "also"- , Space- , Str "collapsible."- ]- ]- , Header 2 ( "_quote" , [] , [] ) [ Str "Quote" ]- , BlockQuote- [ Para- [ Str "Everybody"- , Space- , Str "remember"- , Space- , Str "where"- , Space- , Str "we"- , Space- , Str "parked."- ]- , Para- [ Str "\8212"- , Space- , Str "Captain"- , Space- , Str "James"- , Space- , Str "T."- , Space- , Str "Kirk,"- , Space- , Str "Star"- , Space- , Str "Trek"- , Space- , Str "IV:"- , Space- , Str "The"- , Space- , Str "Voyage"- , Space- , Str "Home"- ]- ]- , BlockQuote- [ Para- [ Str "Dennis:"- , Space- , Str "Come"- , Space- , Str "and"- , Space- , Str "see"- , Space- , Str "the"- , Space- , Str "violence"- , Space- , Str "inherent"- , Space- , Str "in"- , Space- , Str "the"- , Space- , Str "system."- , Space- , Str "Help!"- , Space- , Str "Help!"- , Space- , Str "I\8217m"- , Space- , Str "being"- , SoftBreak- , Str "repressed."- ]- , Para- [ Str "King"- , Space- , Str "Arthur:"- , Space- , Str "Bloody"- , Space- , Str "peasant!"- ]- , Para- [ Str "Dennis:"- , Space- , Str "Oh,"- , Space- , Str "what"- , Space- , Str "a"- , Space- , Str "giveaway!"- , Space- , Str "Did"- , Space- , Str "you"- , Space- , Str "hear"- , Space- , Str "that?"- , Space- , Str "Did"- , Space- , Str "you"- , Space- , Str "hear"- , Space- , Str "that,"- , Space- , Str "eh?"- , Space- , Str "That\8217s"- , Space- , Str "what"- , Space- , Str "I\8217m"- , SoftBreak- , Str "on"- , Space- , Str "about!"- , Space- , Str "Did"- , Space- , Str "you"- , Space- , Str "see"- , Space- , Str "him"- , Space- , Str "repressing"- , Space- , Str "me?"- , Space- , Str "You"- , Space- , Str "saw"- , Space- , Str "him,"- , Space- , Str "Didn\8217t"- , Space- , Str "you?"- ]- , Para- [ Str "\8212"- , Space- , Str "Monty"- , Space- , Str "Python"- , Space- , Str "and"- , Space- , Str "the"- , Space- , Str "Holy"- , Space- , Str "Grail"- ]- ]- , Div- ( "roads" , [ "movie" ] , [ ( "wrapper" , "1" ) ] )- [ BlockQuote- [ Para- [ Str "Roads?"- , Space- , Str "Where"- , Space- , Str "we\8217re"- , Space- , Str "going,"- , Space- , Str "we"- , Space- , Str "don\8217t"- , Space- , Str "need"- , Space- , Str "roads."- ]- , Para- [ Str "\8212"- , Space- , Str "Dr."- , Space- , Str "Emmett"- , Space- , Str "Brown"- ]- ]- ]- , Header 2 ( "_pass" , [] , [] ) [ Str "Pass" ]- , Para [ Str "pass" , Space , Emph [ Str "through" ] ]- , Header- 2- ( "_open_block" , [] , [] )- [ Str "Open" , Space , Str "block" ]- , Div- ( "" , [] , [ ( "key" , "a value" ) ] )- [ Div- ( "" , [ "title" ] , [] )- [ Para [ Str "A" , Space , Str "title." ] ]- , Para- [ Str "Any"- , Space- , Str "content"- , Space- , Str "can"- , Space- , Str "go"- , Space- , Str "here:"- ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "one" ] ] , [ Para [ Str "two" ] ] ]- ]- , Header 2 ( "_anchor" , [] , [] ) [ Str "Anchor" ]- , Div- ( "goals" , [] , [ ( "wrapper" , "1" ) ] )- [ BulletList- [ [ Para [ Str "one" ] ] , [ Para [ Str "two" ] ] ]- ]- , Header 2 ( "_breaks" , [] , [] ) [ Str "Breaks" ]- , Para- [ Str "Asciidoc"- , Space- , Str "thematic"- , Space- , Str "break:"- ]- , HorizontalRule- , Para [ Str "Markdown" , Space , Str "style:" ]- , HorizontalRule- , HorizontalRule- , HorizontalRule- , HorizontalRule- , Para [ Str "Page" , Space , Str "breaks:" ]- , Div- ( "" , [ "page-break" ] , [ ( "wrapper" , "1" ) ] )- [ HorizontalRule ]- , Div- ( ""- , [ "page-break" ]- , [ ( "options" , "always" ) , ( "wrapper" , "1" ) ]- )- [ HorizontalRule ]- , Header 2 ( "_list" , [] , [] ) [ Str "List" ]- , BulletList- [ [ Para- [ Str "Edgar" , Space , Str "Allan" , Space , Str "Poe" ]- ]- , [ Para- [ Str "Sheri" , Space , Str "S." , Space , Str "Tepper" ]- ]- , [ Para [ Str "Bill" , Space , Str "Bryson" ] ]- ]- , Div- ( "" , [] , [] )- [ Div- ( "" , [ "title" ] , [] )- [ Para- [ Str "Kizmet\8217s"- , Space- , Str "Favorite"- , Space- , Str "Authors"- ]- ]- , BulletList- [ [ Para- [ Str "Edgar"- , Space- , Str "Allan"- , Space- , Str "Poe"- ]- ]- , [ Para- [ Str "Sheri"- , Space- , Str "S."- , Space- , Str "Tepper"- ]- ]- , [ Para [ Str "Bill" , Space , Str "Bryson" ] ]- ]- ]- , BulletList- [ [ Para- [ Str "Edgar" , Space , Str "Allan" , Space , Str "Poe" ]- ]- , [ Para- [ Str "Sheri" , Space , Str "S." , Space , Str "Tepper" ]- ]- , [ Para [ Str "Bill" , Space , Str "Bryson" ] ]- ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Nested" , Space , Str "list" ]- , BulletList- [ [ Para- [ Str "West"- , Space- , Str "wood"- , Space- , Str "maze"- ]- , BulletList- [ [ Para [ Str "Maze" , Space , Str "heart" ]- , BulletList- [ [ Para- [ Str "Reflection" , Space , Str "pool" ]- ]- ]- ]- , [ Para [ Str "Secret" , Space , Str "exit" ] ]- ]- ]- , [ Para- [ Str "Level"- , Space- , Str "1"- , Space- , Str "list"- , Space- , Str "item"- ]- , BulletList- [ [ Para- [ Str "Level"- , Space- , Str "2"- , Space- , Str "list"- , Space- , Str "item"- ]- , BulletList- [ [ Para- [ Str "Level"- , Space- , Str "3"- , Space- , Str "list"- , Space- , Str "item"- ]- , BulletList- [ [ Para- [ Str "Level"- , Space- , Str "4"- , Space- , Str "list"- , Space- , Str "item"- ]- , BulletList- [ [ Para- [ Str "Level"- , Space- , Str "5"- , Space- , Str "list"- , Space- , Str "item"- ]- , BulletList- [ [ Para [ Str "etc." ] ] ]- ]- ]- ]- ]- ]- ]- ]- ]- ]- , [ Para- [ Str "Level"- , Space- , Str "1"- , Space- , Str "list"- , Space- , Str "item"- ]- ]- ]- ]- ]- , BulletList- [ [ Para [ Str "one" ] ]- , [ Para [ Str "two" ] ]- , [ Para [ Str "three" ] ]- ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Protons" ] ]- , [ Para [ Str "Electrons" ] ]- , [ Para [ Str "Neutrons" ] ]- ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Protons" ] ]- , [ Para [ Str "Electrons" ] ]- , [ Para [ Str "Neutrons" ] ]- ]- , Para- [ Str "Start" , Space , Str "with" , Space , Str "4:" ]- , OrderedList- ( 4 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Step" , Space , Str "four" ] ]- , [ Para [ Str "Step" , Space , Str "five" ] ]- , [ Para [ Str "Step" , Space , Str "six" ] ]- ]- , Para [ Str "or" ]- , OrderedList- ( 4 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Step" , Space , Str "four" ] ]- , [ Para [ Str "Step" , Space , Str "five" ] ]- , [ Para [ Str "Step" , Space , Str "six" ] ]- ]- , Para [ Str "Reversed:" ]- , Div- ( "" , [] , [ ( "options" , "reversed" ) ] )- [ Div- ( "" , [ "title" ] , [] )- [ Para- [ Str "Parts"- , Space- , Str "of"- , Space- , Str "an"- , Space- , Str "atom"- ]- ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Protons" ] ]- , [ Para [ Str "Electrons" ] ]- , [ Para [ Str "Neutrons" ] ]- ]- ]- , Para [ Str "Nested" ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Step" , Space , Str "1" ] ]- , [ Para [ Str "Step" , Space , Str "2" ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Step" , Space , Str "2a" ] ]- , [ Para [ Str "Step" , Space , Str "2b" ] ]- ]- ]- , [ Para [ Str "Step" , Space , Str "3" ] ]- ]- , Para [ Str "Mixed" , Space , Str "nested" ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Linux" ]- , BulletList- [ [ Para [ Str "Fedora" ] ]- , [ Para [ Str "Ubuntu" ] ]- , [ Para [ Str "Slackware" ] ]- ]- ]- , [ Para [ Str "BSD" ]- , BulletList- [ [ Para [ Str "FreeBSD" ] ] , [ Para [ Str "NetBSD" ] ] ]- ]- ]- , Para [ Str "With" , Space , Str "spacing" ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Linux" ]- , BulletList- [ [ Para [ Str "Fedora" ] ]- , [ Para [ Str "Ubuntu" ] ]- , [ Para [ Str "Slackware" ] ]- ]- ]- , [ Para [ Str "BSD" ]- , BulletList- [ [ Para [ Str "FreeBSD" ] ] , [ Para [ Str "NetBSD" ] ] ]- ]- ]- , Para- [ Str "With" , Space , Str "number" , Space , Str "styles" ]- , OrderedList- ( 5 , LowerRoman , DefaultDelim )- [ [ Para [ Str "Five" ] ]- , [ Para [ Str "Six" ]- , OrderedList- ( 1 , LowerAlpha , DefaultDelim )- [ [ Para [ Str "a" ] ]- , [ Para [ Str "b" ] ]- , [ Para [ Str "c" ] ]- ]- ]- , [ Para [ Str "Seven" ] ]- ]- , Para [ Str "Checklist" ]- , BulletList- [ [ Para [ Str "\9746" , Space , Str "checked" ] ]- , [ Para- [ Str "\9746" , Space , Str "also" , Space , Str "checked" ]- ]- , [ Para- [ Str "\9744" , Space , Str "not" , Space , Str "checked" ]- ]- , [ Para- [ Str "normal" , Space , Str "list" , Space , Str "item" ]- ]- ]- , Para- [ Str "Separate"- , Space- , Str "lists"- , Space- , Str "with"- , Space- , Str "block"- , Space- , Str "attribute"- ]- , BulletList- [ [ Para [ Str "Apples" ] ]- , [ Para [ Str "Oranges" ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [ Str "Wash" ] ] , [ Para [ Str "Slice" ] ] ]- ]- ]- , Para [ Str "Multiline" , Space , Str "items" ]- , BulletList- [ [ Para- [ Str "Blah"- , Space- , Str "blah."- , SoftBreak- , Str "Blah"- , Space- , Str "blah."- ]- ]- , [ Para- [ Str "The"- , Space- , Str "document"- , Space- , Str "header"- , Space- , Str "in"- , Space- , Str "AsciiDoc"- , Space- , Str "is"- , Space- , Str "optional."- , SoftBreak- , Str "If"- , Space- , Str "present,"- , Space- , Str "it"- , Space- , Str "must"- , Space- , Str "start"- , Space- , Str "with"- , Space- , Str "a"- , Space- , Str "document"- , Space- , Str "title."- ]- ]- ]- , BulletList- [ [ Para- [ Str "Optional"- , Space- , Str "author"- , Space- , Str "and"- , Space- , Str "revision"- , Space- , Str "information"- , Space- , Str "lines"- , SoftBreak- , Str "immediately"- , Space- , Str "follow"- , Space- , Str "the"- , Space- , Str "document"- , Space- , Str "title."- ]- ]- ]- , BulletList- [ [ Para- [ Str "The"- , Space- , Str "document"- , Space- , Str "header"- , Space- , Str "must"- , Space- , Str "be"- , Space- , Str "separated"- , Space- , Str "from"- , SoftBreak- , Str "the"- , Space- , Str "remainder"- , Space- , Str "of"- , Space- , Str "the"- , Space- , Str "document"- , Space- , Str "by"- , Space- , Str "one"- , Space- , Str "or"- , Space- , Str "more"- , SoftBreak- , Str "empty"- , Space- , Str "lines"- , Space- , Str "and"- , Space- , Str "it"- , Space- , Str "cannot"- , Space- , Str "contain"- , Space- , Str "empty"- , Space- , Str "lines."- ]- ]- ]- , Para [ Str "Complex" , Space , Str "item" ]- , BulletList- [ [ Para- [ Str "The"- , Space- , Str "header"- , Space- , Str "in"- , Space- , Str "AsciiDoc"- , Space- , Str "must"- , Space- , Str "start"- , Space- , Str "with"- , Space- , Str "a"- , Space- , Str "document"- , Space- , Str "title."- ]- , CodeBlock ( "" , [] , [] ) "= Document Title"- , Para- [ Str "Keep"- , Space- , Str "in"- , Space- , Str "mind"- , Space- , Str "that"- , Space- , Str "the"- , Space- , Str "header"- , Space- , Str "is"- , Space- , Str "optional."- ]- ]- , [ Para- [ Str "Optional"- , Space- , Str "author"- , Space- , Str "and"- , Space- , Str "revision"- , Space- , Str "information"- , Space- , Str "lines"- , Space- , Str "immediately"- , Space- , Str "follow"- , Space- , Str "the"- , Space- , Str "document"- , SoftBreak- , Str "title."- ]- , CodeBlock- ( "" , [] , [] )- "= Document Title\nDoc Writer <doc.writer@asciidoc.org>\nv1.0, 2022-01-01"- ]- , [ Para [ Str "Second" , Space , Str "item" ] ]- ]- , Para- [ Str "Empty"- , Space- , Str "principle"- , Space- , Str "element:"- ]- , OrderedList- ( 1 , DefaultStyle , DefaultDelim )- [ [ Para [] , CodeBlock ( "" , [] , [] ) "test" ] ]- , Header 2 ( "_table" , [] , [] ) [ Str "Table" ]- , Header- 3- ( "_simple_with_column_specs" , [] , [] )- [ Str "Simple"- , Space- , Str "with"- , Space- , Str "column"- , Space- , Str "specs"- ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidth 0.375 )- , ( AlignDefault , ColWidth 0.25 )- , ( AlignDefault , ColWidth 0.375 )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "This"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "placed"- , Space- , Str "in"- , Space- , Str "the"- , Space- , Str "first"- , Space- , Str "cell"- , Space- , Str "of"- , Space- , Str "column"- , Space- , Str "1"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "This"- , Space- , Str "line"- , Space- , Str "starts"- , Space- , Str "with"- , Space- , Str "a"- , Space- , Str "vertical"- , Space- , Str "bar"- , Space- , Str "so"- , Space- , Str "this"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "placed"- , Space- , Str "in"- , Space- , Str "a"- , Space- , Str "new"- , Space- , Str "cell"- , Space- , Str "in"- , SoftBreak- , Str "column"- , Space- , Str "2"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "When"- , Space- , Str "the"- , Space- , Str "processor"- , Space- , Str "encounters"- , Space- , Str "a"- , Space- , Str "whitespace"- , Space- , Str "followed"- , Space- , Str "by"- , Space- , Str "a"- , Space- , Str "vertical"- , Space- , Str "bar"- , Space- , Str "it"- , SoftBreak- , Str "ends"- , Space- , Str "the"- , Space- , Str "previous"- , Space- , Str "cell"- , Space- , Str "and"- , Space- , Str "starts"- , Space- , Str "a"- , Space- , Str "new"- , Space- , Str "cell"- ]- ]- ]- ])- [ TableBody ( "" , [] , [] ) (RowHeadColumns 0) [] [] ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_repeated_column_in_specs" , [] , [] )- [ Str "Repeated"- , Space- , Str "column"- , Space- , Str "in"- , Space- , Str "specs"- ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignRight- (RowSpan 1)- (ColSpan 1)- [ Para- [ Strong- [ Str "This"- , Space- , Str "cell\8217s"- , Space- , Str "specifier"- , Space- , Str "indicates"- , Space- , Str "that"- , Space- , Str "this"- , Space- , Str "cell\8217s"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "right-aligned"- , Space- , Str "and"- , Space- , Str "bold."- ]- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "The"- , Space- , Str "cell"- , Space- , Str "specifier"- , Space- , Str "on"- , Space- , Str "this"- , Space- , Str "cell"- , Space- , Str "hasn\8217t"- , Space- , Str "been"- , Space- , Str "set"- , Space- , Str "explicitly,"- , Space- , Str "so"- , Space- , Str "the"- , Space- , Str "default"- , SoftBreak- , Str "properties"- , Space- , Str "are"- , Space- , Str "applied."- ]- ]- ]- ])- [ TableBody ( "" , [] , [] ) (RowHeadColumns 0) [] [] ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_simple_without_column_specs" , [] , [] )- [ Str "Simple"- , Space- , Str "without"- , Space- , Str "column"- , Space- , Str "specs"- ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "1,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "2,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "2,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1,"- , Space- , Str "row"- , Space- , Str "3"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "2,"- , Space- , Str "row"- , Space- , Str "3"- ]- ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_with_caption" , [] , [] )- [ Str "With" , Space , Str "caption" ]- , Table- ( "" , [] , [] )- (Caption- Nothing- [ Plain- [ Str "My" , Space , Str "cool" , Space , Str "table." ]- ])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "1,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "2,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "2,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1,"- , Space- , Str "row"- , Space- , Str "3"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "2,"- , Space- , Str "row"- , Space- , Str "3"- ]- ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_no_header" , [] , [] )- [ Str "No" , Space , Str "header" ]- , Para- [ Str "By"- , Space- , Str "default"- , Space- , Str "the"- , Space- , Str "first"- , Space- , Str "line"- , Space- , Str "should"- , Space- , Str "turn"- , Space- , Str "into"- , Space- , Str "the"- , Space- , Str "header,"- , Space- , Str "but"- , Space- , Str "this"- , SoftBreak- , Str "can"- , Space- , Str "be"- , Space- , Str "disabled:"- ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead ( "" , [] , [] ) [])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1,"- , Space- , Str "row"- , Space- , Str "1"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "2,"- , Space- , Str "row"- , Space- , Str "1"- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "2,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Para- [ Str "And"- , Space- , Str "also"- , Space- , Str "explicitly"- , Space- , Str "enabled:"- ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Cell" , Space , Str "A1" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Cell" , Space , Str "B1" ] ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Cell" , Space , Str "A2" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Cell" , Space , Str "B2" ] ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Cell" , Space , Str "A3" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Cell" , Space , Str "B3" ] ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header 3 ( "_footer" , [] , [] ) [ Str "Footer" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidth 0.4 )- , ( AlignDefault , ColWidth 0.4 )- , ( AlignDefault , ColWidth 0.2 )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "1,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "2,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "3,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "2,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "3,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "1,"- , Space- , Str "footer"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "2,"- , Space- , Str "footer"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "3,"- , Space- , Str "footer"- , Space- , Str "row"- ]- ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Para [ Str "or" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "1,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "2,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "2,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1,"- , Space- , Str "row"- , Space- , Str "3"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "2,"- , Space- , Str "row"- , Space- , Str "3"- ]- ]- ]- ]- ]- (TableFoot- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "1,"- , Space- , Str "footer"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "2,"- , Space- , Str "footer"- , Space- , Str "row"- ]- ]- ]- ])- , Header 3 ( "_alignment" , [] , [] ) [ Str "Alignment" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Column" , Space , Str "Name" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Column" , Space , Str "Name" ] ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignCenter- (RowSpan 1)- (ColSpan 2)- [ Para- [ Str "This"- , Space- , Str "cell"- , Space- , Str "spans"- , Space- , Str "two"- , Space- , Str "columns,"- , Space- , Str "and"- , Space- , Str "its"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "horizontally"- , Space- , Str "centered"- , Space- , Str "because"- , Space- , Str "the"- , SoftBreak- , Str "cell"- , Space- , Str "specifier"- , Space- , Str "includes"- , Space- , Str "the"- , Space- , Code ( "" , [] , [] ) "^"- , Space- , Str "operator."- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignCenter- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "This"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "duplicated"- , Space- , Str "in"- , Space- , Str "two"- , Space- , Str "adjacent"- , Space- , Str "columns."- , SoftBreak- , Str "Its"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "horizontally"- , Space- , Str "centered"- , Space- , Str "because"- , Space- , Str "the"- , Space- , Str "cell"- , Space- , Str "specifier"- , SoftBreak- , Str "includes"- , Space- , Str "the"- , Space- , Code ( "" , [] , [] ) "^"- , Space- , Str "operator."- ]- ]- , Cell- ( "" , [] , [] )- AlignCenter- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "This"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "duplicated"- , Space- , Str "in"- , Space- , Str "two"- , Space- , Str "adjacent"- , Space- , Str "columns."- , SoftBreak- , Str "Its"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "horizontally"- , Space- , Str "centered"- , Space- , Str "because"- , Space- , Str "the"- , Space- , Str "cell"- , Space- , Str "specifier"- , SoftBreak- , Str "includes"- , Space- , Str "the"- , Space- , Code ( "" , [] , [] ) "^"- , Space- , Str "operator."- ]- ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_multiple_paragraphs_in_cells" , [] , [] )- [ Str "Multiple"- , Space- , Str "paragraphs"- , Space- , Str "in"- , Space- , Str "cells"- ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault ) ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Single"- , Space- , Str "paragraph"- , Space- , Str "on"- , Space- , Str "row"- , Space- , Str "1"- ]- ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "First"- , Space- , Str "paragraph"- , Space- , Str "on"- , Space- , Str "row"- , Space- , Str "2"- ]- , Para- [ Str "Second"- , Space- , Str "paragraph"- , Space- , Str "on"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_complex_table" , [] , [] )- [ Str "Complex" , Space , Str "table" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignRight- (RowSpan 1)- (ColSpan 1)- [ Para- [ Code ( "" , [] , [] ) "This"- , Space- , Code ( "" , [] , [] ) "content"- , Space- , Code ( "" , [] , [] ) "is"- , Space- , Code ( "" , [] , [] ) "duplicated"- , Space- , Code ( "" , [] , [] ) "across"- , Space- , Code ( "" , [] , [] ) "two"- , Space- , Code ( "" , [] , [] ) "columns."- ]- , Para- [ Code ( "" , [] , [] ) "It"- , Space- , Code ( "" , [] , [] ) "is"- , Space- , Code ( "" , [] , [] ) "aligned"- , Space- , Code ( "" , [] , [] ) "right"- , Space- , Code ( "" , [] , [] ) "horizontally."- ]- , Para- [ Code ( "" , [] , [] ) "And"- , Space- , Code ( "" , [] , [] ) "it"- , Space- , Code ( "" , [] , [] ) "is"- , Space- , Code ( "" , [] , [] ) "monospaced."- ]- ]- , Cell- ( "" , [] , [] )- AlignRight- (RowSpan 1)- (ColSpan 1)- [ Para- [ Code ( "" , [] , [] ) "This"- , Space- , Code ( "" , [] , [] ) "content"- , Space- , Code ( "" , [] , [] ) "is"- , Space- , Code ( "" , [] , [] ) "duplicated"- , Space- , Code ( "" , [] , [] ) "across"- , Space- , Code ( "" , [] , [] ) "two"- , Space- , Code ( "" , [] , [] ) "columns."- ]- , Para- [ Code ( "" , [] , [] ) "It"- , Space- , Code ( "" , [] , [] ) "is"- , Space- , Code ( "" , [] , [] ) "aligned"- , Space- , Code ( "" , [] , [] ) "right"- , Space- , Code ( "" , [] , [] ) "horizontally."- ]- , Para- [ Code ( "" , [] , [] ) "And"- , Space- , Code ( "" , [] , [] ) "it"- , Space- , Code ( "" , [] , [] ) "is"- , Space- , Code ( "" , [] , [] ) "monospaced."- ]- ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignCenter- (RowSpan 3)- (ColSpan 1)- [ Para- [ Strong- [ Str "This"- , Space- , Str "cell"- , Space- , Str "spans"- , Space- , Str "3"- , Space- , Str "rows."- , Space- , Str "The"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "centered"- , Space- , Str "horizontally,"- , Space- , Str "aligned"- , Space- , Str "to"- , Space- , Str "the"- , Space- , Str "bottom"- , Space- , Str "of"- , Space- , Str "the"- , Space- , Str "cell,"- , Space- , Str "and"- , Space- , Str "strong."- ]- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Emph- [ Str "This"- , Space- , Str "content"- , Space- , Str "is"- , Space- , Str "emphasized."- ]- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ CodeBlock- ( "" , [] , [] )- "This content is aligned to the top of the cell and literal.\n\n"- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ CodeBlock- ( "" , [] , [] )- "puts \"This is a source block!\""- ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_column_styles" , [] , [] )- [ Str "Column" , Space , Str "styles" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Code ( "" , [] , [] ) "monospace" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Code ( "" , [] , [] ) "mono" ] ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "default" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Code ( "" , [] , [] ) "mono" ] ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_block_elements_in_cells" , [] , [] )- [ Str "Block"- , Space- , Str "elements"- , Space- , Str "in"- , Space- , Str "cells"- ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Normal" , Space , Str "Style" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "AsciiDoc" , Space , Str "Style" ] ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "This"- , Space- , Str "cell"- , Space- , Str "isn\8217t"- , Space- , Str "prefixed"- , Space- , Str "with"- , Space- , Str "an"- , Space- , Code ( "" , [] , [] ) "a"- , Str ","- , Space- , Str "so"- , Space- , Str "the"- , Space- , Str "processor"- , Space- , Str "doesn\8217t"- , Space- , Str "interpret"- , Space- , Str "the"- , SoftBreak- , Str "following"- , Space- , Str "lines"- , Space- , Str "as"- , Space- , Str "an"- , Space- , Str "AsciiDoc"- , Space- , Str "list."- ]- , Para- [ Str "*"- , Space- , Str "List"- , Space- , Str "item"- , Space- , Str "1"- , SoftBreak- , Str "*"- , Space- , Str "List"- , Space- , Str "item"- , Space- , Str "2"- , SoftBreak- , Str "*"- , Space- , Str "List"- , Space- , Str "item"- , Space- , Str "3"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "This"- , Space- , Str "cell"- , Space- , Str "is"- , Space- , Str "prefixed"- , Space- , Str "with"- , Space- , Str "an"- , Space- , Code ( "" , [] , [] ) "a"- , Str ","- , Space- , Str "so"- , Space- , Str "the"- , Space- , Str "processor"- , Space- , Str "interprets"- , Space- , Str "the"- , Space- , Str "following"- , Space- , Str "lines"- , SoftBreak- , Str "as"- , Space- , Str "an"- , Space- , Str "AsciiDoc"- , Space- , Str "list."- ]- , BulletList- [ [ Para- [ Str "List"- , Space- , Str "item"- , Space- , Str "1"- ]- ]- , [ Para- [ Str "List"- , Space- , Str "item"- , Space- , Str "2"- ]- ]- , [ Para- [ Str "List"- , Space- , Str "item"- , Space- , Str "3"- ]- ]- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "This"- , Space- , Str "cell"- , Space- , Str "isn\8217t"- , Space- , Str "prefixed"- , Space- , Str "with"- , Space- , Str "an"- , Space- , Code ( "" , [] , [] ) "a"- , Str ","- , Space- , Str "so"- , Space- , Str "the"- , Space- , Str "processor"- , Space- , Str "doesn\8217t"- , Space- , Str "interpret"- , Space- , Str "the"- , Space- , Str "listing"- , SoftBreak- , Str "block"- , Space- , Str "delimiters"- , Space- , Str "or"- , Space- , Str "the"- , Space- , Code ( "" , [] , [] ) "source"- , Space- , Str "style."- ]- , Para- [ Str "----"- , SoftBreak- , Str "import"- , Space- , Str "os"- , SoftBreak- , Str "print"- , Space- , Str "(\"%s\""- , Space- , Str "%(os.uname()))"- , SoftBreak- , Str "----"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "This"- , Space- , Str "cell"- , Space- , Str "is"- , Space- , Str "prefixed"- , Space- , Str "with"- , Space- , Str "an"- , Space- , Code ( "" , [] , [] ) "a"- , Str ","- , Space- , Str "so"- , Space- , Str "the"- , Space- , Str "listing"- , Space- , Str "block"- , Space- , Str "is"- , Space- , Str "processed"- , Space- , Str "and"- , Space- , Str "rendered"- , SoftBreak- , Str "according"- , Space- , Str "to"- , Space- , Str "the"- , Space- , Code ( "" , [] , [] ) "source"- , Space- , Str "style"- , Space- , Str "rules."- ]- , CodeBlock- ( "" , [ "python" ] , [] )- "import os\nprint \"%s\" %(os.uname())"- ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_col_and_rowspan" , [] , [] )- [ Str "Col" , Space , Str "and" , Space , Str "rowspan" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "1,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "2,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Column"- , Space- , Str "3,"- , Space- , Str "header"- , Space- , Str "row"- ]- ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 2)- (ColSpan 2)- [ Para- [ Str "This"- , Space- , Str "cell"- , Space- , Str "spans"- , Space- , Str "2"- , Space- , Str "cols"- , Space- , Str "and"- , Space- , Str "2"- , Space- , Str "rows"- ]- ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "3,"- , Space- , Str "row"- , Space- , Str "2"- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "3,"- , Space- , Str "row"- , Space- , Str "3"- ]- ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 3)- [ Para- [ Str "Cell"- , Space- , Str "in"- , Space- , Str "column"- , Space- , Str "1-3,"- , Space- , Str "row"- , Space- , Str "4"- ]- ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_csv_table" , [] , [] )- [ Str "CSV" , Space , Str "table" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Artist" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Track" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Genre" ] ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Baauer" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Harlem" , Space , Str "Shake" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Hip" , Space , Str "Hop" ] ]- ]- , Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "The" , Space , Str "Lumineers" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Ho" , Space , Str "Hey" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Folk" , Space , Str "Rock" ] ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Para [ Str "or" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Artist" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Track" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Genre" ] ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Baauer" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Harlem" , Space , Str "Shake" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Hip" , Space , Str "Hop" ] ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Header- 3- ( "_dsv_table" , [] , [] )- [ Str "DSV" , Space , Str "table" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "a" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "b" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "c" ] ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "d" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "e" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "f" ] ]- ]- ]- ]- (TableFoot ( "" , [] , [] ) [])- , Para [ Str "or" ]- , Table- ( "" , [] , [] )- (Caption Nothing [])- [ ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- , ( AlignDefault , ColWidthDefault )- ]- (TableHead- ( "" , [] , [] )- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Artist" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Track" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Genre" ] ]- ]- ])- [ TableBody- ( "" , [] , [] )- (RowHeadColumns 0)- []- [ Row- ( "" , [] , [] )- [ Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Robyn" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Indestructible" ] ]- , Cell- ( "" , [] , [] )- AlignDefault- (RowSpan 1)- (ColSpan 1)- [ Para [ Str "Dance" ] ]+ ( "" , [ "red" , "icon" ] , [] )+ []+ ( "./images/icons/heart.png" , "" )+ ]+ , Para [ Str "anchor:tiger" ]+ , Para [ Strong [ Str "*bold*" ] ]+ , Para+ [ Link+ ( "" , [] , [] )+ [ Str "Get" , Space , Str "Report" ]+ ( "downloads/report.pdf" , "" )+ ]+ , Para+ [ Link+ ( "" , [] , [] )+ [ Str "tools.html#editors" ]+ ( "tools.html#editors" , "" )+ ]+ , Para+ [ Link+ ( "" , [] , [] )+ [ Str "Your" , Space , Str "files" ]+ ( "file:///home/username" , "" )+ ]+ , Para [ Str "Tricky" , Space , Str "cases:" ]+ , Para+ [ Link+ ( "" , [] , [] )+ [ Str "Get" , Space , Str "Report" ]+ ( "My Documents/report.pdf" , "" )+ ]+ , Para+ [ Link+ ( "" , [] , [] )+ [ Str "Get" , Space , Str "Report" ]+ ( "My Documents/report.pdf" , "" )+ ]+ , Para+ [ Link+ ( "" , [] , [] )+ [ Str "Get" , Space , Str "Report" ]+ ( "My%20Documents/report.pdf" , "" )+ ]+ , Para+ [ Link+ ( "" , [] , [] )+ [ Str "https://example.org/now_this__link_works.html" ]+ ( "https://example.org/now_this__link_works.html" , "" )+ ]+ , Para+ [ Link+ ( "" , [] , [] )+ [ Str "Subscribe" ]+ ( "join@discuss.example.org" , "" )+ ]+ , Para+ [ Link+ ( "" , [ "mail" ] , [] )+ [ Str "Click,"+ , Space+ , Str "subscribe,"+ , Space+ , Str "and"+ , Space+ , Str "participate!"+ ]+ ( "join@discuss.example.org" , "" )+ ]+ , Para+ [ Link+ ( "" , [ "cross-reference" ] , [] )+ [ Str "use"+ , Space+ , Str "attributes"+ , Space+ , Str "within"+ , Space+ , Str "the"+ , Space+ , Str "link"+ , Space+ , Str "macro"+ ]+ ( "#link-macro-attributes" , "" )+ ]+ , Figure+ ( "" , [] , [] )+ (Caption Nothing [])+ [ Plain+ [ Image+ ( "" , [] , [] ) [ Str "Sunset" ] ( "sunset.jpg" , "" )+ ]+ ]+ , Figure+ ( "" , [] , [] )+ (Caption Nothing [])+ [ Plain [ Image ( "" , [] , [] ) [] ( "name.png" , "" ) ] ]+ , Figure+ ( "" , [] , [] )+ (Caption Nothing [])+ [ Plain+ [ Image+ ( ""+ , []+ , [ ( "width" , "300px" ) , ( "height" , "400px" ) ]+ )+ [ Str "Sunset" ]+ ( "sunset.jpg" , "" )+ ]+ ]+ , Div+ ( ""+ , []+ , [ ( "wrapper" , "1" )+ , ( "alt" , "Sunset" )+ , ( "height" , "400" )+ , ( "width" , "300" )+ ]+ )+ [ Figure+ ( "" , [] , [] )+ (Caption Nothing [])+ [ Plain [ Image ( "" , [] , [] ) [] ( "sunset.jpg" , "" ) ]+ ]+ ]+ , Para [ Math DisplayMath "e=mc^2\n" ]+ , Para [ Math DisplayMath "sin n / 3\n" ]+ , Para [ Math DisplayMath "e^i\n" ]+ , Header+ 2+ ( "_attribute_substitutions" , [] , [] )+ [ Str "Attribute" , Space , Str "substitutions" ]+ , Para [ Str "Foo" , Space , Str "bar" , Space , Str "baz" ]+ , Para [ Str "{nonexistent}" ]+ , Para+ [ Str "Built"+ , Space+ , Str "in:"+ , Space+ , Str "xyz"+ , Space+ , Str "a\160b\8203c'd\8216"+ ]+ , Header+ 2+ ( "_bold_and_italic" , [] , [] )+ [ Str "Bold" , Space , Str "and" , Space , Str "italic" ]+ , Para+ [ Str "Constrained:"+ , Space+ , Strong+ [ Str "this"+ , Space+ , Str "is"+ , Space+ , Str "bold"+ , Space+ , Emph [ Str "and" , Space , Str "italic" ]+ ]+ , Str "."+ ]+ , Para+ [ Str "Unconstrained:"+ , Space+ , Str "wild"+ , Strong+ [ Str "content"+ , Emph [ Str "with" , Space , Str "italic" ]+ , Str "stuff"+ ]+ , Str "."+ ]+ , Header 2 ( "_monospace" , [] , [] ) [ Str "Monospace" ]+ , Para [ Code ( "" , [] , [] ) "simple" ]+ , Para+ [ Code ( "" , [] , [] ) "complex"+ , Space+ , Strong+ [ Code ( "" , [] , [] ) "with"+ , Space+ , Code ( "" , [] , [] ) "bold"+ ]+ , Space+ , Code ( "" , [] , [] ) "text"+ , Space+ , Code ( "" , [] , [] ) "and"+ , Space+ , Code ( "" , [] , [] ) "a"+ , Space+ , Link+ ( "" , [] , [] )+ [ Code ( "" , [] , [] ) "foo.html" ]+ ( "foo.html" , "" )+ ]+ , Para+ [ Str "unconstrained"+ , Code ( "" , [] , [] ) "wwow"+ , Str "okay"+ ]+ , Header+ 2+ ( "_span_and_inline_attributes" , [] , [] )+ [ Str "Span"+ , Space+ , Str "and"+ , Space+ , Str "inline"+ , Space+ , Str "attributes"+ ]+ , Para+ [ Span+ ( "" , [ "red" ] , [] )+ [ Str "Bonjour" , Space , Strong [ Str "monsieur" ] ]+ ]+ , Para+ [ Str "Un"+ , Span ( "" , [ "red" ] , [] ) [ Str "constrained" ]+ , Str "content"+ ]+ , Para+ [ Str "With"+ , Space+ , Span+ ( "" , [ "mark" ] , [] )+ [ Str "no" , Space , Str "attribute" ]+ , Space+ , Str "it\8217s"+ , Space+ , Str "highlighted."+ ]+ , Header+ 2+ ( "_sub_and_superscript" , [] , [] )+ [ Str "Sub"+ , Space+ , Str "and"+ , Space+ , Str "superscript"+ ]+ , Para [ Str "H" , Subscript [ Str "2" ] , Str "O" ]+ , Para+ [ Str "H"+ , Subscript [ Str "a" , Space , Str "b" ]+ , Str "O"+ ]+ , Para+ [ Str "Not"+ , Space+ , Str "subscript:"+ , Space+ , Str "H~a"+ , Space+ , Str "b~O."+ ]+ , Para [ Str "H^2&O" ]+ , Para+ [ Str "H"+ , Superscript [ Str "a" , Space , Str "b" ]+ , Str "O"+ ]+ , Para+ [ Str "Not"+ , Space+ , Str "subscript:"+ , Space+ , Str "H^a"+ , Space+ , Str "b^O."+ ]+ , Header+ 2 ( "_passthrough" , [] , [] ) [ Str "Passthrough" ]+ , Para+ [ Str "Here"+ , Space+ , Str "the"+ , Space+ , Str "special"+ , Space+ , Str "characters"+ , Space+ , Str "just"+ , Space+ , Str "come"+ , Space+ , Str "through"+ , Space+ , Str "as"+ , Space+ , Str "literal:"+ ]+ , Para [ Str "<b>*test*</b>" ]+ , Para [ Str "xx<b>*test*</b>xx" ]+ , Para+ [ Str "But"+ , Space+ , Str "here"+ , Space+ , Str "they"+ , Space+ , Str "are"+ , Space+ , Str "passed"+ , Space+ , Str "through:"+ ]+ , Para [ Str "xx" , Strong [ Str "*test*" ] , Str "xx" ]+ , Header 2 ( "_quoted" , [] , [] ) [ Str "Quoted" ]+ , Para+ [ Quoted DoubleQuote [ Str "double" , Space , Str "quoted" ]+ ]+ , Para+ [ Quoted SingleQuote [ Str "single" , Space , Str "quoted" ]+ ]+ , Header 2 ( "_footnotes" , [] , [] ) [ Str "Footnotes" ]+ , Para+ [ Str "Double"+ , Note+ [ Para+ [ Str "The"+ , Space+ , Str "double"+ , Space+ , Str "hail-and-rainbow"+ , Space+ , Str "level"+ , Space+ , Str "makes"+ , Space+ , Str "my"+ , Space+ , Str "toes"+ , Space+ , Str "tingle."+ ]+ ]+ ]+ , Para+ [ Str "A"+ , Space+ , Str "bold"+ , Space+ , Str "statement!"+ , Note+ [ Para+ [ Str "Opinions"+ , Space+ , Str "are"+ , Space+ , Str "my"+ , Space+ , Str "own."+ ]+ ]+ , SoftBreak+ , Str "Another"+ , Space+ , Str "outrageous"+ , Space+ , Str "statement."+ , Note+ [ Para+ [ Str "Opinions"+ , Space+ , Str "are"+ , Space+ , Str "my"+ , Space+ , Str "own."+ ]+ ]+ ]+ , Header+ 1+ ( "_block_markup" , [] , [] )+ [ Str "Block" , Space , Str "markup" ]+ , Header 2 ( "_sections" , [] , [] ) [ Str "Sections" ]+ , Header+ 3+ ( "_another_level" , [] , [] )+ [ Str "Another" , Space , Str "level" ]+ , Header+ 4 ( "_level_5" , [] , [] ) [ Str "Level" , Space , Str "5" ]+ , Header+ 3+ ( "_markdown_style" , [] , [] )+ [ Str "Markdown" , Space , Str "style" ]+ , Header+ 4+ ( "_level_5_2" , [] , [] )+ [ Str "Level" , Space , Str "5" ]+ , Header+ 2+ ( "_discrete_heading" , [] , [] )+ [ Str "Discrete" , Space , Str "heading" ]+ , Header+ 3+ ( "" , [] , [] )+ [ Str "A"+ , Space+ , Str "discrete"+ , Space+ , Str "heading,"+ , Space+ , Str "not"+ , Space+ , Str "a"+ , Space+ , Str "section"+ ]+ , Header 2 ( "_paragraph" , [] , [] ) [ Str "Paragraph" ]+ , Para+ [ Str "This"+ , Space+ , Str "is"+ , Space+ , Str "a"+ , Space+ , Str "paragraph"+ , SoftBreak+ , Str "whose"+ , Space+ , Str "source"+ , Space+ , Str "fits"+ , Space+ , Str "on"+ , Space+ , Str "two"+ , Space+ , Str "lines."+ ]+ , Para+ [ Str "{.This"+ , Space+ , Str "is"+ , Space+ , Str "my"+ , Space+ , Str "title}"+ , SoftBreak+ , Str "A"+ , Space+ , Str "paragraph"+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Str "title."+ ]+ , Header+ 2+ ( "_example_block" , [] , [] )+ [ Str "Example" , Space , Str "block" ]+ , Div+ ( "" , [] , [] )+ [ Div+ ( "" , [ "title" ] , [] )+ [ Para [ Str "Optional" , Space , Str "title" ] ]+ , Para+ [ Str "This"+ , Space+ , Str "is"+ , Space+ , Str "an"+ , Space+ , Str "example"+ , Space+ , Str "of"+ , Space+ , Str "an"+ , Space+ , Str "example"+ , Space+ , Str "block."+ ]+ ]+ , Div+ ( "" , [ "example" ] , [] )+ [ Div+ ( "" , [ "title" ] , [] )+ [ Para [ Str "Optional" , Space , Str "title" ] ]+ , Para+ [ Str "Paragraph" , Space , Strong [ Str "one" ] , Str "." ]+ , Para+ [ Str "Paragraph" , Space , Strong [ Str "two" ] , Str "." ]+ ]+ , Header 2 ( "_admonition" , [] , [] ) [ Str "Admonition" ]+ , Para [ Str "Simple" , Space , Str "form:" ]+ , Div+ ( "" , [ "warning" ] , [] )+ [ Div ( "" , [ "title" ] , [] ) [ Para [ Str "Warning" ] ]+ , Para+ [ Str "This"+ , Space+ , Str "is"+ , Space+ , Str "very"+ , Space+ , Str "dangerous."+ , SoftBreak+ , Str "Don\8217t"+ , Space+ , Str "do"+ , Space+ , Str "it"+ , Space+ , Str "unless"+ , Space+ , Str "you"+ , Space+ , Str "understand"+ , Space+ , Str "the"+ , Space+ , Str "risks."+ ]+ ]+ , Div+ ( "" , [ "important" ] , [] )+ [ Div+ ( "" , [ "title" ] , [] )+ [ Para+ [ Str "Title"+ , Space+ , Str "of"+ , Space+ , Str "the"+ , Space+ , Str "admonition"+ ]+ ]+ , Para [ Str "Remember:" ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para+ [ Str "Don\8217t"+ , Space+ , Str "do"+ , Space+ , Str "this."+ ]+ ]+ , [ Para+ [ Str "And"+ , Space+ , Str "don\8217t"+ , Space+ , Str "do"+ , Space+ , Str "that."+ ]+ ]+ ]+ ]+ , Header 2 ( "_sidebar" , [] , [] ) [ Str "Sidebar" ]+ , Para+ [ Str "A" , Space , Str "simple" , Space , Str "sidebar." ]+ , Div+ ( "" , [ "sidebar" ] , [] )+ [ Div+ ( "" , [ "title" ] , [] )+ [ Para+ [ Str "Optional"+ , Space+ , Str "Title"+ , Space+ , Strong+ [ Str "with"+ , Space+ , Str "strong"+ , Space+ , Str "emphasis"+ ]+ ]+ ]+ , Para+ [ Str "Here"+ , Space+ , Str "is"+ , Space+ , Str "a"+ , Space+ , Str "sidebar."+ ]+ , Div+ ( "" , [ "tip" ] , [] )+ [ Div ( "" , [ "title" ] , [] ) [ Para [ Str "Tip" ] ]+ , Para+ [ Str "It"+ , Space+ , Str "can"+ , Space+ , Str "contain"+ , Space+ , Str "any"+ , Space+ , Str "type"+ , Space+ , Str "of"+ , Space+ , Str "content."+ ]+ ]+ ]+ , Header+ 2+ ( "_literal_block" , [] , [] )+ [ Str "Literal" , Space , Str "block" ]+ , Para+ [ Str "Short"+ , Space+ , Str "indented"+ , Space+ , Str "code:"+ ]+ , CodeBlock+ ( "" , [] , [] )+ "$ ls -a\n$ cat /foo/bar/baz \\\n /bi/bim/bop\n"+ , CodeBlock+ ( "" , [] , [] ) "This is\n a literal block too.\n"+ , CodeBlock+ ( "" , [] , [] )+ " Fenced\n $+ *a* literal\n\n****\nnot a sidebar\n****\n"+ , Header 2 ( "_listing" , [] , [] ) [ Str "Listing" ]+ , CodeBlock+ ( "" , [ "ruby" ] , [] )+ "require 'sinatra'\n\nget '/hi' do\n \"Hello World!\"\nend"+ , Para [ Str "Implied:" ]+ , CodeBlock+ ( "" , [ "ruby" ] , [] )+ "require 'sinatra'\n\nget '/hi' do\n \"Hello World!\"\nend"+ , CodeBlock+ ( "" , [ "ruby" ] , [] )+ "# A function\ndef foo\n return 42\nend"+ , CodeBlock+ ( "hello" , [ "haskell" ] , [] )+ "putStrLn $ unwords [\"Hello\", \"world\"]"+ , Para [ Str "Line" , Space , Str "numbering:" ]+ , CodeBlock+ ( "" , [] , [ ( "options" , "linenums" ) ] )+ "puts 1\nputs 2\nputs 3"+ , CodeBlock+ ( "" , [] , [] ) "This doesn't have a language.\n +=\"hi\""+ , Para+ [ Str "And"+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Str "callout"+ , Space+ , Str "list:"+ ]+ , CodeBlock+ ( "" , [ "ruby" ] , [] )+ "require 'sinatra' \9312\n\nget '/hi' do \9313 \9314\n \"Hello World!\"\nend"+ , Div+ ( "" , [ "callout-list" ] , [] )+ [ OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Library" , Space , Str "import" ] ]+ , [ Para [ Str "URL" , Space , Str "mapping" ] ]+ , [ Para [ Str "Response" , Space , Str "block" ] ]+ ]+ ]+ , Para [ Str "Markdown-style" , Space , Str "fenced:" ]+ , CodeBlock+ ( "" , [ "ruby" ] , [] ) "def foo\n return 5\nend"+ , Header 2 ( "_verse" , [] , [] ) [ Str "Verse" ]+ , BlockQuote+ [ Para+ [ Str "The"+ , Space+ , Str "fog"+ , Space+ , Str "comes"+ , LineBreak+ , Str "on"+ , Space+ , Str "little"+ , Space+ , Str "cat"+ , Space+ , Str "feet."+ ]+ , Para+ [ Str "\8212"+ , Space+ , Str "Carl"+ , Space+ , Str "Sandburg,"+ , Space+ , Str "two"+ , Space+ , Str "lines"+ , Space+ , Str "from"+ , Space+ , Str "the"+ , Space+ , Str "poem"+ , Space+ , Str "Fog"+ ]+ ]+ , BlockQuote+ [ Para+ [ Str "The"+ , Space+ , Str "fog"+ , Space+ , Str "comes"+ , LineBreak+ , Str "on"+ , Space+ , Str "little"+ , Space+ , Str "cat"+ , Space+ , Str "feet."+ , LineBreak+ , Str "It"+ , Space+ , Str "sits"+ , Space+ , Str "looking"+ , LineBreak+ , Str "over"+ , Space+ , Str "harbor"+ , Space+ , Str "and"+ , Space+ , Str "city"+ , LineBreak+ , Str "on"+ , Space+ , Str "silent"+ , Space+ , Str "haunches"+ , LineBreak+ , Str "and"+ , Space+ , Str "then"+ , Space+ , Str "moves"+ , Space+ , Str "on."+ ]+ , Para+ [ Str "\8212"+ , Space+ , Str "Carl"+ , Space+ , Str "Sandburg,"+ , Space+ , Str "Fog"+ ]+ ]+ , Header+ 2 ( "_collapsible" , [] , [] ) [ Str "Collapsible" ]+ , Para+ [ Str "Click"+ , Space+ , Str "here"+ , Space+ , Str "for"+ , Space+ , Str "more."+ ]+ , Div+ ( ""+ , [ "example" ]+ , [ ( "options" , "collapsible,open" ) ]+ )+ [ Para+ [ Str "This"+ , Space+ , Str "is"+ , Space+ , Str "collapsible."+ ]+ , Para+ [ Str "It"+ , Space+ , Str "can"+ , Space+ , Str "be"+ , Space+ , Str "hidden."+ ]+ ]+ , Div+ ( "" , [] , [ ( "options" , "collapsible" ) ] )+ [ Div+ ( "" , [ "title" ] , [] )+ [ Para [ Str "Click" , Space , Str "me!" ] ]+ , Para+ [ Str "This"+ , Space+ , Str "paragraph"+ , Space+ , Str "is"+ , SoftBreak+ , Str "also"+ , Space+ , Str "collapsible."+ ]+ ]+ , Header 2 ( "_quote" , [] , [] ) [ Str "Quote" ]+ , BlockQuote+ [ Para+ [ Str "Everybody"+ , Space+ , Str "remember"+ , Space+ , Str "where"+ , Space+ , Str "we"+ , Space+ , Str "parked."+ ]+ , Para+ [ Str "\8212"+ , Space+ , Str "Captain"+ , Space+ , Str "James"+ , Space+ , Str "T."+ , Space+ , Str "Kirk,"+ , Space+ , Str "Star"+ , Space+ , Str "Trek"+ , Space+ , Str "IV:"+ , Space+ , Str "The"+ , Space+ , Str "Voyage"+ , Space+ , Str "Home"+ ]+ ]+ , BlockQuote+ [ Para+ [ Str "Dennis:"+ , Space+ , Str "Come"+ , Space+ , Str "and"+ , Space+ , Str "see"+ , Space+ , Str "the"+ , Space+ , Str "violence"+ , Space+ , Str "inherent"+ , Space+ , Str "in"+ , Space+ , Str "the"+ , Space+ , Str "system."+ , Space+ , Str "Help!"+ , Space+ , Str "Help!"+ , Space+ , Str "I\8217m"+ , Space+ , Str "being"+ , SoftBreak+ , Str "repressed."+ ]+ , Para+ [ Str "King"+ , Space+ , Str "Arthur:"+ , Space+ , Str "Bloody"+ , Space+ , Str "peasant!"+ ]+ , Para+ [ Str "Dennis:"+ , Space+ , Str "Oh,"+ , Space+ , Str "what"+ , Space+ , Str "a"+ , Space+ , Str "giveaway!"+ , Space+ , Str "Did"+ , Space+ , Str "you"+ , Space+ , Str "hear"+ , Space+ , Str "that?"+ , Space+ , Str "Did"+ , Space+ , Str "you"+ , Space+ , Str "hear"+ , Space+ , Str "that,"+ , Space+ , Str "eh?"+ , Space+ , Str "That\8217s"+ , Space+ , Str "what"+ , Space+ , Str "I\8217m"+ , SoftBreak+ , Str "on"+ , Space+ , Str "about!"+ , Space+ , Str "Did"+ , Space+ , Str "you"+ , Space+ , Str "see"+ , Space+ , Str "him"+ , Space+ , Str "repressing"+ , Space+ , Str "me?"+ , Space+ , Str "You"+ , Space+ , Str "saw"+ , Space+ , Str "him,"+ , Space+ , Str "Didn\8217t"+ , Space+ , Str "you?"+ ]+ , Para+ [ Str "\8212"+ , Space+ , Str "Monty"+ , Space+ , Str "Python"+ , Space+ , Str "and"+ , Space+ , Str "the"+ , Space+ , Str "Holy"+ , Space+ , Str "Grail"+ ]+ ]+ , Div+ ( "roads" , [ "movie" ] , [ ( "wrapper" , "1" ) ] )+ [ BlockQuote+ [ Para+ [ Str "Roads?"+ , Space+ , Str "Where"+ , Space+ , Str "we\8217re"+ , Space+ , Str "going,"+ , Space+ , Str "we"+ , Space+ , Str "don\8217t"+ , Space+ , Str "need"+ , Space+ , Str "roads."+ ]+ , Para+ [ Str "\8212"+ , Space+ , Str "Dr."+ , Space+ , Str "Emmett"+ , Space+ , Str "Brown"+ ]+ ]+ ]+ , Header 2 ( "_pass" , [] , [] ) [ Str "Pass" ]+ , Para [ Str "pass" , Space , Emph [ Str "through" ] ]+ , Header+ 2+ ( "_open_block" , [] , [] )+ [ Str "Open" , Space , Str "block" ]+ , Div+ ( "" , [] , [ ( "key" , "a value" ) ] )+ [ Div+ ( "" , [ "title" ] , [] )+ [ Para [ Str "A" , Space , Str "title." ] ]+ , Para+ [ Str "Any"+ , Space+ , Str "content"+ , Space+ , Str "can"+ , Space+ , Str "go"+ , Space+ , Str "here:"+ ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "one" ] ] , [ Para [ Str "two" ] ] ]+ ]+ , Header 2 ( "_anchor" , [] , [] ) [ Str "Anchor" ]+ , Div+ ( "goals" , [] , [ ( "wrapper" , "1" ) ] )+ [ BulletList+ [ [ Para [ Str "one" ] ] , [ Para [ Str "two" ] ] ]+ ]+ , Header 2 ( "_breaks" , [] , [] ) [ Str "Breaks" ]+ , Para+ [ Str "Asciidoc"+ , Space+ , Str "thematic"+ , Space+ , Str "break:"+ ]+ , HorizontalRule+ , Para [ Str "Markdown" , Space , Str "style:" ]+ , HorizontalRule+ , HorizontalRule+ , HorizontalRule+ , HorizontalRule+ , Para [ Str "Page" , Space , Str "breaks:" ]+ , Div+ ( "" , [ "page-break" ] , [ ( "wrapper" , "1" ) ] )+ [ HorizontalRule ]+ , Div+ ( ""+ , [ "page-break" ]+ , [ ( "options" , "always" ) , ( "wrapper" , "1" ) ]+ )+ [ HorizontalRule ]+ , Header 2 ( "_list" , [] , [] ) [ Str "List" ]+ , BulletList+ [ [ Para+ [ Str "Edgar" , Space , Str "Allan" , Space , Str "Poe" ]+ ]+ , [ Para+ [ Str "Sheri" , Space , Str "S." , Space , Str "Tepper" ]+ ]+ , [ Para [ Str "Bill" , Space , Str "Bryson" ] ]+ ]+ , Div+ ( "" , [] , [] )+ [ Div+ ( "" , [ "title" ] , [] )+ [ Para+ [ Str "Kizmet\8217s"+ , Space+ , Str "Favorite"+ , Space+ , Str "Authors"+ ]+ ]+ , BulletList+ [ [ Para+ [ Str "Edgar"+ , Space+ , Str "Allan"+ , Space+ , Str "Poe"+ ]+ ]+ , [ Para+ [ Str "Sheri"+ , Space+ , Str "S."+ , Space+ , Str "Tepper"+ ]+ ]+ , [ Para [ Str "Bill" , Space , Str "Bryson" ] ]+ ]+ ]+ , BulletList+ [ [ Para+ [ Str "Edgar" , Space , Str "Allan" , Space , Str "Poe" ]+ ]+ , [ Para+ [ Str "Sheri" , Space , Str "S." , Space , Str "Tepper" ]+ ]+ , [ Para [ Str "Bill" , Space , Str "Bryson" ] ]+ ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Nested" , Space , Str "list" ]+ , BulletList+ [ [ Para+ [ Str "West"+ , Space+ , Str "wood"+ , Space+ , Str "maze"+ ]+ , BulletList+ [ [ Para [ Str "Maze" , Space , Str "heart" ]+ , BulletList+ [ [ Para+ [ Str "Reflection" , Space , Str "pool" ]+ ]+ ]+ ]+ , [ Para [ Str "Secret" , Space , Str "exit" ] ]+ ]+ ]+ , [ Para+ [ Str "Level"+ , Space+ , Str "1"+ , Space+ , Str "list"+ , Space+ , Str "item"+ ]+ , BulletList+ [ [ Para+ [ Str "Level"+ , Space+ , Str "2"+ , Space+ , Str "list"+ , Space+ , Str "item"+ ]+ , BulletList+ [ [ Para+ [ Str "Level"+ , Space+ , Str "3"+ , Space+ , Str "list"+ , Space+ , Str "item"+ ]+ , BulletList+ [ [ Para+ [ Str "Level"+ , Space+ , Str "4"+ , Space+ , Str "list"+ , Space+ , Str "item"+ ]+ , BulletList+ [ [ Para+ [ Str "Level"+ , Space+ , Str "5"+ , Space+ , Str "list"+ , Space+ , Str "item"+ ]+ , BulletList+ [ [ Para [ Str "etc." ] ] ]+ ]+ ]+ ]+ ]+ ]+ ]+ ]+ ]+ ]+ , [ Para+ [ Str "Level"+ , Space+ , Str "1"+ , Space+ , Str "list"+ , Space+ , Str "item"+ ]+ ]+ ]+ ]+ ]+ , BulletList+ [ [ Para [ Str "one" ] ]+ , [ Para [ Str "two" ] ]+ , [ Para [ Str "three" ] ]+ ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Protons" ] ]+ , [ Para [ Str "Electrons" ] ]+ , [ Para [ Str "Neutrons" ] ]+ ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Protons" ] ]+ , [ Para [ Str "Electrons" ] ]+ , [ Para [ Str "Neutrons" ] ]+ ]+ , Para+ [ Str "Start" , Space , Str "with" , Space , Str "4:" ]+ , OrderedList+ ( 4 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Step" , Space , Str "four" ] ]+ , [ Para [ Str "Step" , Space , Str "five" ] ]+ , [ Para [ Str "Step" , Space , Str "six" ] ]+ ]+ , Para [ Str "or" ]+ , OrderedList+ ( 4 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Step" , Space , Str "four" ] ]+ , [ Para [ Str "Step" , Space , Str "five" ] ]+ , [ Para [ Str "Step" , Space , Str "six" ] ]+ ]+ , Para [ Str "Reversed:" ]+ , Div+ ( "" , [] , [ ( "options" , "reversed" ) ] )+ [ Div+ ( "" , [ "title" ] , [] )+ [ Para+ [ Str "Parts"+ , Space+ , Str "of"+ , Space+ , Str "an"+ , Space+ , Str "atom"+ ]+ ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Protons" ] ]+ , [ Para [ Str "Electrons" ] ]+ , [ Para [ Str "Neutrons" ] ]+ ]+ ]+ , Para [ Str "Nested" ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Step" , Space , Str "1" ] ]+ , [ Para [ Str "Step" , Space , Str "2" ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Step" , Space , Str "2a" ] ]+ , [ Para [ Str "Step" , Space , Str "2b" ] ]+ ]+ ]+ , [ Para [ Str "Step" , Space , Str "3" ] ]+ ]+ , Para [ Str "Mixed" , Space , Str "nested" ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Linux" ]+ , BulletList+ [ [ Para [ Str "Fedora" ] ]+ , [ Para [ Str "Ubuntu" ] ]+ , [ Para [ Str "Slackware" ] ]+ ]+ ]+ , [ Para [ Str "BSD" ]+ , BulletList+ [ [ Para [ Str "FreeBSD" ] ] , [ Para [ Str "NetBSD" ] ] ]+ ]+ ]+ , Para [ Str "With" , Space , Str "spacing" ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Linux" ]+ , BulletList+ [ [ Para [ Str "Fedora" ] ]+ , [ Para [ Str "Ubuntu" ] ]+ , [ Para [ Str "Slackware" ] ]+ ]+ ]+ , [ Para [ Str "BSD" ]+ , BulletList+ [ [ Para [ Str "FreeBSD" ] ] , [ Para [ Str "NetBSD" ] ] ]+ ]+ ]+ , Para+ [ Str "With" , Space , Str "number" , Space , Str "styles" ]+ , OrderedList+ ( 5 , LowerRoman , DefaultDelim )+ [ [ Para [ Str "Five" ] ]+ , [ Para [ Str "Six" ]+ , OrderedList+ ( 1 , LowerAlpha , DefaultDelim )+ [ [ Para [ Str "a" ] ]+ , [ Para [ Str "b" ] ]+ , [ Para [ Str "c" ] ]+ ]+ ]+ , [ Para [ Str "Seven" ] ]+ ]+ , Para [ Str "Checklist" ]+ , BulletList+ [ [ Para [ Str "\9746" , Space , Str "checked" ] ]+ , [ Para+ [ Str "\9746" , Space , Str "also" , Space , Str "checked" ]+ ]+ , [ Para+ [ Str "\9744" , Space , Str "not" , Space , Str "checked" ]+ ]+ , [ Para+ [ Str "normal" , Space , Str "list" , Space , Str "item" ]+ ]+ ]+ , Para+ [ Str "Separate"+ , Space+ , Str "lists"+ , Space+ , Str "with"+ , Space+ , Str "block"+ , Space+ , Str "attribute"+ ]+ , BulletList+ [ [ Para [ Str "Apples" ] ]+ , [ Para [ Str "Oranges" ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [ Str "Wash" ] ] , [ Para [ Str "Slice" ] ] ]+ ]+ ]+ , Para [ Str "Multiline" , Space , Str "items" ]+ , BulletList+ [ [ Para+ [ Str "Blah"+ , Space+ , Str "blah."+ , SoftBreak+ , Str "Blah"+ , Space+ , Str "blah."+ ]+ ]+ , [ Para+ [ Str "The"+ , Space+ , Str "document"+ , Space+ , Str "header"+ , Space+ , Str "in"+ , Space+ , Str "AsciiDoc"+ , Space+ , Str "is"+ , Space+ , Str "optional."+ , SoftBreak+ , Str "If"+ , Space+ , Str "present,"+ , Space+ , Str "it"+ , Space+ , Str "must"+ , Space+ , Str "start"+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Str "document"+ , Space+ , Str "title."+ ]+ ]+ ]+ , BulletList+ [ [ Para+ [ Str "Optional"+ , Space+ , Str "author"+ , Space+ , Str "and"+ , Space+ , Str "revision"+ , Space+ , Str "information"+ , Space+ , Str "lines"+ , SoftBreak+ , Str "immediately"+ , Space+ , Str "follow"+ , Space+ , Str "the"+ , Space+ , Str "document"+ , Space+ , Str "title."+ ]+ ]+ ]+ , BulletList+ [ [ Para+ [ Str "The"+ , Space+ , Str "document"+ , Space+ , Str "header"+ , Space+ , Str "must"+ , Space+ , Str "be"+ , Space+ , Str "separated"+ , Space+ , Str "from"+ , SoftBreak+ , Str "the"+ , Space+ , Str "remainder"+ , Space+ , Str "of"+ , Space+ , Str "the"+ , Space+ , Str "document"+ , Space+ , Str "by"+ , Space+ , Str "one"+ , Space+ , Str "or"+ , Space+ , Str "more"+ , SoftBreak+ , Str "empty"+ , Space+ , Str "lines"+ , Space+ , Str "and"+ , Space+ , Str "it"+ , Space+ , Str "cannot"+ , Space+ , Str "contain"+ , Space+ , Str "empty"+ , Space+ , Str "lines."+ ]+ ]+ ]+ , Para [ Str "Complex" , Space , Str "item" ]+ , BulletList+ [ [ Para+ [ Str "The"+ , Space+ , Str "header"+ , Space+ , Str "in"+ , Space+ , Str "AsciiDoc"+ , Space+ , Str "must"+ , Space+ , Str "start"+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Str "document"+ , Space+ , Str "title."+ ]+ , CodeBlock ( "" , [] , [] ) "= Document Title"+ , Para+ [ Str "Keep"+ , Space+ , Str "in"+ , Space+ , Str "mind"+ , Space+ , Str "that"+ , Space+ , Str "the"+ , Space+ , Str "header"+ , Space+ , Str "is"+ , Space+ , Str "optional."+ ]+ ]+ , [ Para+ [ Str "Optional"+ , Space+ , Str "author"+ , Space+ , Str "and"+ , Space+ , Str "revision"+ , Space+ , Str "information"+ , Space+ , Str "lines"+ , Space+ , Str "immediately"+ , Space+ , Str "follow"+ , Space+ , Str "the"+ , Space+ , Str "document"+ , SoftBreak+ , Str "title."+ ]+ , CodeBlock+ ( "" , [] , [] )+ "= Document Title\nDoc Writer <doc.writer@asciidoc.org>\nv1.0, 2022-01-01"+ ]+ , [ Para [ Str "Second" , Space , Str "item" ] ]+ ]+ , Para+ [ Str "Empty"+ , Space+ , Str "principle"+ , Space+ , Str "element:"+ ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Para [] , CodeBlock ( "" , [] , [] ) "test" ] ]+ , Header 2 ( "_table" , [] , [] ) [ Str "Table" ]+ , Header+ 3+ ( "_simple_with_column_specs" , [] , [] )+ [ Str "Simple"+ , Space+ , Str "with"+ , Space+ , Str "column"+ , Space+ , Str "specs"+ ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidth 0.375 )+ , ( AlignDefault , ColWidth 0.25 )+ , ( AlignDefault , ColWidth 0.375 )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "This"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "placed"+ , Space+ , Str "in"+ , Space+ , Str "the"+ , Space+ , Str "first"+ , Space+ , Str "cell"+ , Space+ , Str "of"+ , Space+ , Str "column"+ , Space+ , Str "1"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "This"+ , Space+ , Str "line"+ , Space+ , Str "starts"+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Str "vertical"+ , Space+ , Str "bar"+ , Space+ , Str "so"+ , Space+ , Str "this"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "placed"+ , Space+ , Str "in"+ , Space+ , Str "a"+ , Space+ , Str "new"+ , Space+ , Str "cell"+ , Space+ , Str "in"+ , SoftBreak+ , Str "column"+ , Space+ , Str "2"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "When"+ , Space+ , Str "the"+ , Space+ , Str "processor"+ , Space+ , Str "encounters"+ , Space+ , Str "a"+ , Space+ , Str "whitespace"+ , Space+ , Str "followed"+ , Space+ , Str "by"+ , Space+ , Str "a"+ , Space+ , Str "vertical"+ , Space+ , Str "bar"+ , Space+ , Str "it"+ , SoftBreak+ , Str "ends"+ , Space+ , Str "the"+ , Space+ , Str "previous"+ , Space+ , Str "cell"+ , Space+ , Str "and"+ , Space+ , Str "starts"+ , Space+ , Str "a"+ , Space+ , Str "new"+ , Space+ , Str "cell"+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_repeated_column_in_specs" , [] , [] )+ [ Str "Repeated"+ , Space+ , Str "column"+ , Space+ , Str "in"+ , Space+ , Str "specs"+ ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignRight+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Strong+ [ Str "This"+ , Space+ , Str "cell\8217s"+ , Space+ , Str "specifier"+ , Space+ , Str "indicates"+ , Space+ , Str "that"+ , Space+ , Str "this"+ , Space+ , Str "cell\8217s"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "right-aligned"+ , Space+ , Str "and"+ , Space+ , Str "bold."+ ]+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "The"+ , Space+ , Str "cell"+ , Space+ , Str "specifier"+ , Space+ , Str "on"+ , Space+ , Str "this"+ , Space+ , Str "cell"+ , Space+ , Str "hasn\8217t"+ , Space+ , Str "been"+ , Space+ , Str "set"+ , Space+ , Str "explicitly,"+ , Space+ , Str "so"+ , Space+ , Str "the"+ , Space+ , Str "default"+ , SoftBreak+ , Str "properties"+ , Space+ , Str "are"+ , Space+ , Str "applied."+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_simple_without_column_specs" , [] , [] )+ [ Str "Simple"+ , Space+ , Str "without"+ , Space+ , Str "column"+ , Space+ , Str "specs"+ ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead+ ( "" , [] , [] )+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "1,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "2,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ ]+ ])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "2,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1,"+ , Space+ , Str "row"+ , Space+ , Str "3"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "2,"+ , Space+ , Str "row"+ , Space+ , Str "3"+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_with_caption" , [] , [] )+ [ Str "With" , Space , Str "caption" ]+ , Table+ ( "" , [] , [] )+ (Caption+ Nothing+ [ Plain+ [ Str "My" , Space , Str "cool" , Space , Str "table." ]+ ])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead+ ( "" , [] , [] )+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "1,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "2,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ ]+ ])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "2,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1,"+ , Space+ , Str "row"+ , Space+ , Str "3"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "2,"+ , Space+ , Str "row"+ , Space+ , Str "3"+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_no_header" , [] , [] )+ [ Str "No" , Space , Str "header" ]+ , Para+ [ Str "By"+ , Space+ , Str "default"+ , Space+ , Str "the"+ , Space+ , Str "first"+ , Space+ , Str "line"+ , Space+ , Str "should"+ , Space+ , Str "turn"+ , Space+ , Str "into"+ , Space+ , Str "the"+ , Space+ , Str "header,"+ , Space+ , Str "but"+ , Space+ , Str "this"+ , SoftBreak+ , Str "can"+ , Space+ , Str "be"+ , Space+ , Str "disabled:"+ ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1,"+ , Space+ , Str "row"+ , Space+ , Str "1"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "2,"+ , Space+ , Str "row"+ , Space+ , Str "1"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "2,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Para+ [ Str "And"+ , Space+ , Str "also"+ , Space+ , Str "explicitly"+ , Space+ , Str "enabled:"+ ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Cell" , Space , Str "A1" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Cell" , Space , Str "B1" ] ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Cell" , Space , Str "A2" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Cell" , Space , Str "B2" ] ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Cell" , Space , Str "A3" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Cell" , Space , Str "B3" ] ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header 3 ( "_footer" , [] , [] ) [ Str "Footer" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidth 0.4 )+ , ( AlignDefault , ColWidth 0.4 )+ , ( AlignDefault , ColWidth 0.2 )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "1,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "2,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "3,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "2,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "3,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "1,"+ , Space+ , Str "footer"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "2,"+ , Space+ , Str "footer"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "3,"+ , Space+ , Str "footer"+ , Space+ , Str "row"+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Para [ Str "or" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "1,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "2,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "2,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1,"+ , Space+ , Str "row"+ , Space+ , Str "3"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "2,"+ , Space+ , Str "row"+ , Space+ , Str "3"+ ]+ ]+ ]+ ]+ ]+ (TableFoot+ ( "" , [] , [] )+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "1,"+ , Space+ , Str "footer"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "2,"+ , Space+ , Str "footer"+ , Space+ , Str "row"+ ]+ ]+ ]+ ])+ , Header 3 ( "_alignment" , [] , [] ) [ Str "Alignment" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Column" , Space , Str "Name" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Column" , Space , Str "Name" ] ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignCenter+ (RowSpan 1)+ (ColSpan 2)+ [ Plain+ [ Str "This"+ , Space+ , Str "cell"+ , Space+ , Str "spans"+ , Space+ , Str "two"+ , Space+ , Str "columns,"+ , Space+ , Str "and"+ , Space+ , Str "its"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "horizontally"+ , Space+ , Str "centered"+ , Space+ , Str "because"+ , Space+ , Str "the"+ , SoftBreak+ , Str "cell"+ , Space+ , Str "specifier"+ , Space+ , Str "includes"+ , Space+ , Str "the"+ , Space+ , Code ( "" , [] , [] ) "^"+ , Space+ , Str "operator."+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignCenter+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "This"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "duplicated"+ , Space+ , Str "in"+ , Space+ , Str "two"+ , Space+ , Str "adjacent"+ , Space+ , Str "columns."+ , SoftBreak+ , Str "Its"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "horizontally"+ , Space+ , Str "centered"+ , Space+ , Str "because"+ , Space+ , Str "the"+ , Space+ , Str "cell"+ , Space+ , Str "specifier"+ , SoftBreak+ , Str "includes"+ , Space+ , Str "the"+ , Space+ , Code ( "" , [] , [] ) "^"+ , Space+ , Str "operator."+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignCenter+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "This"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "duplicated"+ , Space+ , Str "in"+ , Space+ , Str "two"+ , Space+ , Str "adjacent"+ , Space+ , Str "columns."+ , SoftBreak+ , Str "Its"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "horizontally"+ , Space+ , Str "centered"+ , Space+ , Str "because"+ , Space+ , Str "the"+ , Space+ , Str "cell"+ , Space+ , Str "specifier"+ , SoftBreak+ , Str "includes"+ , Space+ , Str "the"+ , Space+ , Code ( "" , [] , [] ) "^"+ , Space+ , Str "operator."+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_multiple_paragraphs_in_cells" , [] , [] )+ [ Str "Multiple"+ , Space+ , Str "paragraphs"+ , Space+ , Str "in"+ , Space+ , Str "cells"+ ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault ) ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Para+ [ Str "Single"+ , Space+ , Str "paragraph"+ , Space+ , Str "on"+ , Space+ , Str "row"+ , Space+ , Str "1"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Para+ [ Str "First"+ , Space+ , Str "paragraph"+ , Space+ , Str "on"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ , Para+ [ Str "Second"+ , Space+ , Str "paragraph"+ , Space+ , Str "on"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_complex_table" , [] , [] )+ [ Str "Complex" , Space , Str "table" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignRight+ (RowSpan 1)+ (ColSpan 1)+ [ Para+ [ Code ( "" , [] , [] ) "This"+ , Space+ , Code ( "" , [] , [] ) "content"+ , Space+ , Code ( "" , [] , [] ) "is"+ , Space+ , Code ( "" , [] , [] ) "duplicated"+ , Space+ , Code ( "" , [] , [] ) "across"+ , Space+ , Code ( "" , [] , [] ) "two"+ , Space+ , Code ( "" , [] , [] ) "columns."+ ]+ , Para+ [ Code ( "" , [] , [] ) "It"+ , Space+ , Code ( "" , [] , [] ) "is"+ , Space+ , Code ( "" , [] , [] ) "aligned"+ , Space+ , Code ( "" , [] , [] ) "right"+ , Space+ , Code ( "" , [] , [] ) "horizontally."+ ]+ , Para+ [ Code ( "" , [] , [] ) "And"+ , Space+ , Code ( "" , [] , [] ) "it"+ , Space+ , Code ( "" , [] , [] ) "is"+ , Space+ , Code ( "" , [] , [] ) "monospaced."+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignRight+ (RowSpan 1)+ (ColSpan 1)+ [ Para+ [ Code ( "" , [] , [] ) "This"+ , Space+ , Code ( "" , [] , [] ) "content"+ , Space+ , Code ( "" , [] , [] ) "is"+ , Space+ , Code ( "" , [] , [] ) "duplicated"+ , Space+ , Code ( "" , [] , [] ) "across"+ , Space+ , Code ( "" , [] , [] ) "two"+ , Space+ , Code ( "" , [] , [] ) "columns."+ ]+ , Para+ [ Code ( "" , [] , [] ) "It"+ , Space+ , Code ( "" , [] , [] ) "is"+ , Space+ , Code ( "" , [] , [] ) "aligned"+ , Space+ , Code ( "" , [] , [] ) "right"+ , Space+ , Code ( "" , [] , [] ) "horizontally."+ ]+ , Para+ [ Code ( "" , [] , [] ) "And"+ , Space+ , Code ( "" , [] , [] ) "it"+ , Space+ , Code ( "" , [] , [] ) "is"+ , Space+ , Code ( "" , [] , [] ) "monospaced."+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignCenter+ (RowSpan 3)+ (ColSpan 1)+ [ Para+ [ Strong+ [ Str "This"+ , Space+ , Str "cell"+ , Space+ , Str "spans"+ , Space+ , Str "3"+ , Space+ , Str "rows."+ , Space+ , Str "The"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "centered"+ , Space+ , Str "horizontally,"+ , Space+ , Str "aligned"+ , Space+ , Str "to"+ , Space+ , Str "the"+ , Space+ , Str "bottom"+ , Space+ , Str "of"+ , Space+ , Str "the"+ , Space+ , Str "cell,"+ , Space+ , Str "and"+ , Space+ , Str "strong."+ ]+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Para+ [ Emph+ [ Str "This"+ , Space+ , Str "content"+ , Space+ , Str "is"+ , Space+ , Str "emphasized."+ ]+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ CodeBlock+ ( "" , [] , [] )+ "This content is aligned to the top of the cell and literal.\n\n"+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ CodeBlock+ ( "" , [] , [] )+ "puts \"This is a source block!\""+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_column_styles" , [] , [] )+ [ Str "Column" , Space , Str "styles" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Code ( "" , [] , [] ) "monospace" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Code ( "" , [] , [] ) "mono" ] ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "default" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Code ( "" , [] , [] ) "mono" ] ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_block_elements_in_cells" , [] , [] )+ [ Str "Block"+ , Space+ , Str "elements"+ , Space+ , Str "in"+ , Space+ , Str "cells"+ ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Para [ Str "Normal" , Space , Str "Style" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Para [ Str "AsciiDoc" , Space , Str "Style" ] ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Para+ [ Str "This"+ , Space+ , Str "cell"+ , Space+ , Str "isn\8217t"+ , Space+ , Str "prefixed"+ , Space+ , Str "with"+ , Space+ , Str "an"+ , Space+ , Code ( "" , [] , [] ) "a"+ , Str ","+ , Space+ , Str "so"+ , Space+ , Str "the"+ , Space+ , Str "processor"+ , Space+ , Str "doesn\8217t"+ , Space+ , Str "interpret"+ , Space+ , Str "the"+ , SoftBreak+ , Str "following"+ , Space+ , Str "lines"+ , Space+ , Str "as"+ , Space+ , Str "an"+ , Space+ , Str "AsciiDoc"+ , Space+ , Str "list."+ ]+ , Para+ [ Str "*"+ , Space+ , Str "List"+ , Space+ , Str "item"+ , Space+ , Str "1"+ , SoftBreak+ , Str "*"+ , Space+ , Str "List"+ , Space+ , Str "item"+ , Space+ , Str "2"+ , SoftBreak+ , Str "*"+ , Space+ , Str "List"+ , Space+ , Str "item"+ , Space+ , Str "3"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Para+ [ Str "This"+ , Space+ , Str "cell"+ , Space+ , Str "is"+ , Space+ , Str "prefixed"+ , Space+ , Str "with"+ , Space+ , Str "an"+ , Space+ , Code ( "" , [] , [] ) "a"+ , Str ","+ , Space+ , Str "so"+ , Space+ , Str "the"+ , Space+ , Str "processor"+ , Space+ , Str "interprets"+ , Space+ , Str "the"+ , Space+ , Str "following"+ , Space+ , Str "lines"+ , SoftBreak+ , Str "as"+ , Space+ , Str "an"+ , Space+ , Str "AsciiDoc"+ , Space+ , Str "list."+ ]+ , BulletList+ [ [ Para+ [ Str "List"+ , Space+ , Str "item"+ , Space+ , Str "1"+ ]+ ]+ , [ Para+ [ Str "List"+ , Space+ , Str "item"+ , Space+ , Str "2"+ ]+ ]+ , [ Para+ [ Str "List"+ , Space+ , Str "item"+ , Space+ , Str "3"+ ]+ ]+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Para+ [ Str "This"+ , Space+ , Str "cell"+ , Space+ , Str "isn\8217t"+ , Space+ , Str "prefixed"+ , Space+ , Str "with"+ , Space+ , Str "an"+ , Space+ , Code ( "" , [] , [] ) "a"+ , Str ","+ , Space+ , Str "so"+ , Space+ , Str "the"+ , Space+ , Str "processor"+ , Space+ , Str "doesn\8217t"+ , Space+ , Str "interpret"+ , Space+ , Str "the"+ , Space+ , Str "listing"+ , SoftBreak+ , Str "block"+ , Space+ , Str "delimiters"+ , Space+ , Str "or"+ , Space+ , Str "the"+ , Space+ , Code ( "" , [] , [] ) "source"+ , Space+ , Str "style."+ ]+ , Para+ [ Str "----"+ , SoftBreak+ , Str "import"+ , Space+ , Str "os"+ , SoftBreak+ , Str "print"+ , Space+ , Str "(\"%s\""+ , Space+ , Str "%(os.uname()))"+ , SoftBreak+ , Str "----"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Para+ [ Str "This"+ , Space+ , Str "cell"+ , Space+ , Str "is"+ , Space+ , Str "prefixed"+ , Space+ , Str "with"+ , Space+ , Str "an"+ , Space+ , Code ( "" , [] , [] ) "a"+ , Str ","+ , Space+ , Str "so"+ , Space+ , Str "the"+ , Space+ , Str "listing"+ , Space+ , Str "block"+ , Space+ , Str "is"+ , Space+ , Str "processed"+ , Space+ , Str "and"+ , Space+ , Str "rendered"+ , SoftBreak+ , Str "according"+ , Space+ , Str "to"+ , Space+ , Str "the"+ , Space+ , Code ( "" , [] , [] ) "source"+ , Space+ , Str "style"+ , Space+ , Str "rules."+ ]+ , CodeBlock+ ( "" , [ "python" ] , [] )+ "import os\nprint \"%s\" %(os.uname())"+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_col_and_rowspan" , [] , [] )+ [ Str "Col" , Space , Str "and" , Space , Str "rowspan" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "1,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "2,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Column"+ , Space+ , Str "3,"+ , Space+ , Str "header"+ , Space+ , Str "row"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 2)+ (ColSpan 2)+ [ Plain+ [ Str "This"+ , Space+ , Str "cell"+ , Space+ , Str "spans"+ , Space+ , Str "2"+ , Space+ , Str "cols"+ , Space+ , Str "and"+ , Space+ , Str "2"+ , Space+ , Str "rows"+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "3,"+ , Space+ , Str "row"+ , Space+ , Str "2"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "3,"+ , Space+ , Str "row"+ , Space+ , Str "3"+ ]+ ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 3)+ [ Plain+ [ Str "Cell"+ , Space+ , Str "in"+ , Space+ , Str "column"+ , Space+ , Str "1-3,"+ , Space+ , Str "row"+ , Space+ , Str "4"+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_csv_table" , [] , [] )+ [ Str "CSV" , Space , Str "table" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead+ ( "" , [] , [] )+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Artist" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Track" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Genre" ] ]+ ]+ ])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Baauer" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Harlem" , Space , Str "Shake" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Hip" , Space , Str "Hop" ] ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "The" , Space , Str "Lumineers" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Ho" , Space , Str "Hey" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Folk" , Space , Str "Rock" ] ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Para [ Str "or" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Artist" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Track" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Genre" ] ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Baauer" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Harlem" , Space , Str "Shake" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Hip" , Space , Str "Hop" ] ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Header+ 3+ ( "_dsv_table" , [] , [] )+ [ Str "DSV" , Space , Str "table" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "a" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "b" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "c" ] ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "d" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "e" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "f" ] ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+ , Para [ Str "or" ]+ , Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Artist" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Track" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Genre" ] ]+ ]+ , Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Robyn" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Indestructible" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "Dance" ] ] ] ] ]
@@ -100,7 +100,7 @@ >{\raggedright\arraybackslash}p{(\linewidth - 6\tabcolsep) * \real{0.2078}}@{}} \toprule\noalign{} \begin{minipage}[b]{\linewidth}\centering-Name\footnotemark{}+Name\pandocfootnotemark{1} \end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright Capital \end{minipage} & \begin{minipage}[b]{\linewidth}\raggedright@@ -113,7 +113,7 @@ \midrule\noalign{} Denmark & Copenhagen & 5,809,502 & 43,094 \\ Finland & Helsinki & 5,537,364 & 338,145 \\-Iceland\footnotemark{} & Reykjavik & 343,518 & 103,000 \\+Iceland\pandocfootnotemark{2} & Reykjavik & 343,518 & 103,000 \\ Norway & Oslo & 5,372,191 & 323,802 \\ Sweden & Stockholm & 10,313,447 & 450,295 \\ \midrule\noalign{}@@ -121,9 +121,7 @@ \bottomrule\noalign{} \end{tabular} \end{table}-\addtocounter{footnote}{-1}-\footnotetext{In English.}-\addtocounter{footnote}{1}-\footnotetext{Brrr.}+\pandocfootnotetext{1}{In English.}+\pandocfootnotetext{2}{Brrr.} ```
@@ -11,12 +11,12 @@ <caption>Overview of basic table markup</caption> <thead> <tr>-<th><p>Key</p></th>+<th>Key</th> </tr> </thead> <tbody> <tr>-<td><p>Value</p></td>+<td>Value</td> </tr> </tbody> </table>
@@ -18,31 +18,31 @@ <table> <thead> <tr>-<th style=""><p>Witness program version</p></th>-<th colspan="4" style=""><p>Hash size</p></th>+<th style="">Witness program version</th>+<th colspan="4" style="">Hash size</th> </tr> </thead> <tbody> <tr>-<td><p>Mainnet</p></td>-<td><p>Testnet</p></td>-<td><p>Mainnet</p></td>-<td><p>Testnet</p></td>+<td>Mainnet</td>+<td>Testnet</td>+<td>Mainnet</td>+<td>Testnet</td> <td></td> </tr> <tr>-<td><p>0</p></td>-<td><p>p2</p></td>-<td><p>QW</p></td>-<td><p>7Xh</p></td>-<td><p>T7n</p></td>+<td>0</td>+<td>p2</td>+<td>QW</td>+<td>7Xh</td>+<td>T7n</td> </tr> <tr>-<td><p>1</p></td>-<td><p>p4</p></td>-<td><p>QY</p></td>-<td><p>7Xq</p></td>-<td><p>T7w</p></td>+<td>1</td>+<td>p4</td>+<td>QY</td>+<td>7Xq</td>+<td>T7w</td> </tr> </tbody> </table>
@@ -1,5 +1,5 @@ ```-% pandoc -f html -t native+% pandoc -f html+raw_html -t native <p>A<style></style>B</p> ^D [ Para@@ -8,4 +8,14 @@ , Str "B" ] ]+```++The style element's contents should not be parsed as text+even when raw_html is disabled:++```+% pandoc -f html -t native+<p>A<style>p { color: red; }</style>B</p>+^D+[ Para [ Str "AB" ] ] ```
@@ -0,0 +1,10 @@+```+% pandoc -f latex -t native 2>&1+\makeatletter+\def\small{\@setfontsize\small\@ixpt{10}}+\makeatother+\small test+^D+Loop encountered in expanding macro small+=> 91+```
binary file changed (14206 → 3166 bytes)
@@ -9,7 +9,6 @@ \end{eqnarray} $$ ^D- \begin{eqnarray} S &\rightarrow& a A \nonumber \\ A &\rightarrow& d B \ | \ b A \ | \ c A \nonumber \\
@@ -24,15 +24,15 @@ </thead> <tbody> <tr>-<td>1</td>-<td>Second column of row 1.</td>+<td><p>1</p></td>+<td><p>Second column of row 1.</p></td> </tr> <tr>-<td>2</td>-<td>Second column of row 2. Second line of paragraph.</td>+<td><p>2</p></td>+<td><p>Second column of row 2. Second line of paragraph.</p></td> </tr> <tr>-<td>3</td>+<td><p>3</p></td> <td><ul> <li>Second column of row 3.</li> <li>Second item in bullet list (row 3, column 2).</li>@@ -40,7 +40,7 @@ </tr> <tr> <td></td>-<td>Row 4; column 1 will be empty.</td>+<td><p>Row 4; column 1 will be empty.</p></td> </tr> </tbody> </table>
binary file changed (15080 → 4154 bytes)
binary file changed (10947 → 5711 bytes)
@@ -0,0 +1,39 @@+A heading's bookmark is written inside the heading's paragraph, around its+text, rather than around its whole section, so a link to the heading+survives a docx round trip, and a screen reader meets the bookmark at the+heading (#11845).++```+% pandoc -f markdown -t docx -o - | pandoc -f docx -t native+# Costs {#price}++Back to [costs](#price), on to [later](#later-part).++# Later Part++End.+^D+[ Header 1 ( "costs" , [] , [] ) [ Str "Costs" ]+, Para+ [ Str "Back"+ , Space+ , Str "to"+ , Space+ , Link ( "" , [] , [] ) [ Str "costs" ] ( "#costs" , "" )+ , Str ","+ , Space+ , Str "on"+ , Space+ , Str "to"+ , Space+ , Link+ ( "" , [] , [] ) [ Str "later" ] ( "#later-part" , "" )+ , Str "."+ ]+, Header+ 1+ ( "later-part" , [] , [] )+ [ Str "Later" , Space , Str "Part" ]+, Para [ Str "End." ]+]+```
@@ -0,0 +1,42 @@+```+% pandoc -t typst+| A | B |+|---|---|+| 1 | 2 |++: Example {#table-id}+^D+#figure(+ align(center)[#table(+ columns: 2,+ align: (auto,auto,),+ table.header([A], [B],),+ table.hline(),+ [1], [2],+ )]+ , caption: [Example]+ , kind: table+ )+<table-id>++```++```+% pandoc -t typst+| A | B |+|---|---|+| 1 | 2 |++: Example {#table-id .typst:no-figure}+^D+#table(+ columns: 2,+ align: (auto,auto,),+ table.header([A], [B],),+ table.hline(),+ [1], [2],+)+<table-id>++```+
@@ -0,0 +1,33 @@+```+% pandoc -f html -t native+<p>a<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a></p>+<section id="footnotes" class="footnotes" role="doc-endnotes"><hr /><ol>+<li id="fn1"><p>note</p><ol><li>one</li><li>two</li></ol>+<a href="#fnref1" class="footnote-back" role="doc-backlink">x</a></li></ol></section>+^D+[ Para+ [ Str "a"+ , Note+ [ Para [ Str "note" ]+ , OrderedList+ ( 1 , DefaultStyle , DefaultDelim )+ [ [ Plain [ Str "one" ] ] , [ Plain [ Str "two" ] ] ]+ ]+ ]+]+```++```+% pandoc -f html -t native+<p>a<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a></p>+<section id="footnotes" class="footnotes" role="doc-endnotes"><hr /><ol>+<li id="fn1"><p>one</p><hr /><p>two</p>+<a href="#fnref1" class="footnote-back" role="doc-backlink">x</a></li></ol></section>+^D+[ Para+ [ Str "a"+ , Note+ [ Para [ Str "one" ] , HorizontalRule , Para [ Str "two" ] ]+ ]+]+```
@@ -0,0 +1,11 @@+```+% pandoc -f markdown+rebase_relative_paths -t html+[Zotero item](zotero://select/library/items/ABCD1234)+[Mail](mailto:a@b.c)+^D+<p><a href="zotero://select/library/items/ABCD1234">Zotero item</a> <a+href="mailto:a@b.c">Mail</a></p>++```++
@@ -0,0 +1,8 @@+A link's title is written to docx as a ScreenTip (`w:tooltip`) and read back.++```+% pandoc -f markdown -t docx -o - | pandoc -f docx -t markdown+See [the report](https://example.org/r.pdf "Annual report, PDF, 2 MB").+^D+See [the report](https://example.org/r.pdf "Annual report, PDF, 2 MB").+```
@@ -0,0 +1,46 @@+```+% pandoc -f markdown+rebase_relative_paths command/11888/section1/lab.md command/11888/section2/lab.md command/11888/section3/lab.md+^D+<h1 id="heading-1">Heading 1</h1>+<figure>+<img src="command/11888/section1/media/Pic1_1.png" alt="pic 1" />+<figcaption aria-hidden="true">pic 1</figcaption>+</figure>+<blockquote>+<p>a blockquote</p>+<figure>+<img src="command/11888/section1/media/Pic1_2.png" alt="pic 2" />+<figcaption aria-hidden="true">pic 2</figcaption>+</figure>+<p>with an image reference</p>+<figure>+<img src="command/11888/section1/media/Pic1_3.png" alt="pic 3" />+<figcaption aria-hidden="true">pic 3</figcaption>+</figure>+</blockquote>+<h1 id="heading-2">Heading 2</h1>+<figure>+<img src="command/11888/section2/media/Pic2_1.png" alt="pic 3" />+<figcaption aria-hidden="true">pic 3</figcaption>+</figure>+<ol type="1">+<li><p>A list</p>+<figure>+<img src="command/11888/section2/media/Pic2_2.png" alt="pic 4" />+<figcaption aria-hidden="true">pic 4</figcaption>+</figure></li>+</ol>+<h1 id="heading-3">Heading 3</h1>+<figure>+<img src="command/11888/section3/media/Pic3_1.png" alt="pic 5" />+<figcaption aria-hidden="true">pic 5</figcaption>+</figure>+<ol type="1">+<li><p>A list</p>+<figure>+<img src="command/11888/section3/media/Pic3_2.png" alt="pic 6" />+<figcaption aria-hidden="true">pic 6</figcaption>+</figure></li>+</ol>++```
@@ -0,0 +1,11 @@+# Heading 1++++> a blockquote+>+> +>+> with an image reference+>+> 
@@ -0,0 +1,8 @@+# Heading 2++++1. A list++ +
@@ -0,0 +1,7 @@+# Heading 3++++1. A list+ + 
@@ -0,0 +1,67 @@+```+% pandoc -f typst -t native+One cluster: @a @b.++Split by a newline:+@c+@d.+^D+[ Para+ [ Str "One"+ , Space+ , Str "cluster:"+ , Space+ , Cite+ [ Citation+ { citationId = "a"+ , citationPrefix = []+ , citationSuffix = []+ , citationMode = NormalCitation+ , citationNoteNum = 0+ , citationHash = 0+ }+ , Citation+ { citationId = "b"+ , citationPrefix = []+ , citationSuffix = []+ , citationMode = NormalCitation+ , citationNoteNum = 0+ , citationHash = 0+ }+ ]+ [ Str "[a]" , Str "[b]" ]+ , Str "."+ ]+, Para+ [ Str "Split"+ , Space+ , Str "by"+ , Space+ , Str "a"+ , Space+ , Str "newline:"+ , SoftBreak+ , Cite+ [ Citation+ { citationId = "c"+ , citationPrefix = []+ , citationSuffix = []+ , citationMode = NormalCitation+ , citationNoteNum = 0+ , citationHash = 0+ }+ , Citation+ { citationId = "d"+ , citationPrefix = []+ , citationSuffix = []+ , citationMode = NormalCitation+ , citationNoteNum = 0+ , citationHash = 0+ }+ ]+ [ Str "[c]" , Str "[d]" ]+ , Str "."+ ]+]++```
@@ -0,0 +1,6 @@+```+% pandoc -f native -t commonmark+[ Para [ Str "a\\(b\\)c" , Space , Str "\\*x" ] ]+^D+a\\(b\\)c \\\*x+```
@@ -0,0 +1,139 @@+Footnotes in tables are deferred. Each `\pandocfootnotetext` must+be matched to its `\pandocfootnotemark`, and the text of a nested+note must come after the text containing its mark.++```+% pandoc -f native -t latex+[ Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead+ ( "" , [] , [] )+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "A" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "B" ] ]+ ]+ ])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain+ [ Str "x"+ , Note+ [ Para+ [ Str "outer"+ , Space+ , Note [ Para [ Str "inner note" ] ]+ ]+ ]+ ]+ ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "y" , Note [ Para [ Str "second" ] ] ] ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+]+^D+{\def\LTcaptype{none} % do not increment counter+\begin{longtable}[]{@{}ll@{}}+\toprule\noalign{}+A & B \\+\midrule\noalign{}+\endhead+\bottomrule\noalign{}+\endlastfoot+x\pandocfootnotemark{2} & y\pandocfootnotemark{3} \\+\end{longtable}+}+\pandocfootnotetext{2}{outer \pandocfootnotemark{1}}+\pandocfootnotetext{1}{inner note}+\pandocfootnotetext{3}{second}+```++The LaTeX reader handles this output, ignoring the definitions of+the macros from the template:++```+% pandoc -f latex -t markdown+\makeatletter+\protected\def\pandocfootnotemark#1{\footnotemark{}%+ \expandafter\xdef\csname pandoc@fn@#1\endcsname{%+ \noexpand\pandoc@fnrestore+ {\unexpanded\expandafter{\@thefnmark}}%+ {\ifdefined\Hy@footnote@currentHref+ \unexpanded\expandafter{\Hy@footnote@currentHref}\fi}}}+\def\pandoc@fnrestore#1#2{%+ \gdef\@thefnmark{#1}%+ \ifdefined\Hy@footnote@currentHref\gdef\Hy@footnote@currentHref{#2}\fi}+\newcommand\pandocfootnotetext[1]{%+ \csname pandoc@fn@#1\endcsname\@footnotetext}+\makeatother++\begin{tabular}{ll}+x\pandocfootnotemark{2} & y\pandocfootnotemark{3} \\+\end{tabular}+\pandocfootnotetext{2}{outer \pandocfootnotemark{1}}+\pandocfootnotetext{1}{inner note}+\pandocfootnotetext{3}{second}+^D+ ------- -------+ x[^1] y[^2]+ ------- -------++[^1]: outer [^3]++[^2]: second++[^3]: inner note+```++It also handles adjustments to the footnote counter with+`\footnotemark` and `\footnotetext`, as in output from+earlier versions of pandoc:++```+% pandoc -f latex -t markdown+\begin{tabular}{ll}+x\footnotemark{} & y\footnotemark{} \\+\end{tabular}+\addtocounter{footnote}{-1}+\footnotetext{first}+\addtocounter{footnote}{1}+\footnotetext{second}+^D+ ------- -------+ x[^1] y[^2]+ ------- -------++[^1]: first++[^2]: second+```
@@ -0,0 +1,19 @@+```+% pandoc -f native -t markdown+[ Table ("", [], [])+ (Caption Nothing [Plain [Str "Caption"]])+ [(AlignDefault, ColWidth 1)]+ (TableHead ("", [], [])+ [Row ("", [], [])+ [Cell ("", [], []) AlignDefault (RowSpan 1) (ColSpan 1)+ [Plain [Str "x"]]]])+ [TableBody ("", [], []) (RowHeadColumns 0) [] []]+ (TableFoot ("", [], []) [])+]+^D++-----------------------------------------------------------------------++| x |++=======================================================================+++: Caption+```
@@ -0,0 +1,1 @@+var sel = '#main' & 1; // </script>
@@ -0,0 +1,14 @@+Characters with special meaning in a URI must be percent-encoded in a+textual `data:` URI. In particular a bare `#` would truncate the+payload at the fragment delimiter, and bare `&` or `'` would be+HTML-escaped into entities by the serializer.++(The `</script>` in the resource is what forces the `data:` URI path+rather than inlining the script contents.)++```+% pandoc --embed-resources+<script src="command/11942.js"></script>+^D+<script src="data:text/javascript;charset=utf-8,var%20sel%20%3D%20%27%23main%27%20%26%201%3B%20%2F%2F%20%3C%2Fscript%3E%0A"></script>+```
binary file changed (absent → 4132 bytes)
@@ -0,0 +1,14 @@+A `w:fldSimple` keeps its result text; a REF field with `\h` becomes a+link to the bookmark it refers to. A caption whose label is not `Figure`+or `Table` keeps its label and number.++```+% pandoc command/11944.docx -t html+^D+<p>See figure <a href="#_Ref1">1</a>.</p>+<figure>+<img src="media/image1.png" style="width:1.09702in;height:1.09702in" />+<figcaption><p><span id="_Ref1" class="anchor"></span>Figur 1 Blue+square.</p></figcaption>+</figure>+```
@@ -0,0 +1,34 @@+A table with more columns than `columns / 8` is still written as a grid+table, rather than falling back to raw HTML.++```+% pandoc -f html -t markdown-simple_tables-multiline_tables-pipe_tables+<table>+<tr><th>H1</th><th>H2</th><th>H3</th><th>H4</th><th>H5</th><th>H6</th><th>H7</th><th>H8</th><th>H9</th><th>H10</th></tr>+<tr><td>c1</td><td>c2</td><td>c3</td><td>c4</td><td>c5</td><td>c6</td><td>c7</td><td>c8</td><td>c9</td><td>c10</td></tr>+</table>+^D++----+----+----+----+----+----+----+----+----+-----++| H1 | H2 | H3 | H4 | H5 | H6 | H7 | H8 | H9 | H10 |++====+====+====+====+====+====+====+====+====+=====++| c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10 |++----+----+----+----+----+----+----+----+----+-----++```++This includes a table with block content in a cell, which has no other+markdown representation.++```+% pandoc -f html -t markdown+<table>+<tr><th>H1</th><th>H2</th><th>H3</th><th>H4</th><th>H5</th><th>H6</th><th>H7</th><th>H8</th><th>H9</th><th>H10</th></tr>+<tr><td><ul><li>a</li><li>b</li></ul></td><td>c2</td><td>c3</td><td>c4</td><td>c5</td><td>c6</td><td>c7</td><td>c8</td><td>c9</td><td>c10</td></tr>+</table>+^D++------+------+------+------+------+------+------+------+------+------++| H1 | H2 | H3 | H4 | H5 | H6 | H7 | H8 | H9 | H10 |++======+======+======+======+======+======+======+======+======+======++| - a | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10 |+| - b | | | | | | | | | |++------+------+------+------+------+------+------+------+------+------++```
@@ -0,0 +1,12 @@+```+% pandoc -t latex+key = value+: Definition+^D+\begin{description}+\tightlist+\item[{key = value}]+Definition+\end{description}++```
@@ -0,0 +1,8 @@+```+% pandoc -f html -t docx -o - | pandoc -f docx -t html+<p><em>Emph <em>in emph <em>flip</em> flops</em></em> and more</p>+^D+<p><em>Emph</em> in emph <em>flip</em> flops and more</p>++```+
@@ -0,0 +1,6 @@+```+% pandoc -f native -t typst+[Str "-?", Space, Str "...", Space, Str "a-b."]+^D+\-\? \.\.\. a-b.+```
@@ -0,0 +1,14 @@+```+% pandoc -f native -t rtf | pandoc -f rtf -t native+[ Para [ Str "abc\8217def" ]+, Para [ Str "\8220abc\8221" , Space , Str "\8216abc\8217" ]+, Para [ Str "abc\8217def" ]+, Para [ Str "abc\8212def" ]+]+^D+[ Para [ Str "abc\8217def" ]+, Para [ Str "\8220abc\8221" , Space , Str "\8216abc\8217" ]+, Para [ Str "abc\8217def" ]+, Para [ Str "abc\8212def" ]+]+```
@@ -0,0 +1,9 @@+If no class matches a language, we just use the first one.+````+% pandoc -f native -t markdown+[ CodeBlock ( "" , [ "text" ] , [] ) "x" ]+^D+``` text+x+```+````
binary file changed (absent → 1000 bytes)
@@ -0,0 +1,25 @@+Every underline type except `none` is read as underlined text, not+only `single`.++```+% pandoc command/11988.docx -t native+^D+[ Para+ [ Underline+ [ Str "single"+ , Space+ , Str "double"+ , Space+ , Str "thick"+ , Space+ , Str "dotted"+ , Space+ , Str "wave"+ , Space+ , Str "words"+ ]+ , Space+ , Str "none"+ ]+]+```
binary file changed (absent → 970 bytes)
@@ -0,0 +1,19 @@+A `w:gridCol` need not have a width (`w:w`). Such a column is kept,+and since its width is missing, every column gets the default width.++```+% pandoc command/11989.docx -t html+^D+<table>+<tbody>+<tr>+<td>a</td>+<td>b</td>+</tr>+<tr>+<td>c</td>+<td>d</td>+</tr>+</tbody>+</table>+```
binary file changed (absent → 2687 bytes)
@@ -0,0 +1,37 @@+Notes and comments are read with the namespace prefixes declared in+their own parts (word/footnotes.xml, word/endnotes.xml,+word/comments.xml). Here word/document.xml declares only `w`; the+footnote's hyperlink uses `r`, and the equations in the endnote and+the comment use `m`.++```+% pandoc command/11991.docx -t native --track-changes=all+^D+[ Para+ [ Span+ ( ""+ , [ "comment-start" ]+ , [ ( "comment-id" , "0" )+ , ( "author" , "A" )+ , ( "date" , "2026-01-01T00:00:00Z" )+ ]+ )+ [ Str "Math:" , Space , Math InlineMath "x" ]+ , Str "Text"+ , Span+ ( "" , [ "comment-end" ] , [ ( "comment-id" , "0" ) ] ) []+ , Note+ [ Para+ [ Str "See"+ , Space+ , Link+ ( "" , [] , [] )+ [ Str "example" ]+ ( "https://example.com/" , "" )+ ]+ ]+ , Note+ [ Para [ Str "Math:" , Space , Math InlineMath "x" ] ]+ ]+]+```
@@ -0,0 +1,68 @@+```+% pandoc -f markdown+rebase_relative_paths -t native command/12001/note/doc.md command/12001/next/doc.md command/12001/grid/doc.md command/12001/next/doc.md+^D+[ Para+ [ Str "Text"+ , Note+ [ Para+ [ Image+ ( "" , [] , [] )+ [ Str "C" ]+ ( "command/12001/note/img/note.png" , "" )+ , Space+ , Link+ ( "" , [] , [] )+ [ Str "doc" ]+ ( "command/12001/note/files/doc.pdf" , "" )+ ]+ ]+ ]+, Para+ [ Str "Next"+ , Space+ , Image+ ( "" , [] , [] )+ [ Str "N" ]+ ( "command/12001/next/img/next.png" , "" )+ ]+, Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidth 0.3194444444444444 ) ]+ (TableHead ( "" , [] , [] ) [])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Figure+ ( "" , [] , [] )+ (Caption Nothing [ Plain [ Str "C" ] ])+ [ Plain+ [ Image+ ( "" , [] , [] )+ [ Str "C" ]+ ( "command/12001/grid/img/grid.png" , "" )+ ]+ ]+ ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+, Para+ [ Str "Next"+ , Space+ , Image+ ( "" , [] , [] )+ [ Str "N" ]+ ( "command/12001/next/img/next.png" , "" )+ ]+]+```
@@ -0,0 +1,3 @@++----------------------++|  |++----------------------+
@@ -0,0 +1,1 @@+Next 
@@ -0,0 +1,3 @@+Text[^1]++[^1]:  [doc](files/doc.pdf)
@@ -0,0 +1,10 @@+```+% pandoc -f markdown -t native+\itshape+hello\+world+^D+[ Para+ [ RawInline (Format "tex") "\\itshape\nhello\\\nworld" ]+]+```
@@ -0,0 +1,12 @@+Custom style names are encoded as LibreOffice does:++```+% pandoc -t opendocument+::: {custom-style="My Style"}+[Text here]{custom-style="Char (1)"} and [more]{custom-style="Quote_20_Text"}+:::+^D+<text:p text:style-name="My_20_Style"><text:span text:style-name="Char_20__28_1_29_">Text+here</text:span> and+<text:span text:style-name="Quote_20_Text">more</text:span></text:p>+```
@@ -0,0 +1,14 @@+```+% pandoc -f rst -t gfm+======== ====================+Suffix Meaning+======== ====================+-set Set a list of items+-append Append single item+======== ====================+^D+| Suffix | Meaning |+|---------|---------------------|+| -set | Set a list of items |+| -append | Append single item |+```
@@ -0,0 +1,11 @@+```+% pandoc -f rst -t native+::++ foo+ bar++ baz+^D+[ CodeBlock ( "" , [] , [] ) " foo\nbar\n\n baz" ]+```
@@ -14,7 +14,7 @@ \begin{longtable}[]{@{}ll@{}} \caption{a table}\tabularnewline \toprule\noalign{}-x & y\footnote{a footnote} \\+x & y\pandocfootnotemark{1} \\ \midrule\noalign{} \endfirsthead \toprule\noalign{}@@ -25,4 +25,5 @@ \endlastfoot 1 & 2 \\ \end{longtable}+\pandocfootnotetext{1}{a footnote} ```
@@ -7,7 +7,7 @@ <table> <tbody> <tr>-<td><p>* hello</p></td>+<td>* hello</td> </tr> </tbody> </table>@@ -41,7 +41,7 @@ <table> <tbody> <tr>-<td><p><code>* hello</code></p></td>+<td><code>* hello</code></td> </tr> </tbody> </table>
@@ -0,0 +1,7 @@+```+% pandoc command/2623.odt+^D+<p><em>Italics</em></p>+<p>not <em>italics</em></p>+```+
binary file changed (absent → 3846 bytes)
@@ -32,27 +32,27 @@ <table> <tbody> <tr>-<td><p>peildatum Simbase</p></td>-<td><p>november 2005</p></td>-<td colspan="2"><p><strong>uitslagen Flohrgambiet</strong></p></td>+<td>peildatum Simbase</td>+<td>november 2005</td>+<td colspan="2"><strong>uitslagen Flohrgambiet</strong></td> </tr> <tr>-<td><p>totaal aantal partijen Simbase</p></td>-<td><p>7.316.773</p></td>-<td><p>wit wint</p></td>-<td><p>53%</p></td>+<td>totaal aantal partijen Simbase</td>+<td>7.316.773</td>+<td>wit wint</td>+<td>53%</td> </tr> <tr>-<td><p>percentage (en partijen) Flohrgambiet</p></td>-<td><p>0.023 % (1.699)</p></td>-<td><p>zwart wint</p></td>-<td><p>27%</p></td>+<td>percentage (en partijen) Flohrgambiet</td>+<td>0.023 % (1.699)</td>+<td>zwart wint</td>+<td>27%</td> </tr> <tr>-<td><p>percentage Flohrgambiet in aug 2003</p></td>-<td><p>0.035 %</p></td>-<td><p>remise</p></td>-<td><p>20%</p></td>+<td>percentage Flohrgambiet in aug 2003</td>+<td>0.035 %</td>+<td>remise</td>+<td>20%</td> </tr> </tbody> </table>
@@ -4,7 +4,7 @@ author="Jesse Rosenthal" date="2016-05-09T16:13:00Z"}some text to have a comment []{.comment-end id="0"}on it. ^D-I want [I left a comment.]{.comment-start id="0"+I want [I left a comment.]{.comment-start comment-id="0" author="Jesse Rosenthal" date="2016-05-09T16:13:00Z"}some text to have a-comment []{.comment-end id="0"}on it.+comment []{.comment-end comment-id="0"}on it. ```
@@ -9,12 +9,12 @@ % pandoc -f latex -t native \texttt{``hi''} ^D-[ Para [ Code ( "" , [] , [] ) "\8216\8216hi\8217\8217" ] ]+[ Para [ Code ( "" , [] , [] ) "``hi''" ] ] ``` ``` % pandoc -f latex -t native \texttt{`hi'} ^D-[ Para [ Code ( "" , [] , [] ) "\8216hi\8217" ] ]+[ Para [ Code ( "" , [] , [] ) "`hi'" ] ] ```
@@ -0,0 +1,69 @@+```+% pandoc -f html -t latex+<table class="repeat-caption">+ <caption>Repeated caption.</caption>+ <thead><tr><th>one</th><th>two</th></tr></thead>+ <tbody><tr><td>1</td><td>2</td></tr></tbody>+</table>+^D+\begin{longtable}[]{@{}ll@{}}+\caption{Repeated caption.}\tabularnewline+\toprule\noalign{}+one & two \\+\midrule\noalign{}+\endfirsthead+\caption[]{Repeated caption.}\tabularnewline+\toprule\noalign{}+one & two \\+\midrule\noalign{}+\endhead+\bottomrule\noalign{}+\endlastfoot+1 & 2 \\+\end{longtable}+```++```+% pandoc -f html -t latex+<table class="repeat-caption">+ <caption>Repeated caption.</caption>+ <tbody><tr><td>one</td><td>two</td></tr></tbody>+</table>+^D+\begin{longtable}[]{@{}ll@{}}+\caption{Repeated caption.}\tabularnewline+\toprule\noalign{}+\endfirsthead+\caption[]{Repeated caption.}\tabularnewline+\toprule\noalign{}+\endhead+\bottomrule\noalign{}+\endlastfoot+one & two \\+\end{longtable}+```++```+% pandoc -f html -t latex --table-caption-position=below+<table class="repeat-caption">+ <caption>Repeated caption.</caption>+ <thead><tr><th>one</th><th>two</th></tr></thead>+ <tbody><tr><td>1</td><td>2</td></tr></tbody>+</table>+^D+\begin{longtable}[]{@{}ll@{}}+\toprule\noalign{}+one & two \\+\midrule\noalign{}+\endhead+\bottomrule\noalign{}+\tabularnewline+\caption[]{Repeated caption.}+\endfoot+\bottomrule\noalign{}+\tabularnewline+\caption{Repeated caption.}+\endlastfoot+1 & 2 \\+\end{longtable}+```
@@ -49,7 +49,9 @@ ... ^D Pandoc- Meta { unMeta = fromList [ ( "foo" , MetaBool False ) ] }+ Meta+ { unMeta = fromList [ ( "foo" , MetaInlines [ Str "no" ] ) ]+ } [] ```
@@ -1,6 +1,6 @@ ```-% pandoc -t latex-$x=y%comment$+% pandoc -t latex -f native+[Math InlineMath "x=y%comment"] ^D \(x=y%comment \)
@@ -12,7 +12,8 @@ ^D \begin{figure} \centering-\pandocbounded{\includegraphics[keepaspectratio,alt={This is a figure.}]{img.jpg}}+\pandocbounded{\includegraphics[keepaspectratio,alt={This is a+figure.}]{img.jpg}} \caption{This is a figure.} \end{figure} @@ -50,7 +51,8 @@ \begin{figure} \centering \caption{This is a figure.}-\pandocbounded{\includegraphics[keepaspectratio,alt={This is a figure.}]{img.jpg}}+\pandocbounded{\includegraphics[keepaspectratio,alt={This is a+figure.}]{img.jpg}} \end{figure} \begin{longtable}[]{@{}rlcl@{}}@@ -86,7 +88,8 @@ ^D \begin{figure} \centering-\pandocbounded{\includegraphics[keepaspectratio,alt={This is a figure.}]{img.jpg}}+\pandocbounded{\includegraphics[keepaspectratio,alt={This is a+figure.}]{img.jpg}} \caption{This is a figure.} \end{figure}
@@ -22,10 +22,11 @@ \begin{longtable}[]{@{} >{\centering\arraybackslash}p{(\linewidth - 0\tabcolsep) * \real{0.1667}}@{}}-\caption[Sample table.]{Sample table.\footnote{caption footnote}}\tabularnewline+\caption[Sample table.]{Sample+table.\pandocfootnotemark{1}}\tabularnewline \toprule\noalign{} \begin{minipage}[b]{\linewidth}\centering-Fruit\footnote{header footnote}+Fruit\pandocfootnotemark{2} \end{minipage} \\ \midrule\noalign{} \endfirsthead@@ -37,8 +38,11 @@ \endhead \bottomrule\noalign{} \endlastfoot-Bans\footnote{table cell footnote} \\+Bans\pandocfootnotemark{3} \\ \end{longtable}+\pandocfootnotetext{1}{caption footnote}+\pandocfootnotetext{2}{header footnote}+\pandocfootnotetext{3}{table cell footnote} dolly\footnote{doc footnote} ```
@@ -5,8 +5,8 @@ \begin{figure} \centering \pandocbounded{\includegraphics[keepaspectratio,alt={moon}]{test/lalune.jpg}}-\caption[moon]{moon\footnotemark{}}+\caption[moon]{moon\pandocfootnotemark{1}} \end{figure}-\footnotetext{the moon}+\pandocfootnotetext{1}{the moon} ```
@@ -11,8 +11,8 @@ end ^D [ Para- [ Math DisplayMath "q_3\n+ 4"- , Math DisplayMath "- 5 +\nq_5"+ [ Math DisplayMath "q_3\n + 4"+ , Math DisplayMath "- 5 +\n q_5" ] , Para [ Str "end" ] ]
@@ -9,13 +9,13 @@ <caption>Test table</caption> <thead> <tr>-<th>Column1</th>-<th>Column2</th>+<th><p>Column1</p></th>+<th><p>Column2</p></th> </tr> </thead> <tbody> <tr>-<td>Data1</td>+<td><p>Data1</p></td> <td><ul> <li>data1</li> <li>data2</li>
@@ -27,9 +27,6 @@ <style:style style:name="P1" style:family="paragraph" style:parent-style-name="Table_20_Contents"> <style:paragraph-properties fo:text-align="center" style:justify-single-word="false" /> </style:style>- <style:style style:name="P2" style:family="paragraph" style:parent-style-name="Table_20_Contents">- <style:paragraph-properties fo:text-align="center" style:justify-single-word="false" />- </style:style> <style:style style:name="TableHeaderRowCell" style:family="table-cell"> <style:table-cell-properties fo:border="none" /> </style:style>@@ -56,7 +53,7 @@ <text:p text:style-name="P1">2</text:p> </table:table-cell> <table:table-cell table:style-name="TableRowCell" office:value-type="string">- <text:p text:style-name="P2">1</text:p>+ <text:p text:style-name="P1">1</text:p> </table:table-cell> </table:table-row> </table:table>
@@ -60,7 +60,8 @@ \begin{figure} \centering-\pandocbounded{\includegraphics[keepaspectratio,alt={This is a test x = 5}]{test.png}}+\pandocbounded{\includegraphics[keepaspectratio,alt={This is a test x =+5}]{test.png}} \caption{This is a test \protect\VERB|\NormalTok{x }\OperatorTok{=} \DecValTok{5}|} \end{figure}
@@ -5,8 +5,9 @@ author="Mike" date="2020-12-17T17:39:00Z"} This is the content being commented on [[]{.comment-end id="2"}]{.comment-end id="1"} ^D-[This is the comment]{.comment-start id="1" author="Mike"-date="2020-12-17T16:53:00Z"} [Here is my reply]{.comment-start id="2"-author="Mike" date="2020-12-17T17:39:00Z"} This is the content being-commented on [[]{.comment-end id="2"}]{.comment-end id="1"}+[This is the comment]{.comment-start comment-id="1" author="Mike"+date="2020-12-17T16:53:00Z"} [Here is my reply]{.comment-start+comment-id="2" author="Mike" date="2020-12-17T17:39:00Z"} This is the+content being commented on [[]{.comment-end+comment-id="2"}]{.comment-end comment-id="1"} ```
@@ -23,7 +23,7 @@ AlignDefault (RowSpan 1) (ColSpan 4)- [ Para [ Str "template" , Space , Str "request" ] ]+ [ Plain [ Str "template" , Space , Str "request" ] ] ] ]) [ TableBody@@ -37,25 +37,25 @@ AlignDefault (RowSpan 1) (ColSpan 1)- [ Para [ Str "mode" ] ]+ [ Plain [ Str "mode" ] ] , Cell ( "" , [] , [] ) AlignDefault (RowSpan 1) (ColSpan 1)- [ Para [ Str "No" ] ]+ [ Plain [ Str "No" ] ] , Cell ( "" , [] , [] ) AlignDefault (RowSpan 1) (ColSpan 1)- [ Para [ Str "String" ] ]+ [ Plain [ Str "String" ] ] , Cell ( "" , [] , [] ) AlignDefault (RowSpan 1) (ColSpan 1)- [ Para+ [ Plain [ Str "MUST" , Space , Str "be"
@@ -4,7 +4,8 @@ ^D \begin{figure} \centering-\pandocbounded{\includegraphics[keepaspectratio,page=13,trim=1cm,clip,width=4cm,alt={Global frog population.}]{slides.pdf}}+\pandocbounded{\includegraphics[keepaspectratio,page=13,trim=1cm,clip,width=4cm,alt={Global+frog population.}]{slides.pdf}} \caption{Global frog population.} \end{figure}
@@ -6,7 +6,7 @@ |abc defghij | def | xyz | +------------+----------+------------------+ ^D-<table style="width:60%;">+<table style="width:59%;"> <colgroup> <col style="width: 18%" /> <col style="width: 15%" />
@@ -9,10 +9,10 @@ .. include:: command/three.txt ^D [ CodeBlock- ( "" , [ "" ] , [ ( "code" , "" ) ] )+ ( "" , [] , [ ( "code" , "" ) ] ) "1st line.\n2nd line.\n3rd line.\n" , CodeBlock- ( "" , [ "" ] , [ ( "literal" , "" ) ] )+ ( "" , [] , [ ( "literal" , "" ) ] ) "1st line.\n2nd line.\n3rd line.\n" , Para [ Str "1st"
binary file changed (11923 → 2565 bytes)
@@ -0,0 +1,62 @@+ANSI escape sequences are stripped from code in output cells+unless the output is ipynb (#5633). This used to take time+quadratic in the length of the output (#7744).++```+% pandoc -f ipynb -t native+{+ "cells": [+ {+ "cell_type": "code",+ "execution_count": 1,+ "metadata": {},+ "outputs": [+ {+ "name": "stdout",+ "output_type": "stream",+ "text": [+ "\u001b[1;31mError\u001b[0m: café 日本\n",+ "[0m and \u001b(B are kept\n",+ "\u001b[32m\u001b[1mok\u001b[0m\u001b[0m\n",+ "\u001b[2K\n",+ "loading model\n",+ "\u001b[31 \u001b[0m tail\n",+ "\u001b[1\u001b(Bm end\n"+ ]+ },+ {+ "name": "stderr",+ "output_type": "stream",+ "text": [+ "cut \u001b[31 at the end"+ ]+ }+ ],+ "source": [+ "print(x)"+ ]+ }+ ],+ "metadata": {},+ "nbformat": 4,+ "nbformat_minor": 5+}+^D+[ Div+ ( ""+ , [ "cell" , "code" ]+ , [ ( "execution_count" , "1" ) ]+ )+ [ CodeBlock ( "" , [ "python" ] , [] ) "print(x)"+ , Div+ ( "" , [ "output" , "stream" , "stdout" ] , [] )+ [ CodeBlock+ ( "" , [] , [] )+ "Error: caf\233 \26085\26412\n[0m and \ESC(B are kept\nok\nodel\n tail\n end\n"+ ]+ , Div+ ( "" , [ "output" , "stream" , "stderr" ] , [] )+ [ CodeBlock ( "" , [] , [] ) "cut " ]+ ]+]+```
@@ -19,21 +19,21 @@ <table> <thead> <tr>-<th><p>Header text</p></th>-<th><p>Header text</p></th>-<th><p>Header text</p></th>+<th>Header text</th>+<th>Header text</th>+<th>Header text</th> </tr> </thead> <tbody> <tr>-<td><p>Example</p></td>-<td><p>Example</p></td>-<td><p>Example</p></td>+<td>Example</td>+<td>Example</td>+<td>Example</td> </tr> <tr>-<td><p>Example</p></td>-<td><p>Example</p></td>-<td><p>Example</p></td>+<td>Example</td>+<td>Example</td>+<td>Example</td> </tr> </tbody> </table>
@@ -7,8 +7,8 @@ ^D \begin{description} \tightlist-\item[Header\footnotemark{}:]+\item[Header\pandocfootnotemark{1}:] Some other text. \end{description}-\footnotetext{A footnote.}+\pandocfootnotetext{1}{A footnote.} ```
@@ -10,7 +10,7 @@ | Sum | 8£ | +======+=======+ ^D-<table style="width:21%;">+<table style="width:20%;"> <colgroup> <col style="width: 9%" /> <col style="width: 11%" />
@@ -22,7 +22,7 @@ | a | b | c | d | +-----+-----+-----+-----------+ ^D-<table style="width:42%;">+<table style="width:41%;"> <colgroup> <col style="width: 8%" /> <col style="width: 8%" />@@ -44,10 +44,10 @@ </thead> <tbody> <tr>-<td style="text-align: left;">a</td>-<td>b</td>-<td style="text-align: right;">c</td>-<td style="text-align: right;">d</td>+<td style="text-align: left;"><p>a</p></td>+<td><p>b</p></td>+<td style="text-align: right;"><p>c</p></td>+<td style="text-align: right;"><p>d</p></td> </tr> </tbody> </table>
@@ -32,7 +32,7 @@ role="doc-backlink">↩︎</a></p></li> </ol> </aside>-<table style="width:17%;">+<table style="width:16%;"> <caption>Sample table.<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a></caption> <colgroup>
@@ -25,7 +25,7 @@ <h1 id="section-1">Section 1</h1> <p>hello<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a></p>-<table style="width:17%;">+<table style="width:16%;"> <caption>Sample table.<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a></caption> <colgroup>
@@ -25,7 +25,7 @@ <h1 id="section-1">Section 1</h1> <p>hello<a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a></p>-<table style="width:17%;">+<table style="width:16%;"> <caption>Sample table.<a href="#fn2" class="footnote-ref" id="fnref2" role="doc-noteref"><sup>2</sup></a></caption> <colgroup>
@@ -25,7 +25,7 @@  ^D <figure>-<img role="img" aria-label="minimal" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PgoKPHN2ZyB2aWV3Qm94PSItLjMzMyAtLjMzMyA0ODAgMTUwIiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CiAgICA8cGF0aCBkPSJNIDAgMzUuNSBMIDYuNSAyMi41IEwgMTYgMzcgTCAyMyAyNCBMIDM0LjggNDMuNyBMIDQyLjUgMzAgTCA1MC4zIDQ3IEwgNTkuNyAyNy43IEwgNjkgNDcgTCA4NSAxNy43IEwgOTguMyAzOSBMIDExMyA5LjcgTCAxMjcuNyA0Mi4zIEwgMTM2LjMgMjMuNyBMIDE0NyA0NC4zIEwgMTU4LjMgMjAuMyBMIDE3MC4zIDQwLjMgTCAxNzcuNyAyNS43IEwgMTg5LjcgNDMgTCAxOTkuNyAyMSBMIDIwNy43IDM1IEwgMjE5IDExIEwgMjMzIDM3IEwgMjQwLjMgMjMuNyBMIDI1MSA0MyBMIDI2MyAxOC4zIEwgMjcyLjcgMzMuMyBMIDI4MyAxMCBMIDI5NSAzMi4zIEwgMzAxLjMgMjMgTCAzMTEuNyAzNyBMIDMyMy43IDcuNyBMIDMzOS4zIDM5IEwgMzQ2LjMgMjUuNyBMIDM1Ni4zIDQyLjMgTCAzNjkuNyAxNSBMIDM3Ni4zIDI1LjcgTCAzODQgOSBMIDM5MyAyOC4zIEwgNDAwLjMgMTkgTCA0MTEuNyAzOC4zIEwgNDIxIDIxIEwgNDM0LjMgNDMgTCA0NDUgMjUgTCA0NTMgMzYuMyBMIDQ2NC4zIDE4LjMgTCA0NzYuMiA0MC4zIEwgNDgwIDMzLjUgTCA0ODAgMjE1IEwgMCAyMTUgTCAwIDM1LjUgWiIgZmlsbD0iIzE3NTcyMCIvPgo8L3N2Zz4K" alt="minimal" />+<img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PgoKPHN2ZyB2aWV3Qm94PSItLjMzMyAtLjMzMyA0ODAgMTUwIiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CiAgICA8cGF0aCBkPSJNIDAgMzUuNSBMIDYuNSAyMi41IEwgMTYgMzcgTCAyMyAyNCBMIDM0LjggNDMuNyBMIDQyLjUgMzAgTCA1MC4zIDQ3IEwgNTkuNyAyNy43IEwgNjkgNDcgTCA4NSAxNy43IEwgOTguMyAzOSBMIDExMyA5LjcgTCAxMjcuNyA0Mi4zIEwgMTM2LjMgMjMuNyBMIDE0NyA0NC4zIEwgMTU4LjMgMjAuMyBMIDE3MC4zIDQwLjMgTCAxNzcuNyAyNS43IEwgMTg5LjcgNDMgTCAxOTkuNyAyMSBMIDIwNy43IDM1IEwgMjE5IDExIEwgMjMzIDM3IEwgMjQwLjMgMjMuNyBMIDI1MSA0MyBMIDI2MyAxOC4zIEwgMjcyLjcgMzMuMyBMIDI4MyAxMCBMIDI5NSAzMi4zIEwgMzAxLjMgMjMgTCAzMTEuNyAzNyBMIDMyMy43IDcuNyBMIDMzOS4zIDM5IEwgMzQ2LjMgMjUuNyBMIDM1Ni4zIDQyLjMgTCAzNjkuNyAxNSBMIDM3Ni4zIDI1LjcgTCAzODQgOSBMIDM5MyAyOC4zIEwgNDAwLjMgMTkgTCA0MTEuNyAzOC4zIEwgNDIxIDIxIEwgNDM0LjMgNDMgTCA0NDUgMjUgTCA0NTMgMzYuMyBMIDQ2NC4zIDE4LjMgTCA0NzYuMiA0MC4zIEwgNDgwIDMzLjUgTCA0ODAgMjE1IEwgMCAyMTUgTCAwIDM1LjUgWiIgZmlsbD0iIzE3NTcyMCIvPgo8L3N2Zz4K" alt="minimal" /> <figcaption aria-hidden="true">minimal</figcaption> </figure> ```
binary file changed (12631 → 3247 bytes)
@@ -2,7 +2,8 @@ % pandoc command/9002.docx -t html ^D <table style="width:40%;">-<caption><p>This is my table!</p></caption>+<caption><p><span id="_Ref143272803" class="anchor"></span> This is my+table!</p></caption> <colgroup> <col style="width: 20%" /> <col style="width: 20%" />
binary file changed (absent → 3761 bytes)
@@ -0,0 +1,10 @@+Adjacent "Source Code" paragraphs become one code block with `+styles`,+as they do without it:+```+% pandoc -f docx+styles -t native command/9183.docx+^D+[ Div+ ( "" , [] , [ ( "custom-style" , "Source Code" ) ] )+ [ CodeBlock ( "" , [] , [] ) "x = 1\ny = x + 1" ]+]+```
@@ -0,0 +1,33 @@+Paragraphs after the first in a list item should not be turned+into items of a separate list, and should round-trip:++```+% pandoc -f native -t docx -o - | pandoc -f docx -t native+[ BulletList+ [ [ Para [ Str "one" ]+ , Para [ Str "sub" ]+ , BulletList+ [ [ Para [ Str "nested" ]+ , Para [ Str "nested" , Space , Str "sub" ]+ ]+ ]+ , Para [ Str "back" ]+ ]+ , [ Para [ Str "two" ] ]+ ]+]+^D+[ BulletList+ [ [ Para [ Str "one" ]+ , Para [ Str "sub" ]+ , BulletList+ [ [ Para [ Str "nested" ]+ , Para [ Str "nested" , Space , Str "sub" ]+ ]+ ]+ , Para [ Str "back" ]+ ]+ , [ Para [ Str "two" ] ]+ ]+]+```
binary file changed (absent → 2856 bytes)
@@ -0,0 +1,10 @@+A list whose abstract numbering only has a `w:numStyleLink` takes its+levels from the abstract numbering with the matching `w:styleLink`:+```+% pandoc -f docx -t native command/9222.docx+^D+[ OrderedList+ ( 1 , Decimal , Period )+ [ [ Plain [ Str "one" ] ] , [ Plain [ Str "two" ] ] ]+]+```
binary file changed (13439 → 3687 bytes)
binary file changed (13701 → 4078 bytes)
@@ -2,6 +2,6 @@ % pandoc --embed-resources  ^D-<p><img role="img" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTkiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB2ZXJzaW9uPSIxLjEiIGlkPSJzdmcyIiB3aWR0aD0iMTkxLjU2MjY3IiBoZWlnaHQ9IjE1MS43MTIwMSIgdmlld0JveD0iMCAwIDE5MS41NjI2NyAxNTEuNzEyMDEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnMgaWQ9ImRlZnM2Ij4KICAgIDxjbGlwUGF0aCBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImNsaXBQYXRoMjQiPgogICAgICA8cGF0aCBkPSJNIDU2LjY5MzYyLDAgMTEzLjM4NzI0LDExMy4zODcyNCAxNzAuMDgwODYsMCBaIiBpZD0icGF0aDIyIiAvPgogICAgPC9jbGlwUGF0aD4KICAgIDxjbGlwUGF0aCBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImNsaXBQYXRoMzgiPgogICAgICA8cGF0aCBkPSJNIDAsMCBIIDEwMCBWIDEwMCBIIDAgWiIgaWQ9InBhdGgzNiIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8cmFkaWFsR3JhZGllbnQgZng9IjUwLjAwMDY0MSIgZnk9IjUwLjAwMDY0MSIgY3g9IjUwLjAwMDY0MSIgY3k9IjUwLjAwMDY0MSIgcj0iNTAuMDAwNjQxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9InJhZGlhbEdyYWRpZW50NDYiPgogICAgICA8c3RvcCBzdHlsZT0ic3RvcC1vcGFjaXR5OjE7c3RvcC1jb2xvcjojZmZmZmZmIiBvZmZzZXQ9IjAiIGlkPSJzdG9wNDAiIC8+CiAgICAgIDxzdG9wIHN0eWxlPSJzdG9wLW9wYWNpdHk6MTtzdG9wLWNvbG9yOiM5OTk5ZmYiIG9mZnNldD0iMjUuMDAwMzIiIGlkPSJzdG9wNDIiIC8+CiAgICAgIDxzdG9wIHN0eWxlPSJzdG9wLW9wYWNpdHk6MTtzdG9wLWNvbG9yOiM5OTk5ZmYiIG9mZnNldD0iNTAuMDAwNjQiIGlkPSJzdG9wNDQiIC8+CiAgICA8L3JhZGlhbEdyYWRpZW50PgogIDwvZGVmcz4KICA8ZyBpZD0iZzgiIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDE1MS43MTIpIj4KICAgIDxnIGlkPSJnMTAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNi42MDYsMC4xOTkpIj4KICAgICAgPGcgaWQ9ImcxMiI+CiAgICAgICAgPGcgaWQ9ImcxNCI+CiAgICAgICAgICA8ZyBpZD0iZzE2Ij4KICAgICAgICAgICAgPGcgaWQ9ImcxOCI+CiAgICAgICAgICAgICAgPGcgaWQ9ImcyMCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoMjQpIj4KICAgICAgICAgICAgICAgIDxnIGlkPSJnMjYiIHRyYW5zZm9ybT0ibWF0cml4KDIuMjY4MDIsMCwwLDIuMjY4MDIsMTEzLjM4NzI0LDU2LjY5MzYyKSI+CiAgICAgICAgICAgICAgICAgIDxnIGlkPSJnMjgiPgogICAgICAgICAgICAgICAgICAgIDxnIGlkPSJnMzAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC01MCwtNTApIj4KICAgICAgICAgICAgICAgICAgICAgIDxnIGlkPSJnMzIiPgogICAgICAgICAgICAgICAgICAgICAgICA8ZyBpZD0iZzM0IiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgzOCkiPgogICAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0gMCwwIEggMTAwIFYgMTAwIEggMCBaIiBzdHlsZT0iZmlsbDp1cmwoI3JhZGlhbEdyYWRpZW50NDYpO3N0cm9rZTpub25lIiBpZD0icGF0aDQ4IiAvPgogICAgICAgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0gNTYuNjkzNjIsMCAxMTMuMzg3MjQsMTEzLjM4NzI0IDE3MC4wODA4NiwwIFoiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiM5OTk5ZmY7c3Ryb2tlLXdpZHRoOjAuMzk4NTtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLW9wYWNpdHk6MSIgaWQ9InBhdGg1MCIgLz4KICAgICAgICAgICAgPGcgaWQ9Imc1MiI+CiAgICAgICAgICAgICAgPGcgaWQ9Imc1NCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjMuMzM3LDUuNDU3KSI+CiAgICAgICAgICAgICAgICA8ZyBpZD0iZzU2Ij4KICAgICAgICAgICAgICAgICAgPGcgaWQ9Imc1OCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTM2LjczMSwtNS42NTYpIj4KICAgICAgICAgICAgICAgICAgICA8dGV4dCB4bWw6c3BhY2U9InByZXNlcnZlIiB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwtMSwzNi43MzEsNS42NTYpIiBzdHlsZT0iZm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zaXplOjkuOTYyNnB4O2ZvbnQtZmFtaWx5OkNNU1MxMDstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOkNNU1MxMDt3cml0aW5nLW1vZGU6bHItdGI7ZmlsbDojOTk5OWZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiBpZD0idGV4dDYyIj48dHNwYW4geD0iMCA0Ljk4MTI5OTkgOC4zODU1MiAxMy4xNzM1NDYgMTguMzIxMjIyIDIwLjcwMTI4NiAyNS4xMjg2NjYgMzAuMjc2MzQgMzcuMTkxMzgzIDQxLjAxMDA0OCA0Ni4xNTc3MjIgNTAuOTQ1NzQ3IDU2LjA5MzQyMiA2MC41MjA4MDIgNjguOTk1OTg3IDcyLjU5MzQ4MyA3NS45OTc3MDQgNzguMzc3NzY5IDgzLjE2NTc5NCA4OC4zMDM1MDUgOTMuMjg0ODA1IDk1LjY2NDg3MSIgeT0iMCIgaWQ9InRzcGFuNjAiPmdyYWRpZW50c2hhZGVkdHJpYW5nbGU8L3RzcGFuPjwvdGV4dD4KICAgICAgICAgICAgICAgICAgICA8ZyBpZD0iZzY0IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzNi43MzEsNS42NTYpIiAvPgogICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8ZyBpZD0iZzY2IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNjMuMzM3LC01LjQ1NykiIC8+CiAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgICA8L2c+CiAgICAgICAgPC9nPgogICAgICA8L2c+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K" /></p>+<p><img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTkiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB2ZXJzaW9uPSIxLjEiIGlkPSJzdmcyIiB3aWR0aD0iMTkxLjU2MjY3IiBoZWlnaHQ9IjE1MS43MTIwMSIgdmlld0JveD0iMCAwIDE5MS41NjI2NyAxNTEuNzEyMDEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnMgaWQ9ImRlZnM2Ij4KICAgIDxjbGlwUGF0aCBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImNsaXBQYXRoMjQiPgogICAgICA8cGF0aCBkPSJNIDU2LjY5MzYyLDAgMTEzLjM4NzI0LDExMy4zODcyNCAxNzAuMDgwODYsMCBaIiBpZD0icGF0aDIyIiAvPgogICAgPC9jbGlwUGF0aD4KICAgIDxjbGlwUGF0aCBjbGlwUGF0aFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImNsaXBQYXRoMzgiPgogICAgICA8cGF0aCBkPSJNIDAsMCBIIDEwMCBWIDEwMCBIIDAgWiIgaWQ9InBhdGgzNiIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8cmFkaWFsR3JhZGllbnQgZng9IjUwLjAwMDY0MSIgZnk9IjUwLjAwMDY0MSIgY3g9IjUwLjAwMDY0MSIgY3k9IjUwLjAwMDY0MSIgcj0iNTAuMDAwNjQxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9InJhZGlhbEdyYWRpZW50NDYiPgogICAgICA8c3RvcCBzdHlsZT0ic3RvcC1vcGFjaXR5OjE7c3RvcC1jb2xvcjojZmZmZmZmIiBvZmZzZXQ9IjAiIGlkPSJzdG9wNDAiIC8+CiAgICAgIDxzdG9wIHN0eWxlPSJzdG9wLW9wYWNpdHk6MTtzdG9wLWNvbG9yOiM5OTk5ZmYiIG9mZnNldD0iMjUuMDAwMzIiIGlkPSJzdG9wNDIiIC8+CiAgICAgIDxzdG9wIHN0eWxlPSJzdG9wLW9wYWNpdHk6MTtzdG9wLWNvbG9yOiM5OTk5ZmYiIG9mZnNldD0iNTAuMDAwNjQiIGlkPSJzdG9wNDQiIC8+CiAgICA8L3JhZGlhbEdyYWRpZW50PgogIDwvZGVmcz4KICA8ZyBpZD0iZzgiIHRyYW5zZm9ybT0ibWF0cml4KDEuMzMzMzMzMywwLDAsLTEuMzMzMzMzMywwLDE1MS43MTIpIj4KICAgIDxnIGlkPSJnMTAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNi42MDYsMC4xOTkpIj4KICAgICAgPGcgaWQ9ImcxMiI+CiAgICAgICAgPGcgaWQ9ImcxNCI+CiAgICAgICAgICA8ZyBpZD0iZzE2Ij4KICAgICAgICAgICAgPGcgaWQ9ImcxOCI+CiAgICAgICAgICAgICAgPGcgaWQ9ImcyMCIgY2xpcC1wYXRoPSJ1cmwoI2NsaXBQYXRoMjQpIj4KICAgICAgICAgICAgICAgIDxnIGlkPSJnMjYiIHRyYW5zZm9ybT0ibWF0cml4KDIuMjY4MDIsMCwwLDIuMjY4MDIsMTEzLjM4NzI0LDU2LjY5MzYyKSI+CiAgICAgICAgICAgICAgICAgIDxnIGlkPSJnMjgiPgogICAgICAgICAgICAgICAgICAgIDxnIGlkPSJnMzAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC01MCwtNTApIj4KICAgICAgICAgICAgICAgICAgICAgIDxnIGlkPSJnMzIiPgogICAgICAgICAgICAgICAgICAgICAgICA8ZyBpZD0iZzM0IiBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgzOCkiPgogICAgICAgICAgICAgICAgICAgICAgICAgIDxwYXRoIGQ9Ik0gMCwwIEggMTAwIFYgMTAwIEggMCBaIiBzdHlsZT0iZmlsbDp1cmwoI3JhZGlhbEdyYWRpZW50NDYpO3N0cm9rZTpub25lIiBpZD0icGF0aDQ4IiAvPgogICAgICAgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgICAgIDxwYXRoIGQ9Ik0gNTYuNjkzNjIsMCAxMTMuMzg3MjQsMTEzLjM4NzI0IDE3MC4wODA4NiwwIFoiIHN0eWxlPSJmaWxsOm5vbmU7c3Ryb2tlOiM5OTk5ZmY7c3Ryb2tlLXdpZHRoOjAuMzk4NTtzdHJva2UtbGluZWNhcDpidXR0O3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLW9wYWNpdHk6MSIgaWQ9InBhdGg1MCIgLz4KICAgICAgICAgICAgPGcgaWQ9Imc1MiI+CiAgICAgICAgICAgICAgPGcgaWQ9Imc1NCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjMuMzM3LDUuNDU3KSI+CiAgICAgICAgICAgICAgICA8ZyBpZD0iZzU2Ij4KICAgICAgICAgICAgICAgICAgPGcgaWQ9Imc1OCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTM2LjczMSwtNS42NTYpIj4KICAgICAgICAgICAgICAgICAgICA8dGV4dCB4bWw6c3BhY2U9InByZXNlcnZlIiB0cmFuc2Zvcm09Im1hdHJpeCgxLDAsMCwtMSwzNi43MzEsNS42NTYpIiBzdHlsZT0iZm9udC12YXJpYW50Om5vcm1hbDtmb250LXdlaWdodDpub3JtYWw7Zm9udC1zaXplOjkuOTYyNnB4O2ZvbnQtZmFtaWx5OkNNU1MxMDstaW5rc2NhcGUtZm9udC1zcGVjaWZpY2F0aW9uOkNNU1MxMDt3cml0aW5nLW1vZGU6bHItdGI7ZmlsbDojOTk5OWZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiBpZD0idGV4dDYyIj48dHNwYW4geD0iMCA0Ljk4MTI5OTkgOC4zODU1MiAxMy4xNzM1NDYgMTguMzIxMjIyIDIwLjcwMTI4NiAyNS4xMjg2NjYgMzAuMjc2MzQgMzcuMTkxMzgzIDQxLjAxMDA0OCA0Ni4xNTc3MjIgNTAuOTQ1NzQ3IDU2LjA5MzQyMiA2MC41MjA4MDIgNjguOTk1OTg3IDcyLjU5MzQ4MyA3NS45OTc3MDQgNzguMzc3NzY5IDgzLjE2NTc5NCA4OC4zMDM1MDUgOTMuMjg0ODA1IDk1LjY2NDg3MSIgeT0iMCIgaWQ9InRzcGFuNjAiPmdyYWRpZW50c2hhZGVkdHJpYW5nbGU8L3RzcGFuPjwvdGV4dD4KICAgICAgICAgICAgICAgICAgICA8ZyBpZD0iZzY0IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzNi43MzEsNS42NTYpIiAvPgogICAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8L2c+CiAgICAgICAgICAgICAgICA8ZyBpZD0iZzY2IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNjMuMzM3LC01LjQ1NykiIC8+CiAgICAgICAgICAgICAgPC9nPgogICAgICAgICAgICA8L2c+CiAgICAgICAgICA8L2c+CiAgICAgICAgPC9nPgogICAgICA8L2c+CiAgICA8L2c+CiAgPC9nPgo8L3N2Zz4K" /></p> ```
binary file changed (13799 → 3254 bytes)
@@ -6,7 +6,7 @@ okay ^D-2> [WARNING] Div at _chunk line 1 column 1 unclosed at _chunk line 5 column 1, closing implicitly.+2> [WARNING] Div at line 1 column 1 unclosed at line 5 column 1, closing implicitly. <blockquote> <div class="fence"> <p>that is not closed</p>
@@ -4,7 +4,7 @@ > A suggestion. ^D [ Div- ( "" , [ "tip" ] , [] )+ ( "" , [ "tip" , "alert" ] , [] ) [ Div ( "" , [ "title" ] , [] ) [ Para [ Str "Tip" ] ] , Para [ Str "A" , Space , Str "suggestion." ] ]
@@ -6,8 +6,6 @@ *#foo* ^D [ Para- [ Strong- [ SoftBreak , Str "bar" , Space , Str "baz" , SoftBreak ]- ]+ [ Strong [ SoftBreak , Str "bar" , Space , Str "baz" ] ] ] ```
@@ -35,6 +35,7 @@ issued: 2004-04-05 type: article-journal - id: item5-1+ issued: "-001" type: article-journal - id: item5-2 issued: "-0876"
@@ -14,11 +14,11 @@ # These should be filled in by pandoc: opts="-f -r --from --read -t -w --to --write -o --output --data-dir -M --metadata --metadata-file -d --defaults --file-scope --sandbox -s --standalone --template -V --variable --variable-json --wrap --ascii --toc --table-of-contents --toc-depth --lof --list-of-figures --lot --list-of-tables -N --number-sections --number-offset --top-level-division --extract-media --resource-path -H --include-in-header -B --include-before-body -A --include-after-body --no-highlight --highlight-style --syntax-definition --syntax-highlighting --dpi --eol --columns -p --preserve-tabs --tab-stop --pdf-engine --pdf-engine-opt --reference-doc --self-contained --embed-resources --link-images --request-header --no-check-certificate --abbreviations --typst-input --indented-code-classes --default-image-extension -F --filter -L --lua-filter --shift-heading-level-by --base-header-level --track-changes --strip-comments --reference-links --reference-location --figure-caption-position --table-caption-position --markdown-headings --list-tables --listings -i --incremental --slide-level --section-divs --html-q-tags --email-obfuscation --id-prefix -T --title-prefix -c --css --epub-subdirectory --epub-cover-image --epub-title-page --epub-metadata --epub-embed-font --split-level --chunk-template --epub-chapter-level --ipynb-output -C --citeproc --bibliography --csl --citation-abbreviations --natbib --biblatex --math-method --mathml --webtex --mathjax --katex --gladtex --trace --dump-args --ignore-args --verbose --quiet --fail-if-warnings --log --completion --bash-completion --list-input-formats --list-output-formats --list-extensions --list-highlight-languages --list-highlight-styles -D --print-default-template --print-default-data-file --print-highlight-style -v --version -h --help"- informats="asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml"- outformats="ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki"+ informats="asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 fodt gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml"+ outformats="ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 fodt gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki" highlight_styles="pygments tango espresso zenburn kate monochrome breezedark haddock" math_methods="plain mathml webtex mathjax katex gladtex"- datafiles="reference.docx reference.odt reference.pptx MANUAL.txt docx/_rels/.rels pptx/_rels/.rels abbreviations creole.lua default.csl docbook-entities.txt docx/[Content_Types].xml docx/docProps/app.xml docx/docProps/core.xml docx/docProps/custom.xml docx/word/_rels/document.xml.rels docx/word/_rels/footnotes.xml.rels docx/word/comments.xml docx/word/document.xml docx/word/fontTable.xml docx/word/footnotes.xml docx/word/numbering.xml docx/word/settings.xml docx/word/styles.xml docx/word/theme/theme1.xml docx/word/webSettings.xml dzslides/template.html epub.css init.lua odt/META-INF/manifest.xml odt/content.xml odt/manifest.rdf odt/meta.xml odt/mimetype odt/styles.xml pptx/[Content_Types].xml pptx/docProps/app.xml pptx/docProps/core.xml pptx/ppt/_rels/presentation.xml.rels pptx/ppt/notesMasters/_rels/notesMaster1.xml.rels pptx/ppt/notesMasters/notesMaster1.xml pptx/ppt/notesSlides/_rels/notesSlide1.xml.rels pptx/ppt/notesSlides/_rels/notesSlide2.xml.rels pptx/ppt/notesSlides/notesSlide1.xml pptx/ppt/notesSlides/notesSlide2.xml pptx/ppt/presProps.xml pptx/ppt/presentation.xml pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels pptx/ppt/slideLayouts/slideLayout1.xml pptx/ppt/slideLayouts/slideLayout10.xml pptx/ppt/slideLayouts/slideLayout11.xml pptx/ppt/slideLayouts/slideLayout2.xml pptx/ppt/slideLayouts/slideLayout3.xml pptx/ppt/slideLayouts/slideLayout4.xml pptx/ppt/slideLayouts/slideLayout5.xml pptx/ppt/slideLayouts/slideLayout6.xml pptx/ppt/slideLayouts/slideLayout7.xml pptx/ppt/slideLayouts/slideLayout8.xml pptx/ppt/slideLayouts/slideLayout9.xml pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels pptx/ppt/slideMasters/slideMaster1.xml pptx/ppt/slides/_rels/slide1.xml.rels pptx/ppt/slides/_rels/slide2.xml.rels pptx/ppt/slides/_rels/slide3.xml.rels pptx/ppt/slides/_rels/slide4.xml.rels pptx/ppt/slides/slide1.xml pptx/ppt/slides/slide2.xml pptx/ppt/slides/slide3.xml pptx/ppt/slides/slide4.xml pptx/ppt/tableStyles.xml pptx/ppt/theme/theme1.xml pptx/ppt/theme/theme2.xml pptx/ppt/viewProps.xml templates/affiliations.jats templates/after-header-includes.latex templates/article.jats_publishing templates/common.latex templates/default.ansi templates/default.asciidoc templates/default.bbcode templates/default.beamer templates/default.biblatex templates/default.bibtex templates/default.chunkedhtml templates/default.commonmark templates/default.context templates/default.djot templates/default.docbook4 templates/default.docbook5 templates/default.dokuwiki templates/default.dzslides templates/default.epub2 templates/default.epub3 templates/default.haddock templates/default.html4 templates/default.html5 templates/default.icml templates/default.jats_archiving templates/default.jats_articleauthoring templates/default.jats_publishing templates/default.jira templates/default.latex templates/default.man templates/default.markdown templates/default.markua templates/default.mediawiki templates/default.ms templates/default.muse templates/default.opendocument templates/default.openxml templates/default.opml templates/default.org templates/default.plain templates/default.revealjs templates/default.rst templates/default.rtf templates/default.s5 templates/default.slideous templates/default.slidy templates/default.t2t templates/default.tei templates/default.texinfo templates/default.textile templates/default.typst templates/default.vimdoc templates/default.xwiki templates/default.zimwiki templates/document-metadata.latex templates/font-settings.latex templates/fonts.latex templates/hypersetup.latex templates/passoptions.latex templates/styles.citations.html templates/styles.html templates/template.typst translations/af.yaml translations/alt.yaml translations/am.yaml translations/ar.yaml translations/as.yaml translations/ast.yaml translations/az.yaml translations/be.yaml translations/bg.yaml translations/bn.yaml translations/bo.yaml translations/br.yaml translations/bs.yaml translations/bua.yaml translations/ca.yaml translations/ckb-Arab.yaml translations/ckb-Latn.yaml translations/cs.yaml translations/cu.yaml translations/cy.yaml translations/cz.yaml translations/da.yaml translations/de.yaml translations/dsb.yaml translations/el.yaml translations/en.yaml translations/eo.yaml translations/es-ES.yaml translations/es-MX.yaml translations/es.yaml translations/et.yaml translations/eu.yaml translations/fa.yaml translations/fi.yaml translations/fil.yaml translations/fr.yaml translations/fur.yaml translations/ga.yaml translations/gd.yaml translations/gl.yaml translations/grc.yaml translations/gu.yaml translations/ha.yaml translations/he.yaml translations/hi.yaml translations/hr.yaml translations/hsb.yaml translations/hu.yaml translations/hy.yaml translations/ia.yaml translations/id.yaml translations/is.yaml translations/it.yaml translations/ja.yaml translations/ka.yaml translations/km.yaml translations/kmr-Arab.yaml translations/kmr-Latn.yaml translations/kn.yaml translations/ko.yaml translations/la.yaml translations/lb.yaml translations/lo.yaml translations/lt.yaml translations/lv.yaml translations/mk.yaml translations/ml.yaml translations/mn.yaml translations/mr.yaml translations/ms.yaml translations/nb.yaml translations/nko.yaml translations/nl.yaml translations/nn.yaml translations/no.yaml translations/oc.yaml translations/or.yaml translations/pa.yaml translations/pl.yaml translations/pms.yaml translations/pt-BR.yaml translations/pt-PT.yaml translations/pt.yaml translations/rm.yaml translations/ro.yaml translations/ru.yaml translations/se.yaml translations/si.yaml translations/sk.yaml translations/sl.yaml translations/sq.yaml translations/sr-Cyrl.yaml translations/sr-Latn.yaml translations/sr.yaml translations/sv.yaml translations/ta.yaml translations/te.yaml translations/th.yaml translations/tk.yaml translations/tr.yaml translations/ua.yaml translations/ug.yaml translations/uk.yaml translations/ur.yaml translations/vi.yaml translations/zh-Hans.yaml translations/zh-Hant.yaml"+ datafiles="MANUAL.txt abbreviations creole.lua default.csl docbook-entities.txt docx/[Content_Types].xml docx/_rels/.rels docx/docProps/app.xml docx/docProps/core.xml docx/docProps/custom.xml docx/word/_rels/document.xml.rels docx/word/_rels/footnotes.xml.rels docx/word/comments.xml docx/word/document.xml docx/word/fontTable.xml docx/word/footnotes.xml docx/word/numbering.xml docx/word/settings.xml docx/word/styles.xml docx/word/theme/theme1.xml docx/word/webSettings.xml dzslides/template.html epub.css init.lua odt/META-INF/manifest.xml odt/content.xml odt/manifest.rdf odt/meta.xml odt/mimetype odt/styles.xml pptx/[Content_Types].xml pptx/_rels/.rels pptx/docProps/app.xml pptx/docProps/core.xml pptx/ppt/_rels/presentation.xml.rels pptx/ppt/notesMasters/_rels/notesMaster1.xml.rels pptx/ppt/notesMasters/notesMaster1.xml pptx/ppt/notesSlides/_rels/notesSlide1.xml.rels pptx/ppt/notesSlides/_rels/notesSlide2.xml.rels pptx/ppt/notesSlides/notesSlide1.xml pptx/ppt/notesSlides/notesSlide2.xml pptx/ppt/presProps.xml pptx/ppt/presentation.xml pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels pptx/ppt/slideLayouts/slideLayout1.xml pptx/ppt/slideLayouts/slideLayout10.xml pptx/ppt/slideLayouts/slideLayout11.xml pptx/ppt/slideLayouts/slideLayout2.xml pptx/ppt/slideLayouts/slideLayout3.xml pptx/ppt/slideLayouts/slideLayout4.xml pptx/ppt/slideLayouts/slideLayout5.xml pptx/ppt/slideLayouts/slideLayout6.xml pptx/ppt/slideLayouts/slideLayout7.xml pptx/ppt/slideLayouts/slideLayout8.xml pptx/ppt/slideLayouts/slideLayout9.xml pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels pptx/ppt/slideMasters/slideMaster1.xml pptx/ppt/slides/_rels/slide1.xml.rels pptx/ppt/slides/_rels/slide2.xml.rels pptx/ppt/slides/_rels/slide3.xml.rels pptx/ppt/slides/_rels/slide4.xml.rels pptx/ppt/slides/slide1.xml pptx/ppt/slides/slide2.xml pptx/ppt/slides/slide3.xml pptx/ppt/slides/slide4.xml pptx/ppt/tableStyles.xml pptx/ppt/theme/theme1.xml pptx/ppt/theme/theme2.xml pptx/ppt/viewProps.xml reference.docx reference.odt reference.pptx templates/affiliations.jats templates/after-header-includes.latex templates/article.jats_publishing templates/common.latex templates/default.ansi templates/default.asciidoc templates/default.bbcode templates/default.beamer templates/default.biblatex templates/default.bibtex templates/default.chunkedhtml templates/default.commonmark templates/default.context templates/default.djot templates/default.docbook4 templates/default.docbook5 templates/default.dokuwiki templates/default.dzslides templates/default.epub2 templates/default.epub3 templates/default.haddock templates/default.html4 templates/default.html5 templates/default.icml templates/default.jats_archiving templates/default.jats_articleauthoring templates/default.jats_publishing templates/default.jira templates/default.latex templates/default.man templates/default.markdown templates/default.markua templates/default.mediawiki templates/default.ms templates/default.muse templates/default.opendocument templates/default.openxml templates/default.opml templates/default.org templates/default.plain templates/default.revealjs templates/default.rst templates/default.rtf templates/default.s5 templates/default.slideous templates/default.slidy templates/default.t2t templates/default.tei templates/default.texinfo templates/default.textile templates/default.typst templates/default.vimdoc templates/default.xwiki templates/default.zimwiki templates/document-metadata.latex templates/font-settings.latex templates/fonts.latex templates/hypersetup.latex templates/passoptions.latex templates/styles.citations.html templates/styles.html templates/template.typst translations/af.yaml translations/alt.yaml translations/am.yaml translations/ar.yaml translations/as.yaml translations/ast.yaml translations/az.yaml translations/be.yaml translations/bg.yaml translations/bn.yaml translations/bo.yaml translations/br.yaml translations/bs.yaml translations/bua.yaml translations/ca.yaml translations/ckb-Arab.yaml translations/ckb-Latn.yaml translations/cs.yaml translations/cu.yaml translations/cy.yaml translations/cz.yaml translations/da.yaml translations/de.yaml translations/dsb.yaml translations/el.yaml translations/en.yaml translations/eo.yaml translations/es-ES.yaml translations/es-MX.yaml translations/es.yaml translations/et.yaml translations/eu.yaml translations/fa.yaml translations/fi.yaml translations/fil.yaml translations/fr.yaml translations/fur.yaml translations/ga.yaml translations/gd.yaml translations/gl.yaml translations/grc.yaml translations/gu.yaml translations/ha.yaml translations/he.yaml translations/hi.yaml translations/hr.yaml translations/hsb.yaml translations/hu.yaml translations/hy.yaml translations/ia.yaml translations/id.yaml translations/is.yaml translations/it.yaml translations/ja.yaml translations/ka.yaml translations/km.yaml translations/kmr-Arab.yaml translations/kmr-Latn.yaml translations/kn.yaml translations/ko.yaml translations/la.yaml translations/lb.yaml translations/lo.yaml translations/lt.yaml translations/lv.yaml translations/mk.yaml translations/ml.yaml translations/mn.yaml translations/mr.yaml translations/ms.yaml translations/nb.yaml translations/nko.yaml translations/nl.yaml translations/nn.yaml translations/no.yaml translations/oc.yaml translations/or.yaml translations/pa.yaml translations/pl.yaml translations/pms.yaml translations/pt-BR.yaml translations/pt-PT.yaml translations/pt.yaml translations/rm.yaml translations/ro.yaml translations/ru.yaml translations/se.yaml translations/si.yaml translations/sk.yaml translations/sl.yaml translations/sq.yaml translations/sr-Cyrl.yaml translations/sr-Latn.yaml translations/sr.yaml translations/sv.yaml translations/ta.yaml translations/te.yaml translations/th.yaml translations/tk.yaml translations/tr.yaml translations/ua.yaml translations/ug.yaml translations/uk.yaml translations/ur.yaml translations/vi.yaml translations/zh-Hans.yaml translations/zh-Hant.yaml" case "${prev}" in -f|-r|--from|--read)@@ -116,14 +116,14 @@ _pandoc() { local -a args args=(- '-f[Reader format]:FORMAT:(asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml)'- '-r[Reader format]:FORMAT:(asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml)'- '--from[Reader format]:FORMAT:(asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml)'- '--read[Reader format]:FORMAT:(asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml)'- '-t[Writer format]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'- '-w[Writer format]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'- '--to[Writer format]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'- '--write[Writer format]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'+ '-f[Reader format]:FORMAT:(asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 fodt gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml)'+ '-r[Reader format]:FORMAT:(asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 fodt gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml)'+ '--from[Reader format]:FORMAT:(asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 fodt gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml)'+ '--read[Reader format]:FORMAT:(asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 fodt gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml)'+ '-t[Writer format]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 fodt gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'+ '-w[Writer format]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 fodt gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'+ '--to[Writer format]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 fodt gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'+ '--write[Writer format]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 fodt gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)' '-o[Output file]:FILE:_files' '--output[Output file]:FILE:_files' '--data-dir[Directory for data files]:DIRECTORY:_files'@@ -245,9 +245,9 @@ '--list-extensions[List supported extensions]' '--list-highlight-languages[List highlighting languages]' '--list-highlight-styles[List highlighting styles]'- '-D[Format to print template for]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'- '--print-default-template[Format to print template for]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'- '--print-default-data-file[Data file to print]:FILE:(reference.docx reference.odt reference.pptx MANUAL.txt docx/_rels/.rels pptx/_rels/.rels abbreviations creole.lua default.csl docbook-entities.txt docx/[Content_Types].xml docx/docProps/app.xml docx/docProps/core.xml docx/docProps/custom.xml docx/word/_rels/document.xml.rels docx/word/_rels/footnotes.xml.rels docx/word/comments.xml docx/word/document.xml docx/word/fontTable.xml docx/word/footnotes.xml docx/word/numbering.xml docx/word/settings.xml docx/word/styles.xml docx/word/theme/theme1.xml docx/word/webSettings.xml dzslides/template.html epub.css init.lua odt/META-INF/manifest.xml odt/content.xml odt/manifest.rdf odt/meta.xml odt/mimetype odt/styles.xml pptx/[Content_Types].xml pptx/docProps/app.xml pptx/docProps/core.xml pptx/ppt/_rels/presentation.xml.rels pptx/ppt/notesMasters/_rels/notesMaster1.xml.rels pptx/ppt/notesMasters/notesMaster1.xml pptx/ppt/notesSlides/_rels/notesSlide1.xml.rels pptx/ppt/notesSlides/_rels/notesSlide2.xml.rels pptx/ppt/notesSlides/notesSlide1.xml pptx/ppt/notesSlides/notesSlide2.xml pptx/ppt/presProps.xml pptx/ppt/presentation.xml pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels pptx/ppt/slideLayouts/slideLayout1.xml pptx/ppt/slideLayouts/slideLayout10.xml pptx/ppt/slideLayouts/slideLayout11.xml pptx/ppt/slideLayouts/slideLayout2.xml pptx/ppt/slideLayouts/slideLayout3.xml pptx/ppt/slideLayouts/slideLayout4.xml pptx/ppt/slideLayouts/slideLayout5.xml pptx/ppt/slideLayouts/slideLayout6.xml pptx/ppt/slideLayouts/slideLayout7.xml pptx/ppt/slideLayouts/slideLayout8.xml pptx/ppt/slideLayouts/slideLayout9.xml pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels pptx/ppt/slideMasters/slideMaster1.xml pptx/ppt/slides/_rels/slide1.xml.rels pptx/ppt/slides/_rels/slide2.xml.rels pptx/ppt/slides/_rels/slide3.xml.rels pptx/ppt/slides/_rels/slide4.xml.rels pptx/ppt/slides/slide1.xml pptx/ppt/slides/slide2.xml pptx/ppt/slides/slide3.xml pptx/ppt/slides/slide4.xml pptx/ppt/tableStyles.xml pptx/ppt/theme/theme1.xml pptx/ppt/theme/theme2.xml pptx/ppt/viewProps.xml templates/affiliations.jats templates/after-header-includes.latex templates/article.jats_publishing templates/common.latex templates/default.ansi templates/default.asciidoc templates/default.bbcode templates/default.beamer templates/default.biblatex templates/default.bibtex templates/default.chunkedhtml templates/default.commonmark templates/default.context templates/default.djot templates/default.docbook4 templates/default.docbook5 templates/default.dokuwiki templates/default.dzslides templates/default.epub2 templates/default.epub3 templates/default.haddock templates/default.html4 templates/default.html5 templates/default.icml templates/default.jats_archiving templates/default.jats_articleauthoring templates/default.jats_publishing templates/default.jira templates/default.latex templates/default.man templates/default.markdown templates/default.markua templates/default.mediawiki templates/default.ms templates/default.muse templates/default.opendocument templates/default.openxml templates/default.opml templates/default.org templates/default.plain templates/default.revealjs templates/default.rst templates/default.rtf templates/default.s5 templates/default.slideous templates/default.slidy templates/default.t2t templates/default.tei templates/default.texinfo templates/default.textile templates/default.typst templates/default.vimdoc templates/default.xwiki templates/default.zimwiki templates/document-metadata.latex templates/font-settings.latex templates/fonts.latex templates/hypersetup.latex templates/passoptions.latex templates/styles.citations.html templates/styles.html templates/template.typst translations/af.yaml translations/alt.yaml translations/am.yaml translations/ar.yaml translations/as.yaml translations/ast.yaml translations/az.yaml translations/be.yaml translations/bg.yaml translations/bn.yaml translations/bo.yaml translations/br.yaml translations/bs.yaml translations/bua.yaml translations/ca.yaml translations/ckb-Arab.yaml translations/ckb-Latn.yaml translations/cs.yaml translations/cu.yaml translations/cy.yaml translations/cz.yaml translations/da.yaml translations/de.yaml translations/dsb.yaml translations/el.yaml translations/en.yaml translations/eo.yaml translations/es-ES.yaml translations/es-MX.yaml translations/es.yaml translations/et.yaml translations/eu.yaml translations/fa.yaml translations/fi.yaml translations/fil.yaml translations/fr.yaml translations/fur.yaml translations/ga.yaml translations/gd.yaml translations/gl.yaml translations/grc.yaml translations/gu.yaml translations/ha.yaml translations/he.yaml translations/hi.yaml translations/hr.yaml translations/hsb.yaml translations/hu.yaml translations/hy.yaml translations/ia.yaml translations/id.yaml translations/is.yaml translations/it.yaml translations/ja.yaml translations/ka.yaml translations/km.yaml translations/kmr-Arab.yaml translations/kmr-Latn.yaml translations/kn.yaml translations/ko.yaml translations/la.yaml translations/lb.yaml translations/lo.yaml translations/lt.yaml translations/lv.yaml translations/mk.yaml translations/ml.yaml translations/mn.yaml translations/mr.yaml translations/ms.yaml translations/nb.yaml translations/nko.yaml translations/nl.yaml translations/nn.yaml translations/no.yaml translations/oc.yaml translations/or.yaml translations/pa.yaml translations/pl.yaml translations/pms.yaml translations/pt-BR.yaml translations/pt-PT.yaml translations/pt.yaml translations/rm.yaml translations/ro.yaml translations/ru.yaml translations/se.yaml translations/si.yaml translations/sk.yaml translations/sl.yaml translations/sq.yaml translations/sr-Cyrl.yaml translations/sr-Latn.yaml translations/sr.yaml translations/sv.yaml translations/ta.yaml translations/te.yaml translations/th.yaml translations/tk.yaml translations/tr.yaml translations/ua.yaml translations/ug.yaml translations/uk.yaml translations/ur.yaml translations/vi.yaml translations/zh-Hans.yaml translations/zh-Hant.yaml)'+ '-D[Format to print template for]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 fodt gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'+ '--print-default-template[Format to print template for]:FORMAT:(ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 fodt gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki)'+ '--print-default-data-file[Data file to print]:FILE:(MANUAL.txt abbreviations creole.lua default.csl docbook-entities.txt docx/[Content_Types].xml docx/_rels/.rels docx/docProps/app.xml docx/docProps/core.xml docx/docProps/custom.xml docx/word/_rels/document.xml.rels docx/word/_rels/footnotes.xml.rels docx/word/comments.xml docx/word/document.xml docx/word/fontTable.xml docx/word/footnotes.xml docx/word/numbering.xml docx/word/settings.xml docx/word/styles.xml docx/word/theme/theme1.xml docx/word/webSettings.xml dzslides/template.html epub.css init.lua odt/META-INF/manifest.xml odt/content.xml odt/manifest.rdf odt/meta.xml odt/mimetype odt/styles.xml pptx/[Content_Types].xml pptx/_rels/.rels pptx/docProps/app.xml pptx/docProps/core.xml pptx/ppt/_rels/presentation.xml.rels pptx/ppt/notesMasters/_rels/notesMaster1.xml.rels pptx/ppt/notesMasters/notesMaster1.xml pptx/ppt/notesSlides/_rels/notesSlide1.xml.rels pptx/ppt/notesSlides/_rels/notesSlide2.xml.rels pptx/ppt/notesSlides/notesSlide1.xml pptx/ppt/notesSlides/notesSlide2.xml pptx/ppt/presProps.xml pptx/ppt/presentation.xml pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels pptx/ppt/slideLayouts/slideLayout1.xml pptx/ppt/slideLayouts/slideLayout10.xml pptx/ppt/slideLayouts/slideLayout11.xml pptx/ppt/slideLayouts/slideLayout2.xml pptx/ppt/slideLayouts/slideLayout3.xml pptx/ppt/slideLayouts/slideLayout4.xml pptx/ppt/slideLayouts/slideLayout5.xml pptx/ppt/slideLayouts/slideLayout6.xml pptx/ppt/slideLayouts/slideLayout7.xml pptx/ppt/slideLayouts/slideLayout8.xml pptx/ppt/slideLayouts/slideLayout9.xml pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels pptx/ppt/slideMasters/slideMaster1.xml pptx/ppt/slides/_rels/slide1.xml.rels pptx/ppt/slides/_rels/slide2.xml.rels pptx/ppt/slides/_rels/slide3.xml.rels pptx/ppt/slides/_rels/slide4.xml.rels pptx/ppt/slides/slide1.xml pptx/ppt/slides/slide2.xml pptx/ppt/slides/slide3.xml pptx/ppt/slides/slide4.xml pptx/ppt/tableStyles.xml pptx/ppt/theme/theme1.xml pptx/ppt/theme/theme2.xml pptx/ppt/viewProps.xml reference.docx reference.odt reference.pptx templates/affiliations.jats templates/after-header-includes.latex templates/article.jats_publishing templates/common.latex templates/default.ansi templates/default.asciidoc templates/default.bbcode templates/default.beamer templates/default.biblatex templates/default.bibtex templates/default.chunkedhtml templates/default.commonmark templates/default.context templates/default.djot templates/default.docbook4 templates/default.docbook5 templates/default.dokuwiki templates/default.dzslides templates/default.epub2 templates/default.epub3 templates/default.haddock templates/default.html4 templates/default.html5 templates/default.icml templates/default.jats_archiving templates/default.jats_articleauthoring templates/default.jats_publishing templates/default.jira templates/default.latex templates/default.man templates/default.markdown templates/default.markua templates/default.mediawiki templates/default.ms templates/default.muse templates/default.opendocument templates/default.openxml templates/default.opml templates/default.org templates/default.plain templates/default.revealjs templates/default.rst templates/default.rtf templates/default.s5 templates/default.slideous templates/default.slidy templates/default.t2t templates/default.tei templates/default.texinfo templates/default.textile templates/default.typst templates/default.vimdoc templates/default.xwiki templates/default.zimwiki templates/document-metadata.latex templates/font-settings.latex templates/fonts.latex templates/hypersetup.latex templates/passoptions.latex templates/styles.citations.html templates/styles.html templates/template.typst translations/af.yaml translations/alt.yaml translations/am.yaml translations/ar.yaml translations/as.yaml translations/ast.yaml translations/az.yaml translations/be.yaml translations/bg.yaml translations/bn.yaml translations/bo.yaml translations/br.yaml translations/bs.yaml translations/bua.yaml translations/ca.yaml translations/ckb-Arab.yaml translations/ckb-Latn.yaml translations/cs.yaml translations/cu.yaml translations/cy.yaml translations/cz.yaml translations/da.yaml translations/de.yaml translations/dsb.yaml translations/el.yaml translations/en.yaml translations/eo.yaml translations/es-ES.yaml translations/es-MX.yaml translations/es.yaml translations/et.yaml translations/eu.yaml translations/fa.yaml translations/fi.yaml translations/fil.yaml translations/fr.yaml translations/fur.yaml translations/ga.yaml translations/gd.yaml translations/gl.yaml translations/grc.yaml translations/gu.yaml translations/ha.yaml translations/he.yaml translations/hi.yaml translations/hr.yaml translations/hsb.yaml translations/hu.yaml translations/hy.yaml translations/ia.yaml translations/id.yaml translations/is.yaml translations/it.yaml translations/ja.yaml translations/ka.yaml translations/km.yaml translations/kmr-Arab.yaml translations/kmr-Latn.yaml translations/kn.yaml translations/ko.yaml translations/la.yaml translations/lb.yaml translations/lo.yaml translations/lt.yaml translations/lv.yaml translations/mk.yaml translations/ml.yaml translations/mn.yaml translations/mr.yaml translations/ms.yaml translations/nb.yaml translations/nko.yaml translations/nl.yaml translations/nn.yaml translations/no.yaml translations/oc.yaml translations/or.yaml translations/pa.yaml translations/pl.yaml translations/pms.yaml translations/pt-BR.yaml translations/pt-PT.yaml translations/pt.yaml translations/rm.yaml translations/ro.yaml translations/ru.yaml translations/se.yaml translations/si.yaml translations/sk.yaml translations/sl.yaml translations/sq.yaml translations/sr-Cyrl.yaml translations/sr-Latn.yaml translations/sr.yaml translations/sv.yaml translations/ta.yaml translations/te.yaml translations/th.yaml translations/tk.yaml translations/tr.yaml translations/ua.yaml translations/ug.yaml translations/uk.yaml translations/ur.yaml translations/vi.yaml translations/zh-Hans.yaml translations/zh-Hant.yaml)' '--print-highlight-style[Highlighting style]:STYLE:(pygments tango espresso zenburn kate monochrome breezedark haddock)' '-v[Print version]' '--version[Print version]'@@ -266,8 +266,8 @@ ``` % pandoc --completion=fish ^D-complete -c pandoc -l from -d "Reader format" -r -a "asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml"-complete -c pandoc -l to -d "Writer format" -r -a "ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki"+complete -c pandoc -l from -d "Reader format" -r -a "asciidoc biblatex bibtex bits commonmark commonmark_x creole csljson csv djot docbook docx dokuwiki endnotexml epub fb2 fodt gfm haddock html ipynb jats jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict mdoc mediawiki muse native odt opml org pod pptx ris rst rtf t2t textile tikiwiki tsv twiki typst vimwiki xlsx xml"+complete -c pandoc -l to -d "Writer format" -r -a "ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 fodt gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki" complete -c pandoc -s o -l output -d "Output file" -r complete -c pandoc -l data-dir -d "Directory for data files" -r complete -c pandoc -s M -l metadata -d "Metadata field KEY=VALUE" -r@@ -370,8 +370,8 @@ complete -c pandoc -l list-extensions -d "List supported extensions" complete -c pandoc -l list-highlight-languages -d "List highlighting languages" complete -c pandoc -l list-highlight-styles -d "List highlighting styles"-complete -c pandoc -s D -l print-default-template -d "Format to print template for" -r -a "ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki"-complete -c pandoc -l print-default-data-file -d "Data file to print" -r -a "reference.docx reference.odt reference.pptx MANUAL.txt docx/_rels/.rels pptx/_rels/.rels abbreviations creole.lua default.csl docbook-entities.txt docx/[Content_Types].xml docx/docProps/app.xml docx/docProps/core.xml docx/docProps/custom.xml docx/word/_rels/document.xml.rels docx/word/_rels/footnotes.xml.rels docx/word/comments.xml docx/word/document.xml docx/word/fontTable.xml docx/word/footnotes.xml docx/word/numbering.xml docx/word/settings.xml docx/word/styles.xml docx/word/theme/theme1.xml docx/word/webSettings.xml dzslides/template.html epub.css init.lua odt/META-INF/manifest.xml odt/content.xml odt/manifest.rdf odt/meta.xml odt/mimetype odt/styles.xml pptx/[Content_Types].xml pptx/docProps/app.xml pptx/docProps/core.xml pptx/ppt/_rels/presentation.xml.rels pptx/ppt/notesMasters/_rels/notesMaster1.xml.rels pptx/ppt/notesMasters/notesMaster1.xml pptx/ppt/notesSlides/_rels/notesSlide1.xml.rels pptx/ppt/notesSlides/_rels/notesSlide2.xml.rels pptx/ppt/notesSlides/notesSlide1.xml pptx/ppt/notesSlides/notesSlide2.xml pptx/ppt/presProps.xml pptx/ppt/presentation.xml pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels pptx/ppt/slideLayouts/slideLayout1.xml pptx/ppt/slideLayouts/slideLayout10.xml pptx/ppt/slideLayouts/slideLayout11.xml pptx/ppt/slideLayouts/slideLayout2.xml pptx/ppt/slideLayouts/slideLayout3.xml pptx/ppt/slideLayouts/slideLayout4.xml pptx/ppt/slideLayouts/slideLayout5.xml pptx/ppt/slideLayouts/slideLayout6.xml pptx/ppt/slideLayouts/slideLayout7.xml pptx/ppt/slideLayouts/slideLayout8.xml pptx/ppt/slideLayouts/slideLayout9.xml pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels pptx/ppt/slideMasters/slideMaster1.xml pptx/ppt/slides/_rels/slide1.xml.rels pptx/ppt/slides/_rels/slide2.xml.rels pptx/ppt/slides/_rels/slide3.xml.rels pptx/ppt/slides/_rels/slide4.xml.rels pptx/ppt/slides/slide1.xml pptx/ppt/slides/slide2.xml pptx/ppt/slides/slide3.xml pptx/ppt/slides/slide4.xml pptx/ppt/tableStyles.xml pptx/ppt/theme/theme1.xml pptx/ppt/theme/theme2.xml pptx/ppt/viewProps.xml templates/affiliations.jats templates/after-header-includes.latex templates/article.jats_publishing templates/common.latex templates/default.ansi templates/default.asciidoc templates/default.bbcode templates/default.beamer templates/default.biblatex templates/default.bibtex templates/default.chunkedhtml templates/default.commonmark templates/default.context templates/default.djot templates/default.docbook4 templates/default.docbook5 templates/default.dokuwiki templates/default.dzslides templates/default.epub2 templates/default.epub3 templates/default.haddock templates/default.html4 templates/default.html5 templates/default.icml templates/default.jats_archiving templates/default.jats_articleauthoring templates/default.jats_publishing templates/default.jira templates/default.latex templates/default.man templates/default.markdown templates/default.markua templates/default.mediawiki templates/default.ms templates/default.muse templates/default.opendocument templates/default.openxml templates/default.opml templates/default.org templates/default.plain templates/default.revealjs templates/default.rst templates/default.rtf templates/default.s5 templates/default.slideous templates/default.slidy templates/default.t2t templates/default.tei templates/default.texinfo templates/default.textile templates/default.typst templates/default.vimdoc templates/default.xwiki templates/default.zimwiki templates/document-metadata.latex templates/font-settings.latex templates/fonts.latex templates/hypersetup.latex templates/passoptions.latex templates/styles.citations.html templates/styles.html templates/template.typst translations/af.yaml translations/alt.yaml translations/am.yaml translations/ar.yaml translations/as.yaml translations/ast.yaml translations/az.yaml translations/be.yaml translations/bg.yaml translations/bn.yaml translations/bo.yaml translations/br.yaml translations/bs.yaml translations/bua.yaml translations/ca.yaml translations/ckb-Arab.yaml translations/ckb-Latn.yaml translations/cs.yaml translations/cu.yaml translations/cy.yaml translations/cz.yaml translations/da.yaml translations/de.yaml translations/dsb.yaml translations/el.yaml translations/en.yaml translations/eo.yaml translations/es-ES.yaml translations/es-MX.yaml translations/es.yaml translations/et.yaml translations/eu.yaml translations/fa.yaml translations/fi.yaml translations/fil.yaml translations/fr.yaml translations/fur.yaml translations/ga.yaml translations/gd.yaml translations/gl.yaml translations/grc.yaml translations/gu.yaml translations/ha.yaml translations/he.yaml translations/hi.yaml translations/hr.yaml translations/hsb.yaml translations/hu.yaml translations/hy.yaml translations/ia.yaml translations/id.yaml translations/is.yaml translations/it.yaml translations/ja.yaml translations/ka.yaml translations/km.yaml translations/kmr-Arab.yaml translations/kmr-Latn.yaml translations/kn.yaml translations/ko.yaml translations/la.yaml translations/lb.yaml translations/lo.yaml translations/lt.yaml translations/lv.yaml translations/mk.yaml translations/ml.yaml translations/mn.yaml translations/mr.yaml translations/ms.yaml translations/nb.yaml translations/nko.yaml translations/nl.yaml translations/nn.yaml translations/no.yaml translations/oc.yaml translations/or.yaml translations/pa.yaml translations/pl.yaml translations/pms.yaml translations/pt-BR.yaml translations/pt-PT.yaml translations/pt.yaml translations/rm.yaml translations/ro.yaml translations/ru.yaml translations/se.yaml translations/si.yaml translations/sk.yaml translations/sl.yaml translations/sq.yaml translations/sr-Cyrl.yaml translations/sr-Latn.yaml translations/sr.yaml translations/sv.yaml translations/ta.yaml translations/te.yaml translations/th.yaml translations/tk.yaml translations/tr.yaml translations/ua.yaml translations/ug.yaml translations/uk.yaml translations/ur.yaml translations/vi.yaml translations/zh-Hans.yaml translations/zh-Hant.yaml"+complete -c pandoc -s D -l print-default-template -d "Format to print template for" -r -a "ansi asciidoc asciidoc_legacy asciidoctor bbcode bbcode_fluxbb bbcode_hubzilla bbcode_phpbb bbcode_steam bbcode_xenforo beamer biblatex bibtex chunkedhtml commonmark commonmark_x context csljson djot docbook docbook4 docbook5 docx dokuwiki dzslides epub epub2 epub3 fb2 fodt gfm haddock html html4 html5 icml ipynb jats jats_archiving jats_articleauthoring jats_publishing jira json latex man markdown markdown_github markdown_mmd markdown_phpextra markdown_strict markua mediawiki ms muse native odt opendocument opml org pdf plain pptx revealjs rst rtf s5 slideous slidy t2t tei texinfo textile typst vimdoc xml xwiki zimwiki"+complete -c pandoc -l print-default-data-file -d "Data file to print" -r -a "MANUAL.txt abbreviations creole.lua default.csl docbook-entities.txt docx/[Content_Types].xml docx/_rels/.rels docx/docProps/app.xml docx/docProps/core.xml docx/docProps/custom.xml docx/word/_rels/document.xml.rels docx/word/_rels/footnotes.xml.rels docx/word/comments.xml docx/word/document.xml docx/word/fontTable.xml docx/word/footnotes.xml docx/word/numbering.xml docx/word/settings.xml docx/word/styles.xml docx/word/theme/theme1.xml docx/word/webSettings.xml dzslides/template.html epub.css init.lua odt/META-INF/manifest.xml odt/content.xml odt/manifest.rdf odt/meta.xml odt/mimetype odt/styles.xml pptx/[Content_Types].xml pptx/_rels/.rels pptx/docProps/app.xml pptx/docProps/core.xml pptx/ppt/_rels/presentation.xml.rels pptx/ppt/notesMasters/_rels/notesMaster1.xml.rels pptx/ppt/notesMasters/notesMaster1.xml pptx/ppt/notesSlides/_rels/notesSlide1.xml.rels pptx/ppt/notesSlides/_rels/notesSlide2.xml.rels pptx/ppt/notesSlides/notesSlide1.xml pptx/ppt/notesSlides/notesSlide2.xml pptx/ppt/presProps.xml pptx/ppt/presentation.xml pptx/ppt/slideLayouts/_rels/slideLayout1.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout10.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout11.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout2.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout3.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout4.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout5.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout6.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout7.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout8.xml.rels pptx/ppt/slideLayouts/_rels/slideLayout9.xml.rels pptx/ppt/slideLayouts/slideLayout1.xml pptx/ppt/slideLayouts/slideLayout10.xml pptx/ppt/slideLayouts/slideLayout11.xml pptx/ppt/slideLayouts/slideLayout2.xml pptx/ppt/slideLayouts/slideLayout3.xml pptx/ppt/slideLayouts/slideLayout4.xml pptx/ppt/slideLayouts/slideLayout5.xml pptx/ppt/slideLayouts/slideLayout6.xml pptx/ppt/slideLayouts/slideLayout7.xml pptx/ppt/slideLayouts/slideLayout8.xml pptx/ppt/slideLayouts/slideLayout9.xml pptx/ppt/slideMasters/_rels/slideMaster1.xml.rels pptx/ppt/slideMasters/slideMaster1.xml pptx/ppt/slides/_rels/slide1.xml.rels pptx/ppt/slides/_rels/slide2.xml.rels pptx/ppt/slides/_rels/slide3.xml.rels pptx/ppt/slides/_rels/slide4.xml.rels pptx/ppt/slides/slide1.xml pptx/ppt/slides/slide2.xml pptx/ppt/slides/slide3.xml pptx/ppt/slides/slide4.xml pptx/ppt/tableStyles.xml pptx/ppt/theme/theme1.xml pptx/ppt/theme/theme2.xml pptx/ppt/viewProps.xml reference.docx reference.odt reference.pptx templates/affiliations.jats templates/after-header-includes.latex templates/article.jats_publishing templates/common.latex templates/default.ansi templates/default.asciidoc templates/default.bbcode templates/default.beamer templates/default.biblatex templates/default.bibtex templates/default.chunkedhtml templates/default.commonmark templates/default.context templates/default.djot templates/default.docbook4 templates/default.docbook5 templates/default.dokuwiki templates/default.dzslides templates/default.epub2 templates/default.epub3 templates/default.haddock templates/default.html4 templates/default.html5 templates/default.icml templates/default.jats_archiving templates/default.jats_articleauthoring templates/default.jats_publishing templates/default.jira templates/default.latex templates/default.man templates/default.markdown templates/default.markua templates/default.mediawiki templates/default.ms templates/default.muse templates/default.opendocument templates/default.openxml templates/default.opml templates/default.org templates/default.plain templates/default.revealjs templates/default.rst templates/default.rtf templates/default.s5 templates/default.slideous templates/default.slidy templates/default.t2t templates/default.tei templates/default.texinfo templates/default.textile templates/default.typst templates/default.vimdoc templates/default.xwiki templates/default.zimwiki templates/document-metadata.latex templates/font-settings.latex templates/fonts.latex templates/hypersetup.latex templates/passoptions.latex templates/styles.citations.html templates/styles.html templates/template.typst translations/af.yaml translations/alt.yaml translations/am.yaml translations/ar.yaml translations/as.yaml translations/ast.yaml translations/az.yaml translations/be.yaml translations/bg.yaml translations/bn.yaml translations/bo.yaml translations/br.yaml translations/bs.yaml translations/bua.yaml translations/ca.yaml translations/ckb-Arab.yaml translations/ckb-Latn.yaml translations/cs.yaml translations/cu.yaml translations/cy.yaml translations/cz.yaml translations/da.yaml translations/de.yaml translations/dsb.yaml translations/el.yaml translations/en.yaml translations/eo.yaml translations/es-ES.yaml translations/es-MX.yaml translations/es.yaml translations/et.yaml translations/eu.yaml translations/fa.yaml translations/fi.yaml translations/fil.yaml translations/fr.yaml translations/fur.yaml translations/ga.yaml translations/gd.yaml translations/gl.yaml translations/grc.yaml translations/gu.yaml translations/ha.yaml translations/he.yaml translations/hi.yaml translations/hr.yaml translations/hsb.yaml translations/hu.yaml translations/hy.yaml translations/ia.yaml translations/id.yaml translations/is.yaml translations/it.yaml translations/ja.yaml translations/ka.yaml translations/km.yaml translations/kmr-Arab.yaml translations/kmr-Latn.yaml translations/kn.yaml translations/ko.yaml translations/la.yaml translations/lb.yaml translations/lo.yaml translations/lt.yaml translations/lv.yaml translations/mk.yaml translations/ml.yaml translations/mn.yaml translations/mr.yaml translations/ms.yaml translations/nb.yaml translations/nko.yaml translations/nl.yaml translations/nn.yaml translations/no.yaml translations/oc.yaml translations/or.yaml translations/pa.yaml translations/pl.yaml translations/pms.yaml translations/pt-BR.yaml translations/pt-PT.yaml translations/pt.yaml translations/rm.yaml translations/ro.yaml translations/ru.yaml translations/se.yaml translations/si.yaml translations/sk.yaml translations/sl.yaml translations/sq.yaml translations/sr-Cyrl.yaml translations/sr-Latn.yaml translations/sr.yaml translations/sv.yaml translations/ta.yaml translations/te.yaml translations/th.yaml translations/tk.yaml translations/tr.yaml translations/ua.yaml translations/ug.yaml translations/uk.yaml translations/ur.yaml translations/vi.yaml translations/zh-Hans.yaml translations/zh-Hant.yaml" complete -c pandoc -l print-highlight-style -d "Highlighting style" -r -a "pygments tango espresso zenburn kate monochrome breezedark haddock" complete -c pandoc -s v -l version -d "Print version" complete -c pandoc -s h -l help -d "Show help"
@@ -0,0 +1,15 @@+Dates that cannot be normalized are skipped, so that `dc:date`+is not empty.++```+% pandoc -t epub3 -o - | pandoc -f epub -t plain -s+---+title: T+date: [10 septembre 2026, 2026-09-10]+---+^D+T+2026-09-10++T+```
@@ -0,0 +1,22 @@+The identifier of the title header added before initial content+must not clash with an existing one, even inside a div.++```+% pandoc -t epub3 --metadata title=Intro -o - | pandoc -f epub -t html+See [Intro](#intro).++::: box+# Intro+:::+^D+<p><span id="title_page.xhtml"></span></p>+<p><span id="ch001.xhtml"></span></p>+<section id="ch001.xhtml_intro-1" class="unnumbered level1">+<h1 class="unnumbered">Intro</h1>+<p>See <a href="#ch002.xhtml_intro">Intro</a>.</p>+</section>+<p><span id="ch002.xhtml"></span></p>+<section id="ch002.xhtml_intro" class="level1 box">+<h1>Intro</h1>+</section>+```
@@ -0,0 +1,126 @@+Round trips through the flat OpenDocument format. The flat format has no+zip structure, so a relative link is not rewritten, and an image is+embedded as base64 in an `office:binary-data` element rather than being+put in a `Pictures/` entry.++```+% pandoc -f markdown -t fodt | pandoc -f fodt -t native+# A *heading*++Inline $x^2$, display:++$$y = 1$$++| a | b |+|---|---|+| 1 | 2 |++1. one+2. two++A footnote[^1] and a [relative link](foo/bar.html).++[^1]: Note text.+^D+[ Header+ 1+ ( "a-heading" , [] , [] )+ [ Span ( "anchor" , [] , [] ) []+ , Str "A"+ , Space+ , Emph [ Str "heading" ]+ ]+, Para+ [ Str "Inline"+ , Space+ , Math DisplayMath "x^{2}"+ , Str ","+ , Space+ , Str "display:"+ ]+, Para [ Math DisplayMath "y = 1" ]+, Table+ ( "" , [] , [] )+ (Caption Nothing [])+ [ ( AlignDefault , ColWidthDefault )+ , ( AlignDefault , ColWidthDefault )+ ]+ (TableHead+ ( "" , [] , [] )+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Strong [ Str "a" ] ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Strong [ Str "b" ] ] ]+ ]+ ])+ [ TableBody+ ( "" , [] , [] )+ (RowHeadColumns 0)+ []+ [ Row+ ( "" , [] , [] )+ [ Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "1" ] ]+ , Cell+ ( "" , [] , [] )+ AlignDefault+ (RowSpan 1)+ (ColSpan 1)+ [ Plain [ Str "2" ] ]+ ]+ ]+ ]+ (TableFoot ( "" , [] , [] ) [])+, OrderedList+ ( 1 , Decimal , Period )+ [ [ Plain [ Str "one" ] ] , [ Plain [ Str "two" ] ] ]+, Para+ [ Str "A"+ , Space+ , Str "footnote"+ , Note [ Para [ Str "Note" , Space , Str "text." ] ]+ , Space+ , Str "and"+ , Space+ , Str "a"+ , Space+ , Link+ ( "" , [] , [] )+ [ Str "relative" , Space , Str "link" ]+ ( "foo/bar.html" , "" )+ , Str "."+ ]+]+```++The image data survives the base64 round trip.++```+% pandoc -f markdown -t fodt | pandoc -f fodt -t native++^D+[ Para+ [ Image+ ( ""+ , []+ , [ ( "width" , "150.0pt" ) , ( "height" , "150.0pt" ) ]+ )+ []+ ( "Pictures/image1.jpg" , "" )+ ]+]+```
@@ -0,0 +1,38 @@+State changes made while parsing an optional argument (which is not+a TeX group) should persist, instead of being discarded with the+sub-parse.++```+% pandoc -f latex -t native+\begin{description}+\item[\def\x{Y}k] b \x+\end{description}+^D+[ DefinitionList+ [ ( [ Str "k" ]+ , [ [ Para [ Str "b" , Space , Str "Y" ] ] ]+ )+ ]+]+```++```+% pandoc -f latex -t native+\cite[\def\x{q}]{a}\x+^D+[ Para+ [ Cite+ [ Citation+ { citationId = "a"+ , citationPrefix = []+ , citationSuffix = []+ , citationMode = NormalCitation+ , citationNoteNum = 0+ , citationHash = 0+ }+ ]+ [ RawInline (Format "latex") "\\cite[\\def\\x{q}]{a}" ]+ , Str "q"+ ]+]+```
@@ -0,0 +1,13 @@+Source positions of tokens following `##` should not be off by one.+The skipped `\zzz ` spans columns 4-8, so the position after it is+column 9.++```+% pandoc -f latex -t plain --verbose+## \zzz hi+^D+2> [INFO] Parsing unescaped '#' at line 1 column 1+2> [INFO] Parsing unescaped '#' at line 1 column 2+2> [INFO] Skipped '\zzz ' at line 1 column 9+## hi+```
@@ -0,0 +1,12 @@+Tokens produced by retokenizing a comment inside a URL argument+should keep their original source positions. Here the `}` and+`\zzz` come from the retokenized comment; the skipped `\zzz ` spans+columns 12-16, so the position after it is column 17.++```+% pandoc -f latex -t plain --verbose+\url{ab%cd}\zzz x+^D+2> [INFO] Skipped '\zzz ' at line 1 column 17+ab%cdx+```
@@ -0,0 +1,473 @@+Tests for LaTeX3 (xparse) document command support.++`o` specifier and `\IfNoValueTF`:++```+% pandoc -f latex -t plain+\NewDocumentCommand{\name}{o m}{\IfNoValueTF{#1}{#2}{#1: #2}}+\name{Alice}++\name[Dr]{Bob}+^D+Alice++Dr: Bob+```++`O` specifier with default:++```+% pandoc -f latex -t plain+\NewDocumentCommand\greet{O{Hello} m}{#1, #2!}+\greet{world}++\greet[Hi]{there}+^D+Hello, world!++Hi, there!+```++`s` specifier and `\IfBooleanTF`:++```+% pandoc -f latex -t latex+\NewDocumentCommand\maybeemph{s m}{\IfBooleanTF{#1}{\emph{#2}}{#2}}+\maybeemph{plain} and \maybeemph*{starred}+^D+plain and \emph{starred}+```++`t` specifier:++```+% pandoc -f latex -t plain+\NewDocumentCommand\q{t+ m}{\IfBooleanTF{#1}{plus #2}{noplus #2}}+\q+{a} \q{b}+^D+plus a noplus b+```++`d` specifier with custom delimiters:++```+% pandoc -f latex -t plain+\NewDocumentCommand\point{d()}{\IfNoValueTF{#1}{origin}{(#1)}}+\point, \point(3,4)+^D+origin, (3,4)+```++`r` specifier (required, custom delimiters):++```+% pandoc -f latex -t plain+\NewDocumentCommand\req{r()}{req #1}+\req(ok)+^D+req ok+```++`\DeclareDocumentCommand` redefines silently:++```+% pandoc -f latex -t plain+\DeclareDocumentCommand\x{m}{first: #1}+\DeclareDocumentCommand\x{m}{second: #1}+\x{y}+^D+second: y+```++`\ProvideDocumentCommand` does not redefine:++```+% pandoc -f latex -t plain+\NewDocumentCommand\y{m}{first: #1}+\ProvideDocumentCommand\y{m}{second: #1}+\y{z}+^D+first: z+```++A `-NoValue-` marker that leaks into the document is printed+like in xparse:++```+% pandoc -f latex -t plain+\NewDocumentCommand\val{o}{value: #1}+\val+^D+value: -NoValue-+```++Nested brackets in optional arguments:++```+% pandoc -f latex -t plain+\NewDocumentCommand\opt{o}{got #1}+\opt[a[b]c]+^D+got a[b]c+```++`\IfValueT`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\test{o}{\IfValueT{#1}{seen #1}}+\test[x] \test+^D+seen x+```++Definitions pass through as raw LaTeX when `latex_macros`+is disabled:++```+% pandoc -f markdown-latex_macros -t markdown+raw_tex-raw_attribute+\NewDocumentCommand{\my}{m}{\emph{#1}}+\my{hi}+^D+\NewDocumentCommand{\my}{m}{\emph{#1}}+\my{hi}+```++`\NewDocumentEnvironment`: arguments are available in the+end code, too:++```+% pandoc -f latex -t plain+\NewDocumentEnvironment{titled}{m}{start #1.}{end #1.}+\begin{titled}{T}+middle+\end{titled}+^D+start T. middle end T.+```++Environments with arguments nest properly:++```+% pandoc -f latex -t plain+\NewDocumentEnvironment{wrap}{m}{[#1 }{ #1]}+\begin{wrap}{a}+one+\begin{wrap}{b}+two+\end{wrap}+three+\end{wrap}+^D+[a one [b two b] three a]+```++Environment wrapping another environment, with optional+argument:++```+% pandoc -f latex -t latex+\NewDocumentEnvironment{myquote}{o}+ {\begin{quote}\IfValueT{#1}{\textbf{#1}: }}+ {\end{quote}}+\begin{myquote}[Note]+Hello.+\end{myquote}+^D+\begin{quote}+\textbf{Note}: Hello.+\end{quote}+```++`O` specifier with default in environments:++```+% pandoc -f latex -t plain+\NewDocumentEnvironment{sec}{O{A} m}{(#1/#2 }{ #2)}+\begin{sec}[B]{t}+body+\end{sec}++\begin{sec}{u}+more+\end{sec}+^D+(B/t body t)++(A/u more u)+```++`b` specifier grabs the environment body, trimming+surrounding spaces:++```+% pandoc -f latex -t plain+\NewDocumentEnvironment{titledb}{m b}{Title: #1. Body: #2.}{}+\begin{titledb}{T}+some text+\end{titledb}+^D+Title: T. Body: some text.+```++`\DeclareDocumentEnvironment` redefines silently:++```+% pandoc -f latex -t plain+\DeclareDocumentEnvironment{z}{}{one}{}+\DeclareDocumentEnvironment{z}{}{two}{}+\begin{z}x\end{z}+^D+twox+```++`\ProvideDocumentEnvironment` does not redefine:++```+% pandoc -f latex -t plain+\NewDocumentEnvironment{p}{}{one}{}+\ProvideDocumentEnvironment{p}{}{two}{}+\begin{p}x\end{p}+^D+onex+```++`v` specifier (verbatim), with `\verb`-style delimiters or+braces; special characters are not reinterpreted:++```+% pandoc -f latex -t plain+\NewDocumentCommand\showv{v}{[#1]}+\showv|a_b&\foo|++\showv{xy}+^D+[a_b&\foo]++[xy]+```++`e` specifier (embellishments), matched in any order:++```+% pandoc -f latex -t plain+\NewDocumentCommand\x{e{^_}}{sup=#1, sub=#2.}+\x^{up}_{down}++\x_{down}^{up}++\x_{d}+^D+sup=up, sub=down.++sup=up, sub=down.++sup=-NoValue-, sub=d.+```++`e` specifier with `\IfNoValueTF` and single-token argument:++```+% pandoc -f latex -t plain+\NewDocumentCommand\z{e{^}}{\IfNoValueTF{#1}{none}{got #1}}+\z^2 \z+^D+got 2 none+```++`E` specifier (embellishments with defaults):++```+% pandoc -f latex -t plain+\NewDocumentCommand\y{E{^_}{{U}{D}}}{#1/#2}+\y^{a}++\y+^D+a/D++U/D+```++`\TrimSpaces` argument processor:++```+% pandoc -f latex -t plain+\NewDocumentCommand\trim{>{\TrimSpaces}m}{[#1]}+\trim{ hi }+^D+[hi]+```++`\ReverseBoolean` argument processor:++```+% pandoc -f latex -t plain+\NewDocumentCommand\rs{>{\ReverseBoolean}s}{\IfBooleanTF{#1}{T}{F}}+\rs* \rs+^D+F T+```++`\SplitArgument` processor: splits into braced groups, trims+spaces, pads with `-NoValue-`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\three{mmm}{1=#1, 2=#2, 3=#3.}+\NewDocumentCommand\splt{>{\SplitArgument{2}{;}}m}{\three#1}+\splt{a; b ;c}++\splt{a;b}+^D+1=a, 2=b, 3=c.++1=a, 2=b, 3=-NoValue-.+```++`\SplitList` processor:++```+% pandoc -f latex -t plain+\NewDocumentCommand\lst{>{\SplitList{,}}m}{items: #1}+\lst{x, y ,z}+^D+items: xyz+```++Multiple processors are applied starting from the one nearest+the argument specifier:++```+% pandoc -f latex -t plain+\NewDocumentCommand\two{mm}{(#1)(#2)}+\NewDocumentCommand\c{>{\SplitArgument{1}{;}}>{\TrimSpaces}m}{\two#1}+\c{ a;b }+^D+(a)(b)+```++Unknown processors are skipped, keeping the argument:++```+% pandoc -f latex -t plain+\NewDocumentCommand\u{>{\SomeUnknownProc}m}{[#1]}+\u{ok}+^D+[ok]+```++`\NewCommandCopy` snapshots the definition (like `\let`):++```+% pandoc -f latex -t plain+\NewDocumentCommand\old{m}{orig: #1}+\NewCommandCopy\new\old+\RenewDocumentCommand\old{m}{changed: #1}+\new{a} \old{b}+^D+orig: a changed: b+```++`\NewEnvironmentCopy`, copying a built-in environment:++```+% pandoc -f latex -t latex+\NewEnvironmentCopy{myquote}{quote}+\begin{myquote}+Hello.+\end{myquote}+^D+\begin{quote}+Hello.+\end{quote}+```++`\MakeTitlecase` uppercases the first character:++```+% pandoc -f latex -t plain+\MakeTitlecase{hello world} \MakeTitlecase{\emph{foo bar}}+^D+Hello world Foo bar+```++Code between `\ExplSyntaxOn` and `\ExplSyntaxOff` is+tokenized with `:` and `_` as letters, so it can be skipped+cleanly:++```+% pandoc -f latex -t plain+\ExplSyntaxOn+\tl_new:N \l_my_tl+\tl_set:Nn \l_my_tl {stuff}+\ExplSyntaxOff+Text after.+^D+Text after.+```++`\ShowCommand` and the key–value commands are parsed and+ignored:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foo{m}{x#1}+\ShowCommand\foo+\DeclareKeys[fam]{key .store = \myval}+\SetKeys[fam]{key=5}+\ProcessKeyOptions[fam]+Done.+^D+Done.+```++`\inteval`: `+ - * /` with division rounding to nearest (ties+away from zero, as in eTeX):++```+% pandoc -f latex -t plain+\inteval{2+3*4}, \inteval{7/2}, \inteval{-7/2}, \inteval{(1+2)*3}+^D+14, 4, -4, 9+```++Macros in the evaluator argument are expanded:++```+% pandoc -f latex -t plain+\NewDocumentCommand\n{}{4}+\inteval{\n + 1}+^D+5+```++`\fpeval`: floating point expressions; results are rounded to+16 significant digits as in l3fp:++```+% pandoc -f latex -t plain+\fpeval{2^10/8}, \fpeval{0.1 + 0.2}, \fpeval{2**3}, \fpeval{1e3*2}, \fpeval{1/3}+^D+128, 0.3, 8, 2000, 0.3333333333333333+```++Expressions that cannot be evaluated are passed through as+text:++```+% pandoc -f latex -t plain+\fpeval{foo(1)}, \fpeval{1/0}, \inteval{1/0}+^D+foo(1), 1/0, 1/0+```++`\dimeval` and `\skipeval` substitute their (unevaluated)+expression:++```+% pandoc -f latex -t plain+\dimeval{2pt+3pt}, \skipeval{1em plus 2pt}+^D+2pt+3pt, 1em plus 2pt+```
@@ -0,0 +1,453 @@+Tests corresponding to the examples in the LaTeX usrguide+(<https://www.latex-project.org/help/documentation/usrguide.pdf>).++§2.2: the `\chapter`-like example with `s o m`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\mychapter{s o m}+ {\IfBooleanTF{#1}{star: #3}{normal: \IfNoValueTF{#2}{#3}{#2} / #3}\par}+\mychapter{One}+\mychapter*{Two}+\mychapter[Short]{Long}+^D+normal: One / One++star: Two++normal: Short / Long+```++§2.3: spaces around the environment name are ignored:++```+% pandoc -f latex -t plain+\NewDocumentEnvironment{ foo }{}{[}{]}+\begin{foo}x\end{foo}+^D+[x]+```++§2.5: optional arguments may safely be nested:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foo{om}{I grabbed (#1) and (#2)}+\NewDocumentCommand\baz{o}{#1-#1}+\foo[\baz[stuff]]{more stuff}+^D+I grabbed (stuff-stuff) and (more stuff)+```++§2.5: the default for `O` can be the result of grabbing another+argument:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foo{O{#2} m}{opt=#1, mand=#2.}+\foo{A}++\foo[B]{A}+^D+opt=A, mand=A.++opt=B, mand=A.+```++§2.6: spaces before optional arguments are allowed by default,+but not with the `!` modifier:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foobar{m o}{(#1|#2)}+\NewDocumentCommand\foobang{m !o}{(#1|#2)}+\foobar{arg1} [arg2]++\foobang{arg1} [arg2]+^D+(arg1|arg2)++(arg1|-NoValue-) [arg2]+```++§2.8: `\IfNoValueTF`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foo{o m}+ {%+ \IfNoValueTF {#1}%+ {just #2}%+ {both #1 and #2}}+\foo{m}, \foo[o]{m}+^D+just m, both o and m+```++§2.8: the `\IfBlankTF` example, including the effect of `!` on+the last `\foo`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foo{m!o}{\par #1:+ \IfNoValueTF{#2}+ {No optional}%+ {%+ \IfBlankTF{#2}+ {Blanks in or empty}%+ {Real content in}%+ }%+ \space argument!}+\foo{1}[bar] \foo{2}[ ] \foo{3}[] \foo{4}[\space] \foo{5} [x]+^D+1: Real content in argument!++2: Blanks in or empty argument!++3: Blanks in or empty argument!++4: Real content in argument!++5: No optional argument! [x]+```++§2.8: `\IfBooleanTF`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foo{sm}+ {%+ \IfBooleanTF {#1}%+ {with star: #2}%+ {without star: #2}}+\foo{a}, \foo*{b}+^D+without star: a, with star: b+```++§2.9: the `=` modifier (key–value interface for an argument) is+accepted:++```+% pandoc -f latex -t plain+\DeclareDocumentCommand\mycaption{s ={short-text} +O{#3} +m}{[#2|#3]}+\mycaption{Text}+\mycaption[Short]{Long}+^D+[Text|Text] [Short|Long]+```++§2.10: `\SplitArgument` splits into a fixed number of parts,+trimming spaces, padding with `-NoValue-`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\InternalFunctionOfThreeArguments{mmm}{1=#1, 2=#2, 3=#3.}+\NewDocumentCommand\foo{>{\SplitArgument{2}{;}} m}+ {\InternalFunctionOfThreeArguments#1}+\foo{a ; b ; c}++\foo{a;b}+^D+1=a, 2=b, 3=c.++1=a, 2=b, 3=-NoValue-.+```++§2.10: a processor applied to an `e`-type argument applies to+all of its arguments:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foo{ >{\TrimSpaces} e{_^} }+ { [#1](#2) }+\foo_{ a }^{ b }+^D+[a](b)+```++§2.10: `\SplitList` and `\ProcessList`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\SomeDocumentCommand{m}{<#1>}+\NewDocumentCommand\foo{>{\SplitList{;}} m}+ {\ProcessList{#1}{\SomeDocumentCommand}}+\foo{a; b ;c}+^D+<a><b><c>+```++§2.10: `\ProcessList` with arbitrary tokens expecting one+argument:++```+% pandoc -f latex -t plain+\NewDocumentCommand\SomeDocumentCommand{m}{<#1>}+\NewDocumentCommand\foo{>{\SplitList{;}} m}+ {\ProcessList{#1}{Abc \SomeDocumentCommand}}+\foo{a;b}+^D+Abc <a>Abc <b>+```++§2.10: `\ReverseBoolean`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foo{>{\ReverseBoolean} s m}+ {%+ \IfBooleanTF#1%+ {without star: #2}%+ {with star: #2}}+\foo{a}, \foo*{b}+^D+without star: a, with star: b+```++§2.10: `\TrimSpaces`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\foo{>{\TrimSpaces} m}{[#1]}+\foo{ hello world }+^D+[hello world]+```++§2.11: the `b` argument type grabs the body of the environment;+spaces are trimmed at both ends:++```+% pandoc -f latex -t html+\NewDocumentEnvironment{twice}{O{\ttfamily} +b}+ {#2#1#2} {}+\begin{twice}[\itshape]+ Hello world!+\end{twice}+^D+<p>Hello world!<em>Hello world!</em></p>+```++§2.12–2.13: `\NewExpandableDocumentCommand`, wrapping+`\multicolumn` in a tabular cell:++```+% pandoc -f latex -t plain+\NewExpandableDocumentCommand\MyMultiCol{m}{\multicolumn{3}{c}{#1}}+\begin{tabular}{lcr}+a & b & c \\+\MyMultiCol{stuff} \\+\end{tabular}+^D++:----+:---:+----:++| a | b | c |++-----+-----+-----++| stuff |++-----------------++```++§2.16: the `c` argument type grabs the body of the environment+verbatim; it is typeset with spaces as visible spaces (U+2423):++```+% pandoc -f latex -t plain+\NewDocumentEnvironment{MyVerbatim}{!O{\ttfamily} c}+ {\begin{center} #1 #2\end{center}} {}+\begin{MyVerbatim}[\ttfamily\itshape]+ % Some code is shown here+ $y = mx + c$+\end{MyVerbatim}+^D+␣␣%␣Some␣code␣is␣shown␣here+␣␣$y␣=␣mx␣+␣c$+```++§3: `\NewCommandCopy` copies a definition, so the original can+be redefined in terms of the copy:++```+% pandoc -f latex -t plain+\NewDocumentCommand\hi{}{Hello!}+\NewCommandCopy\hiorig\hi+\RenewDocumentCommand\hi{}{\hiorig{} And again: \hiorig}+\hi+^D+Hello! And again: Hello!+```++§4: `\UseName` turns a string into a csname and executes it:++```+% pandoc -f latex -t plain+\NewDocumentCommand\hello{}{Hello!}+\UseName{hello}+^D+Hello!+```++§4: `\ExpandArgs{c}` constructs a command name for+`\NewDocumentCommand`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\newnoter{m}+ {\ExpandArgs{c}\NewDocumentCommand{#1}{m}{#1 note: ##1.}}+\newnoter{todo}+\todo{fix this}+^D+todo note: fix this.+```++§4: `\ExpandArgs{cc}` with `\NewCommandCopy`, copying a command+by string name:++```+% pandoc -f latex -t plain+\NewDocumentCommand\savebyname{m}+ {\ExpandArgs{cc}\NewCommandCopy{saved#1}{#1}}+\NewDocumentCommand\greeting{}{Hello!}+\savebyname{greeting}+\RenewDocumentCommand\greeting{}{Goodbye!}+\greeting{} \UseName{savedgreeting}+^D+Goodbye! Hello!+```++§4: `\ExpandArgs{Nc}`:++```+% pandoc -f latex -t plain+\NewDocumentCommand\target{}{found}+\ExpandArgs{Nc}\NewCommandCopy\mycopy{target}+\mycopy+^D+found+```++§5: the `\fpeval` example:++```+% pandoc -f latex -t plain+\LaTeX{} can now compute: $\fpeval{sin(3.5)/2 + 2e-3}$.+^D+LaTeX can now compute: −0.1733916138448099.+```++§5: assorted `\fpeval` operations:++```+% pandoc -f latex -t plain+\fpeval{pi}, \fpeval{sqrt 2}, \fpeval{exp(0)}, \fpeval{max(1,2,3)},+\fpeval{fact(5)}, \fpeval{abs(-4)}, \fpeval{round(2.345,2)},+\fpeval{floor(2.7)}, \fpeval{ceil(2.2)}, \fpeval{trunc(-2.7)}+^D+3.141592653589793, 1.414213562373095, 1, 3, 120, 4, 2.34, 2, 3, -2+```++§5: the `\inteval` example:++```+% pandoc -f latex -t plain+\LaTeX{} can now compute: The sum of the numbers is $\inteval{1 + 2 + 3}$.+^D+LaTeX can now compute: The sum of the numbers is 6.+```++§6: `\expandableinput` reads a file like `\input`:++```+% pandoc -f latex -t html+Before. \expandableinput{command/bar}+^D+<p>Before. <em>hi there</em></p>+```++§7: `\MakeUppercase`, `\MakeLowercase`, `\MakeTitlecase`:++```+% pandoc -f latex -t plain+\MakeUppercase{hello WORLD ßüé}++\MakeLowercase{hello WORLD ßüé}++\MakeTitlecase{hello WORLD ßüé}+^D+HELLO WORLD SSÜÉ++hello world ßüé++Hello WORLD ßüé+```++§7: the `words` option of `\MakeTitlecase`:++```+% pandoc -f latex -t plain+\MakeTitlecase[words = first]{some words}+\MakeTitlecase[words = all]{some words}+^D+Some words Some Words+```++§7: mathematical content is excluded from case changing, and+`\NoCaseChange` excludes its argument:++```+% pandoc -f latex -t markdown+\MakeUppercase{Some text $y = mx + c$}++\MakeUppercase{\NoCaseChange{iPhone}}+^D+SOME TEXT $y = mx + c$++iPhone+```++§7: `\DeclareTitlecaseExclusions`:++```+% pandoc -f latex -t plain+\DeclareTitlecaseExclusions{a,an,and,on,of,the}+\MakeTitlecase[words = all]{Of mice and men}++\MakeTitlecase[words = all]{The mill on the floss}++\MakeTitlecase[words = all]{The comedy of errors}+^D+Of Mice and Men++The Mill on the Floss++The Comedy of Errors+```++§7: `\DeclareUppercaseMapping` etc. are parsed and ignored:++```+% pandoc -f latex -t plain+\DeclareUppercaseMapping{"01F0}{\v{J}}+\DeclareLowercaseMapping[xx]{"0049}{\i}+Done.+^D+Done.+```++§8: `\textsubscript` and `\textsuperscript`:++```+% pandoc -f latex -t html+A\textsubscript{low} and B\textsuperscript{high}.+^D+<p>A<sub>low</sub> and B<sup>high</sup>.</p>+```++§9: `\listfiles` is parsed and ignored:++```+% pandoc -f latex -t plain+\listfiles[hashes]+Done.+^D+Done.+```
@@ -0,0 +1,43 @@+```+% pandoc -f native -t markdown+[Para [Str "a", Note [Para [Str "outer", Note [Para [Str "inner"]]]], Str "b", Note [Para [Str "second"]]]]+^D+a[^1]b[^2]++[^1]: outer[^3]++[^2]: second++[^3]: inner+```++```+% pandoc -f native -t rst+[Para [Str "a", Note [Para [Str "outer", Note [Para [Str "inner"]]]], Str "b", Note [Para [Str "second"]]]]+^D+a [1]_b [2]_++.. [1]+ outer [3]_++.. [2]+ second++.. [3]+ inner+```++```+% pandoc -f native -t haddock+[Para [Str "a", Note [Para [Str "outer", Note [Para [Str "inner"]]]], Str "b", Note [Para [Str "second"]]]]+^D+a<#notes [1]>b<#notes [2]>++#notes#++1. outer<#notes [3]>++2. second++3. inner+```
@@ -0,0 +1,1 @@+$abstract$
@@ -0,0 +1,1 @@+$body$
@@ -0,0 +1,131 @@+The `abstract` metadata field holds blocks, so it must be rendered+with the block writer: previously it was flattened to inlines, which+produced bare character data (not valid as a child of `office:text`)+and dropped the `Abstract` style.++```+% pandoc -f markdown -t opendocument --template command/opendocument-abstract.opendocument+---+abstract: A one-line abstract.+---++Body.+^D+<text:p text:style-name="Abstract">A one-line abstract.</text:p>+```++A multi-paragraph abstract stays multi-paragraph:++```+% pandoc -f markdown -t opendocument --wrap=none --template command/opendocument-abstract.opendocument+---+abstract: |+ First para.++ Second para.+---++Body.+^D+<text:p text:style-name="Abstract">First para.</text:p><text:p text:style-name="Abstract">Second para.</text:p>+```++A requested paragraph style must also be applied to `Plain` blocks, not+just to `Para`. The body of a caption-less figure is a `Plain`, so it+should still get the `Figure` style:++```+% pandoc -f native -t opendocument --template command/opendocument-body.opendocument+[Figure ("fig1",[],[]) (Caption Nothing []) [Plain [Str "placeholder"]]]+^D+<text:p text:style-name="Figure">placeholder</text:p>+```++Column style names must remain valid NCNames past the 26th column;+incrementing a character code would run into `[`, `\`, etc.++```+% pandoc -f native -t opendocument --template command/opendocument-body.opendocument+[Table ("",[],[]) (Caption Nothing [])+ [(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)+ ,(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)+ ,(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)+ ,(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)+ ,(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)+ ,(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)+ ,(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault),(AlignDefault,ColWidthDefault)]+ (TableHead ("",[],[]) [])+ [(TableBody ("",[],[]) (RowHeadColumns 0) [] [])]+ (TableFoot ("",[],[]) [])]+^D+<table:table table:name="Table1" table:style-name="Table1">+ <table:table-column table:style-name="Table1.A" />+ <table:table-column table:style-name="Table1.B" />+ <table:table-column table:style-name="Table1.C" />+ <table:table-column table:style-name="Table1.D" />+ <table:table-column table:style-name="Table1.E" />+ <table:table-column table:style-name="Table1.F" />+ <table:table-column table:style-name="Table1.G" />+ <table:table-column table:style-name="Table1.H" />+ <table:table-column table:style-name="Table1.I" />+ <table:table-column table:style-name="Table1.J" />+ <table:table-column table:style-name="Table1.K" />+ <table:table-column table:style-name="Table1.L" />+ <table:table-column table:style-name="Table1.M" />+ <table:table-column table:style-name="Table1.N" />+ <table:table-column table:style-name="Table1.O" />+ <table:table-column table:style-name="Table1.P" />+ <table:table-column table:style-name="Table1.Q" />+ <table:table-column table:style-name="Table1.R" />+ <table:table-column table:style-name="Table1.S" />+ <table:table-column table:style-name="Table1.T" />+ <table:table-column table:style-name="Table1.U" />+ <table:table-column table:style-name="Table1.V" />+ <table:table-column table:style-name="Table1.W" />+ <table:table-column table:style-name="Table1.X" />+ <table:table-column table:style-name="Table1.Y" />+ <table:table-column table:style-name="Table1.Z" />+ <table:table-column table:style-name="Table1.AA" />+</table:table>+```++A note nested inside another note must not reuse the outer note's+`text:id`:++```+% pandoc -f markdown -t opendocument --wrap=none --template command/opendocument-body.opendocument+Text^[outer^[inner]]^[second]+^D+<text:p text:style-name="Text_20_body">Text<text:note text:id="ftn0" text:note-class="footnote"><text:note-citation>1</text:note-citation><text:note-body><text:p text:style-name="Footnote">outer<text:note text:id="ftn1" text:note-class="footnote"><text:note-citation>2</text:note-citation><text:note-body><text:p text:style-name="Footnote">inner</text:p></text:note-body></text:note></text:p></text:note-body></text:note><text:note text:id="ftn2" text:note-class="footnote"><text:note-citation>3</text:note-citation><text:note-body><text:p text:style-name="Footnote">second</text:p></text:note-body></text:note></text:p>+```++Automatic text styles are declared in numerical order: sorting them by+name would put `T10` between `T1` and `T2`.++```+% pandoc -f native -t opendocument --wrap=none --template command/11301-styles.opendocument+[Para [SmallCaps [Str "a"]+ ,SmallCaps [Strikeout [Str "b"]]+ ,SmallCaps [Superscript [Str "c"]]+ ,SmallCaps [Subscript [Str "d"]]+ ,SmallCaps [Underline [Str "e"]]+ ,Strikeout [Superscript [Str "f"]]+ ,Strikeout [Subscript [Str "g"]]+ ,Strikeout [Underline [Str "h"]]+ ,Underline [Superscript [Str "i"]]+ ,Underline [Subscript [Str "j"]]]]+^D+<style:style style:name="T1" style:family="text"><style:text-properties fo:font-variant="small-caps" /></style:style>+<style:style style:name="T2" style:family="text"><style:text-properties fo:font-variant="small-caps" style:text-line-through-style="solid" /></style:style>+<style:style style:name="T3" style:family="text"><style:text-properties fo:font-variant="small-caps" style:text-position="super 58%" /></style:style>+<style:style style:name="T4" style:family="text"><style:text-properties fo:font-variant="small-caps" style:text-position="sub 58%" /></style:style>+<style:style style:name="T5" style:family="text"><style:text-properties fo:font-variant="small-caps" style:text-underline-color="font-color" style:text-underline-style="solid" style:text-underline-width="auto" /></style:style>+<style:style style:name="T6" style:family="text"><style:text-properties style:text-line-through-style="solid" style:text-position="super 58%" /></style:style>+<style:style style:name="T7" style:family="text"><style:text-properties style:text-line-through-style="solid" style:text-position="sub 58%" /></style:style>+<style:style style:name="T8" style:family="text"><style:text-properties style:text-line-through-style="solid" style:text-underline-color="font-color" style:text-underline-style="solid" style:text-underline-width="auto" /></style:style>+<style:style style:name="T9" style:family="text"><style:text-properties style:text-position="super 58%" style:text-underline-color="font-color" style:text-underline-style="solid" style:text-underline-width="auto" /></style:style>+<style:style style:name="T10" style:family="text"><style:text-properties style:text-position="sub 58%" style:text-underline-color="font-color" style:text-underline-style="solid" style:text-underline-width="auto" /></style:style>+<style:style style:name="fr2" style:family="graphic" style:parent-style-name="Formula"><style:graphic-properties style:vertical-pos="middle" style:vertical-rel="text" style:horizontal-pos="center" style:horizontal-rel="paragraph-content" style:wrap="none" /></style:style>+<style:style style:name="fr1" style:family="graphic" style:parent-style-name="Formula"><style:graphic-properties style:vertical-pos="middle" style:vertical-rel="text" /></style:style>+<text:p text:style-name="Text_20_body"><text:span text:style-name="T1">a</text:span><text:span text:style-name="T2">b</text:span><text:span text:style-name="T3">c</text:span><text:span text:style-name="T4">d</text:span><text:span text:style-name="T5">e</text:span><text:span text:style-name="T6">f</text:span><text:span text:style-name="T7">g</text:span><text:span text:style-name="T8">h</text:span><text:span text:style-name="T9">i</text:span><text:span text:style-name="T10">j</text:span></text:p>+```
@@ -24,7 +24,7 @@ ::: {#ref-averroes/hannes .csl-entry} Averroes. (1892). *Des Averroës Abhandlung: "Über die Möglichkeit der-Conjunktion" oder "Über den materiellen Intellekt"*. (L. Hannes, Ed. &+Conjunktion" oder "Über den materiellen Intellekt."* (L. Hannes, Ed. & Trans.). Halle an der Saale: C. A. Kaemmerer. :::
@@ -19,11 +19,11 @@ ::::: {#refs .references .csl-bib-body} ::: {#ref-james .csl-entry}-[1. ]{.csl-left-margin}[James MRCEL. ]{.csl-right-inline}+[1. ]{.csl-left-margin}[James MRCEL.]{.csl-right-inline} ::: ::: {#ref-macfarlane .csl-entry}-[2. ]{.csl-left-margin}[MacFarlane JG. ]{.csl-right-inline}+[2. ]{.csl-left-margin}[MacFarlane JG.]{.csl-right-inline} ::: ::::: ```
@@ -30,7 +30,7 @@ :::: {#refs .references .csl-bib-body .hanging-indent entry-spacing="0"} ::: {#ref-Faraday-forthcoming .csl-entry} Faraday, Carry. Forthcoming. "Protean Photography." In *Seven Trips-Beyond the Asteroid Belt*, edited by James Oring. Cape Canaveral, FL:+beyond the Asteroid Belt*, edited by James Oring. Cape Canaveral, FL: Launch Press. ::: ::::
@@ -1,3 +1,6 @@+Note: pandoc no longer parses particles out of names (see #11911).+This test is left to confirm the new behavior.+ ``` % pandoc --citeproc -t markdown-citations ---@@ -41,7 +44,7 @@ :::::::: {#refs .references .csl-bib-body .hanging-indent entry-spacing="0"} ::: {#ref-item4 .csl-entry}-Bennett, Frank G., Jr., n.d.+Bennett, Frank G.,! Jr., n.d. ::: ::: {#ref-item1 .csl-entry}@@ -52,16 +55,16 @@ Dumboldt, Ezekiel, III, n.d. ::: -::: {#ref-item2 .csl-entry}-Gogh, Vincent van, n.d.-:::- ::: {#ref-item3 .csl-entry} Humboldt, Alexander von, n.d. :::++::: {#ref-item2 .csl-entry}+[van Gogh, Vincent]{.nocase}, n.d.+::: :::::::: -[^1]: John Doe III, n.d.; Vincent van Gogh, n.d.; Alexander von- Humboldt, n.d.; Frank G. Bennett, Jr., n.d.; Ezekiel Dumboldt III,- n.d.+[^1]: John, III Doe, n.d.; [Vincent van Gogh]{.nocase}, n.d.; Alexander+ von Humboldt, n.d.; Frank G.,! Jr. Bennett, n.d.; Ezekiel, III+ Dumboldt, n.d. ```
@@ -127,8 +127,9 @@ [^12]: [Doe, *First Book*](#ref-item1) and nowhere else. -[^13]: Like a citation without author: (), and again (), and now Doe- with a locator (["Article," 44](#ref-item2)).+[^13]: Like a citation without author: ([](#ref-item1)), and again+ ([](#ref-item1)), and now Doe with a locator (["Article,"+ 44](#ref-item2)). [^14]: *See* [Doe, *First Book*, 32](#ref-item1). ```
@@ -0,0 +1,98 @@+Highlighted text should be read as a `mark` span, round-tripping with+the Typst writer.++```+% pandoc -f typst -t native+#highlight[hello]+^D+[ Para [ Span ( "" , [ "mark" ] , [] ) [ Str "hello" ] ] ]+```++```+% pandoc -f markdown -t typst+[hello]{.mark}+^D+#highlight[hello]+```++```+% pandoc -f typst -t typst+#highlight[hello]+^D+#highlight[hello]+```++`highlight` may also wrap multiple paragraphs. A pandoc inline cannot+span paragraphs, so the body is split at paragraph breaks and each+paragraph is read as a `mark` span. The same holds for other+inline-styling elements, such as `emph`.++```+% pandoc -f typst -t native+Before.++#highlight[+ Para one.++ Para two.+]+^D+[ Para [ Str "Before." ]+, Para+ [ Span+ ( "" , [ "mark" ] , [] )+ [ SoftBreak , Str "Para" , Space , Str "one." ]+ ]+, Para+ [ Span+ ( "" , [ "mark" ] , [] ) [ Str "Para" , Space , Str "two." ]+ ]+]+```++```+% pandoc -f typst -t typst+Before.++#highlight[+ Para one.++ Para two.+]+^D+Before.++#highlight[ Para one.]++#highlight[Para two.]+```++Highlight bodies may contain math, inline or display, without breaking+the reader.++```+% pandoc -f typst -t native+#highlight[$ a = b $]+^D+[ Para+ [ Span ( "" , [ "mark" ] , [] ) [ Math DisplayMath "a = b" ]+ ]+]+```++```+% pandoc -f typst -t native+#highlight[+ Para.++ $ c = d $+]+^D+[ Para+ [ Span ( "" , [ "mark" ] , [] ) [ SoftBreak , Str "Para." ]+ ]+, Para+ [ Span ( "" , [ "mark" ] , [] ) [ Math DisplayMath "c = d" ]+ ]+]+```
@@ -177,6 +177,7 @@ align: (auto,auto,auto,), [A], [B], [C], )}+ Paragraph after. ``` @@ -231,5 +232,6 @@ align: (auto,auto,auto,), [A], [B], [C], )+ Paragraph after. ```
@@ -299,7 +299,7 @@ , Str "quote:" ] , CodeBlock- ( "" , [ "" ] , [] )+ ( "" , [] , [] ) "sub status {\n print \"working\";\n}\n" , Para [ Str "A" , Space , Str "list:" ] , OrderedList@@ -355,11 +355,11 @@ 1 ( "" , [] , [] ) [ Str "Code" , Space , Str "Blocks" ] , Para [ Str "Code:" ] , CodeBlock- ( "" , [ "" ] , [] )+ ( "" , [] , [] ) "---- (should be four hyphens)\n\nsub status {\n print \"working\";\n}\n\nthis code block is indented by one tab\n" , Para [ Str "And:" ] , CodeBlock- ( "" , [ "" ] , [] )+ ( "" , [] , [] ) " this code block is indented by two tabs\n\nThese should not be escaped: \\$ \\\\ \\> \\[ \\{\n" , HorizontalRule ]@@ -839,8 +839,7 @@ ) , ( [ Emph [ Str "orange" ] ] , [ [ Para [ Str "orange" , Space , Str "fruit" ]- , CodeBlock- ( "" , [ "" ] , [] ) "{ orange code block }\n"+ , CodeBlock ( "" , [] , [] ) "{ orange code block }\n" , BlockQuote [ Para [ Str "orange"@@ -1016,10 +1015,10 @@ , Space , Str "though:" ]- , CodeBlock ( "" , [ "" ] , [] ) "<div>\n foo\n</div>\n"+ , CodeBlock ( "" , [] , [] ) "<div>\n foo\n</div>\n" , Para [ Str "As" , Space , Str "should" , Space , Str "this:" ]- , CodeBlock ( "" , [ "" ] , [] ) "<div>foo</div>\n"+ , CodeBlock ( "" , [] , [] ) "<div>foo</div>\n" , Para [ Str "Now," , Space , Str "nested:" ] , Div ( "" , [] , [] )@@ -1044,7 +1043,7 @@ ] , Para [ Str "Multiline:" ] , Para [ Str "Code" , Space , Str "block:" ]- , CodeBlock ( "" , [ "" ] , [] ) "<!-- Comment -->\n"+ , CodeBlock ( "" , [] , [] ) "<!-- Comment -->\n" , Para [ Str "Just" , Space@@ -1065,7 +1064,7 @@ , Str "line:" ] , Para [ Str "Code:" ]- , CodeBlock ( "" , [ "" ] , [] ) "<hr />\n"+ , CodeBlock ( "" , [] , [] ) "<hr />\n" , Para [ Str "Hr\8217s:" ] , HorizontalRule ]@@ -1824,7 +1823,7 @@ , Space , Str "link." ]- , CodeBlock ( "" , [ "" ] , [] ) "[not]: /url\n"+ , CodeBlock ( "" , [] , [] ) "[not]: /url\n" , Para [ Str "Foo" , Space@@ -2003,7 +2002,7 @@ , Code ( "" , [] , [] ) "<http://example.com/>" ] , CodeBlock- ( "" , [ "" ] , [] ) "or here: <http://example.com/>\n"+ ( "" , [] , [] ) "or here: <http://example.com/>\n" , HorizontalRule ] ]@@ -2177,7 +2176,7 @@ , Space , Str "items)." ]- , CodeBlock ( "" , [ "" ] , [] ) "{ <code> }\n"+ , CodeBlock ( "" , [] , [] ) "{ <code> }\n" , Para [ Str "If" , Space
binary file changed (35146 → 13802 bytes)
binary file changed (22264 → 6782 bytes)
binary file changed (8463 → 3413 bytes)
binary file changed (11582 → 2924 bytes)
binary file changed (27098 → 7785 bytes)
binary file changed (12153 → 3449 bytes)
binary file changed (18059 → 2649 bytes)
binary file changed (8465 → 3363 bytes)
binary file changed (17109 → 4929 bytes)
@@ -1,4 +1,152 @@-[Para [Str "I",Space,Str "want",Space,Span ("",["comment-start"],[("id","0"),("author","Jesse Rosenthal"),("date","2016-05-09T16:13:00Z")]) [Str "I",Space,Str "left",Space,Str "a",Space,Str "comment."],Str "some",Space,Str "text",Space,Str "to",Space,Str "have",Space,Str "a",Space,Str "comment",Space,Span ("",["comment-end"],[("id","0")]) [],Str "on",Space,Str "it."]-,Para [Str "This",Space,Str "is",Space,Span ("",["comment-start"],[("id","1"),("author","Jesse Rosenthal"),("date","2016-05-09T16:13:00Z")]) [Str "A",Space,Str "comment",Space,Str "across",Space,Str "paragraphs."],Str "a",Space,Str "new",Space,Str "paragraph."]-,Para [Str "And",Space,Str "so",Span ("",["comment-end"],[("id","1")]) [],Space,Str "is",Space,Str "this."]-,Para [Str "One",Space,Span ("",["comment-start"],[("id","2"),("author","Jesse Rosenthal"),("date","2016-05-09T16:14:00Z")]) [Str "This",Space,Str "one",Space,Str "has",Space,Str "multiple",Space,Str "paragraphs.",LineBreak, Str "See?"],Str "more",Span ("",["comment-end"],[("id","2")]) [],Str ".",Space,Str "And",Space,Str "this",Space,Str "is",Space,Str "one",Space,Str "with",Space,Str "a",Space,Span ("",["comment-start"],[("id","3"),("author","Jesse Rosenthal"),("date","2016-06-22T14:35:00Z")]) [Str "Do",Space,Str "something."],Span ("",["comment-start"],[("id","4"),("author","Jesse Rosenthal"),("date","2016-06-22T14:36:00Z")]) [Str "Do",Space,Str "something",Space,Str "else."],Str "comment",Space,Str "in",Space,Str "a",Space,Str "comment",Span ("",["comment-end"],[("id","3")]) [Span ("",["comment-end"],[("id","4")]) []],Str "."]]+Pandoc+ Meta { unMeta = fromList [] }+ [ Para+ [ Str "I"+ , Space+ , Str "want"+ , Space+ , Span+ ( ""+ , [ "comment-start" ]+ , [ ( "comment-id" , "0" )+ , ( "author" , "Jesse Rosenthal" )+ , ( "date" , "2016-05-09T16:13:00Z" )+ ]+ )+ [ Str "I"+ , Space+ , Str "left"+ , Space+ , Str "a"+ , Space+ , Str "comment."+ ]+ , Str "some"+ , Space+ , Str "text"+ , Space+ , Str "to"+ , Space+ , Str "have"+ , Space+ , Str "a"+ , Space+ , Str "comment"+ , Space+ , Span+ ( "" , [ "comment-end" ] , [ ( "comment-id" , "0" ) ] ) []+ , Str "on"+ , Space+ , Str "it."+ ]+ , Para+ [ Str "This"+ , Space+ , Str "is"+ , Space+ , Span+ ( ""+ , [ "comment-start" ]+ , [ ( "comment-id" , "1" )+ , ( "author" , "Jesse Rosenthal" )+ , ( "date" , "2016-05-09T16:13:00Z" )+ ]+ )+ [ Str "A"+ , Space+ , Str "comment"+ , Space+ , Str "across"+ , Space+ , Str "paragraphs."+ ]+ , Str "a"+ , Space+ , Str "new"+ , Space+ , Str "paragraph."+ ]+ , Para+ [ Str "And"+ , Space+ , Str "so"+ , Span+ ( "" , [ "comment-end" ] , [ ( "comment-id" , "1" ) ] ) []+ , Space+ , Str "is"+ , Space+ , Str "this."+ ]+ , Para+ [ Str "One"+ , Space+ , Span+ ( ""+ , [ "comment-start" ]+ , [ ( "comment-id" , "2" )+ , ( "author" , "Jesse Rosenthal" )+ , ( "date" , "2016-05-09T16:14:00Z" )+ ]+ )+ [ Str "This"+ , Space+ , Str "one"+ , Space+ , Str "has"+ , Space+ , Str "multiple"+ , Space+ , Str "paragraphs."+ , LineBreak+ , Str "See?"+ ]+ , Str "more"+ , Span+ ( "" , [ "comment-end" ] , [ ( "comment-id" , "2" ) ] ) []+ , Str "."+ , Space+ , Str "And"+ , Space+ , Str "this"+ , Space+ , Str "is"+ , Space+ , Str "one"+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Span+ ( ""+ , [ "comment-start" ]+ , [ ( "comment-id" , "3" )+ , ( "author" , "Jesse Rosenthal" )+ , ( "date" , "2016-06-22T14:35:00Z" )+ ]+ )+ [ Str "Do" , Space , Str "something." ]+ , Span+ ( ""+ , [ "comment-start" ]+ , [ ( "comment-id" , "4" )+ , ( "author" , "Jesse Rosenthal" )+ , ( "date" , "2016-06-22T14:36:00Z" )+ ]+ )+ [ Str "Do" , Space , Str "something" , Space , Str "else." ]+ , Str "comment"+ , Space+ , Str "in"+ , Space+ , Str "a"+ , Space+ , Str "comment"+ , Span+ ( "" , [ "comment-end" ] , [ ( "comment-id" , "3" ) ] )+ [ Span+ ( "" , [ "comment-end" ] , [ ( "comment-id" , "4" ) ] ) []+ ]+ , Str "."+ ]+ ]
binary file changed (17078 → 5058 bytes)
binary file changed (9951 → 3822 bytes)
binary file changed (14978 → 3061 bytes)
@@ -0,0 +1,50 @@+[ Header 1 ( "refs-title" , [] , [] ) [ Str "References" ]+, Div+ ( "refs"+ , [ "references" , "csl-bib-body" , "hanging-indent" ]+ , [ ( "entry-spacing" , "1.0" ) , ( "line-spacing" , "2.0" ) ]+ )+ [ Div+ ( "ref-doe2020" , [ "csl-entry" ] , [] )+ [ Para+ [ Str "Doe,"+ , Space+ , Str "J.,"+ , Space+ , Str "&"+ , Space+ , Str "Smith,"+ , Space+ , Str "J."+ , Space+ , Str "(2020)."+ , Space+ , Str "A"+ , Space+ , Str "very"+ , Space+ , Str "long"+ , Space+ , Str "title"+ , Space+ , Str "that"+ , Space+ , Str "wraps"+ , Space+ , Str "onto"+ , Space+ , Str "multiple"+ , Space+ , Str "lines."+ , Space+ , Emph [ Str "Journal" , Space , Str "of" , Space , Str "Testing" ]+ , Str ","+ , Space+ , Emph [ Str "12" ]+ , Str ","+ , Space+ , Str "1\8211\&25."+ ]+ ]+ ]+]
binary file changed (absent → 995 bytes)
@@ -0,0 +1,36 @@+[ Para+ [ Str "This"+ , Space+ , Str "is"+ , Space+ , Str "some"+ , Space+ , Str "sample"+ , Space+ , Str "content"+ , Space+ , Str "that"+ , Space+ , Str "will"+ , Space+ , Str "be"+ , Space+ , Str "wrapped"+ , Space+ , Str "inside"+ , Space+ , Str "a"+ , Space+ , Str "customXml"+ , Space+ , Str "element"+ , Space+ , Str "in"+ , Space+ , Str "the"+ , Space+ , Str "OpenXML"+ , Space+ , Str "structure."+ ]+]
binary file changed (18131 → 3446 bytes)
binary file changed (9174 → 3357 bytes)
binary file changed (22716 → 10159 bytes)
binary file changed (absent → 3032 bytes)
@@ -0,0 +1,68 @@+Pandoc+ Meta { unMeta = fromList [] }+ [ Para+ [ Str "This"+ , Space+ , Str "is"+ , Space+ , Strikeout [ Str "single" , Space , Str "strikethrough" ]+ , Str ","+ , Space+ , Str "and"+ , Space+ , Str "this"+ , Space+ , Str "is"+ , Space+ , Strikeout [ Str "double" , Space , Str "strikethrough" ]+ , Str "."+ ]+ , Para+ [ Str "A"+ , Space+ , Str "character"+ , Space+ , Str "style"+ , Space+ , Str "can"+ , Space+ , Strikeout+ [ Str "strike" , Space , Str "this" , Space , Str "out," ]+ , Space+ , Str "cancel"+ , Space+ , Str "this,"+ , Space+ , Strikeout+ [ Str "or"+ , Space+ , Str "strike"+ , Space+ , Str "this"+ , Space+ , Str "out"+ , Space+ , Str "twice"+ ]+ , Str "."+ ]+ , Para+ [ Str "This"+ , Space+ , Str "is"+ , Space+ , Strikeout+ [ Str "double-strikethrough,"+ , Space+ , Str "overridden"+ , Space+ , Str "to"+ , Space+ , Str "single,"+ , Space+ , Str "and"+ ]+ , Space+ , Str "cancelled."+ ]+ ]
binary file changed (15671 → 2531 bytes)
binary file changed (70197 → 5664 bytes)
binary file changed (70234 → 5695 bytes)
binary file changed (14312 → 4004 bytes)
binary file changed (12539 → 3796 bytes)
binary file changed (absent → 1751 bytes)
@@ -0,0 +1,24 @@+[ Para+ [ Str "Error!"+ , Space+ , Str "Reference"+ , Space+ , Str "source"+ , Space+ , Str "not"+ , Space+ , Str "found."+ ]+, Para+ [ Link+ ( "" , [] , [] )+ [ Str "Target" , Space , Str "B" ]+ ( "#_Ref2" , "" )+ ]+, BulletList+ [ [ Para [ Str "Item" , Space , Str "one" ] ]+ , [ Para [ Str "Item" , Space , Str "two" ] ]+ ]+, Para+ [ Str "After" , Space , Str "the" , Space , Str "list." ]+]
binary file changed (absent → 1986 bytes)
@@ -0,0 +1,1 @@+[ CodeBlock ( "" , [] , [] ) "int x;" ]
binary file changed (absent → 1780 bytes)
@@ -0,0 +1,14 @@+[ Para [ Str "Target" , Space , Str "A" ]+, Para+ [ Link+ ( "" , [] , [] )+ [ Str "Target" , Space , Str "B" ]+ ( "#_Ref2" , "" )+ ]+, BulletList+ [ [ Para [ Str "Item" , Space , Str "one" ] ]+ , [ Para [ Str "Item" , Space , Str "two" ] ]+ ]+, Para+ [ Str "After" , Space , Str "the" , Space , Str "list." ]+]
binary file changed (43957 → 6699 bytes)
binary file changed (10692 → 10684 bytes)
binary file changed (10510 → 10474 bytes)
binary file changed (10824 → 10814 bytes)
binary file changed (absent → 10670 bytes)
binary file changed (10609 → 10574 bytes)
binary file changed (11236 → 11229 bytes)
binary file changed (10501 → 10465 bytes)
binary file changed (10515 → 10478 bytes)
binary file changed (10993 → 10983 bytes)
binary file changed (10655 → 10764 bytes)
binary file changed (27383 → 27420 bytes)
binary file changed (10451 → 10415 bytes)
binary file changed (10627 → 10591 bytes)
binary file changed (27383 → 27347 bytes)
binary file changed (10664 → 10628 bytes)
binary file changed (10842 → 10847 bytes)
binary file changed (11034 → 11058 bytes)
binary file changed (10788 → 10752 bytes)
binary file changed (10702 → 10667 bytes)
binary file changed (10685 → 10681 bytes)
binary file changed (10918 → 10893 bytes)
binary file changed (10699 → 10676 bytes)
binary file changed (absent → 10738 bytes)
binary file changed (10845 → 10864 bytes)
binary file changed (10615 → 10614 bytes)
binary file changed (10544 → 10508 bytes)
binary file changed (10683 → 10647 bytes)
binary file changed (10496 → 10486 bytes)
binary file changed (10946 → 10941 bytes)
binary file changed (10882 → 10865 bytes)
binary file changed (10870 → 10879 bytes)
binary file changed (10519 → 10483 bytes)
binary file changed (10783 → 10781 bytes)
binary file changed (10492 → 10456 bytes)
binary file changed (10476 → 10437 bytes)
binary file changed (10506 → 10470 bytes)
binary file changed (10617 → 10581 bytes)
binary file changed (10468 → 10432 bytes)
binary file changed (10479 → 10443 bytes)
binary file changed (14754 → 2240 bytes)
binary file changed (18926 → 2701 bytes)
binary file changed (13680 → 4188 bytes)
binary file changed (35407 → 25821 bytes)
binary file changed (23559 → 18373 bytes)
binary file changed (74199 → 16180 bytes)
binary file changed (19882 → 9793 bytes)
binary file changed (8379 → 3226 bytes)
binary file changed (20047 → 2739 bytes)
binary file changed (13628 → 3485 bytes)
binary file changed (16269 → 4146 bytes)
binary file changed (absent → 4982 bytes)
@@ -0,0 +1,112 @@+Pandoc+ Meta { unMeta = fromList [] }+ [ Para+ [ Str "An"+ , Space+ , Link+ ( "" , [] , [] )+ [ Str "external" , Space , Str "link" ]+ ( "https://example.org/report.pdf"+ , "Annual report, PDF, 2 MB"+ )+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Str "ScreenTip."+ ]+ , Para+ [ Str "An"+ , Space+ , Link+ ( "" , [] , [] )+ [ Str "external"+ , Space+ , Str "link"+ , Space+ , Str "to"+ , Space+ , Str "a"+ , Space+ , Str "fragment"+ ]+ ( "https://example.org/guide.html#install"+ , "Installation steps"+ )+ , Str ","+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Str "ScreenTip."+ ]+ , Para+ [ Str "An"+ , Space+ , Link+ ( "" , [] , [] )+ [ Str "internal" , Space , Str "link" ]+ ( "#methods" , "Jump to the Methods section" )+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Str "ScreenTip."+ ]+ , Para+ [ Str "A"+ , Space+ , Link+ ( "" , [] , [] )+ [ Str "link" ]+ ( "https://example.com/" , "" )+ , Space+ , Str "without"+ , Space+ , Str "a"+ , Space+ , Str "ScreenTip."+ ]+ , Para+ [ Str "A"+ , Space+ , Str "ScreenTip"+ , Space+ , Str "with"+ , Space+ , Link+ ( "" , [] , [] )+ [ Str "markup" , Space , Str "characters" ]+ ( "https://example.org/escape" , "Q&A: \"Why?\" <draft>" )+ , Str "."+ ]+ , Para+ [ Str "A"+ , Space+ , Link+ ( "" , [] , [] )+ [ Str "field-code" , Space , Str "link" ]+ ( "https://example.org/field"+ , "ScreenTip from a field code"+ )+ , Space+ , Str "with"+ , Space+ , Str "a"+ , Space+ , Str "ScreenTip."+ ]+ , Para+ [ Span ( "methods" , [ "anchor" ] , [] ) []+ , Str "Methods"+ , Space+ , Str "are"+ , Space+ , Str "described"+ , Space+ , Str "here."+ ]+ ]
binary file changed (32356 → 3164 bytes)
binary file changed (9952 → 3822 bytes)
binary file changed (9473 → 3800 bytes)
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff
file too large to diff