apex-ruby 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +119 -0
- data/apex-ruby.gemspec +31 -0
- data/ext/apex_ext/apex_ext.c +215 -0
- data/ext/apex_ext/apex_src/BENCHMARK.md +32 -0
- data/ext/apex_ext/apex_src/BENCHMARK_COMPARISON.md +67 -0
- data/ext/apex_ext/apex_src/CHANGELOG.md +2454 -0
- data/ext/apex_ext/apex_src/CMakeLists.txt +454 -0
- data/ext/apex_ext/apex_src/Dockerfile.linux-build +15 -0
- data/ext/apex_ext/apex_src/Formula/apex.rb +38 -0
- data/ext/apex_ext/apex_src/Info.plist.in +27 -0
- data/ext/apex_ext/apex_src/LICENSE +21 -0
- data/ext/apex_ext/apex_src/Package.swift +160 -0
- data/ext/apex_ext/apex_src/PackageSupport/README.md +17 -0
- data/ext/apex_ext/apex_src/PackageSupport/cmark-gfm/cmark-gfm_export.h +20 -0
- data/ext/apex_ext/apex_src/PackageSupport/cmark-gfm/cmark-gfm_version.h +14 -0
- data/ext/apex_ext/apex_src/PackageSupport/cmark-gfm/cmark_gfm_spm_stub.c +4 -0
- data/ext/apex_ext/apex_src/PackageSupport/cmark-gfm/config.h +41 -0
- data/ext/apex_ext/apex_src/README.md +452 -0
- data/ext/apex_ext/apex_src/VERSION +1 -0
- data/ext/apex_ext/apex_src/apex-header-2-rb@2x.webp +0 -0
- data/ext/apex_ext/apex_src/apex-plugins.json.example +20 -0
- data/ext/apex_ext/apex_src/apex.pc.in +11 -0
- data/ext/apex_ext/apex_src/cli/main.c +2720 -0
- data/ext/apex_ext/apex_src/debug_test.sh +22 -0
- data/ext/apex_ext/apex_src/docs/API_REFERENCE.md +451 -0
- data/ext/apex_ext/apex_src/docs/ARCHITECTURE.md +166 -0
- data/ext/apex_ext/apex_src/docs/CMARK_INTEGRATION.md +220 -0
- data/ext/apex_ext/apex_src/docs/CRITICMARKUP.md +501 -0
- data/ext/apex_ext/apex_src/docs/DEBUGGING.md +73 -0
- data/ext/apex_ext/apex_src/docs/FINAL_STATUS.md +391 -0
- data/ext/apex_ext/apex_src/docs/FINAL_STATUS_UPDATE.md +237 -0
- data/ext/apex_ext/apex_src/docs/FUTURE_FEATURES.md +456 -0
- data/ext/apex_ext/apex_src/docs/IAL_FEATURES.md +210 -0
- data/ext/apex_ext/apex_src/docs/IAL_STATUS.md +344 -0
- data/ext/apex_ext/apex_src/docs/INTEGRATION_EXAMPLE.m +144 -0
- data/ext/apex_ext/apex_src/docs/LIMITATIONS_RESOLVED.md +278 -0
- data/ext/apex_ext/apex_src/docs/OUTPUT_MODES.md +321 -0
- data/ext/apex_ext/apex_src/docs/PROGRESS.md +167 -0
- data/ext/apex_ext/apex_src/docs/STANDALONE_FEATURE.md +174 -0
- data/ext/apex_ext/apex_src/docs/TABLE_SPANS_STATUS.md +243 -0
- data/ext/apex_ext/apex_src/docs/TEST_COVERAGE.md +316 -0
- data/ext/apex_ext/apex_src/docs/USER_GUIDE.md +803 -0
- data/ext/apex_ext/apex_src/docs/WIKI_LINKS_ISSUE.md +91 -0
- data/ext/apex_ext/apex_src/documentation/README.md +160 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex Command Line Options.cheatsheet.txt +365 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Info.plist +24 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/C-API.html +1737 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Citations.html +1420 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Command-Line-Options.html +3574 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Configuration.html +1603 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Credits.html +910 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Examples.html +1168 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Getting-Started.html +1003 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Header-IDs.html +1308 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Home.html +1078 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Inline-Attribute-Lists.html +1622 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Installation.html +1168 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Limitations-and-Roadmap.html +1698 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Metadata-Transforms.html +1531 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Modes.html +1980 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Multi-File-Documents.html +1368 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Pandoc-Integration.html +1151 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Plugins.html +2861 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Syntax.html +3981 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Troubleshooting.html +1454 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Usage.html +1200 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/Documents/Xcode-Integration.html +2066 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/docSet.dsidx +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/optimizedIndex.dsidx +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/Apex.docset/Contents/Resources/tempOptimizedIndex.dsidx +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Info.plist +22 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/cheatset_resources/Open_Sans.woff +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/cheatset_resources/Open_Sans_Bold.woff +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/cheatset_resources/Open_Sans_Bold_Italic.woff +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/cheatset_resources/Open_Sans_Extrabold.woff +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/cheatset_resources/Open_Sans_Extrabold_Italic.woff +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/cheatset_resources/Open_Sans_Italic.woff +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/cheatset_resources/Open_Sans_Semibold.woff +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/cheatset_resources/Open_Sans_Semibold_Italic.woff +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/index.html +914 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/Documents/style.css +399 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/docSet.dsidx +0 -0
- data/ext/apex_ext/apex_src/documentation/docsets/ApexCLI.docset/Contents/Resources/optimizedIndex.dsidx +0 -0
- data/ext/apex_ext/apex_src/documentation/generate_app_docs.rb +772 -0
- data/ext/apex_ext/apex_src/documentation/generate_app_docs_ai.rb +678 -0
- data/ext/apex_ext/apex_src/documentation/generate_docset.rb +873 -0
- data/ext/apex_ext/apex_src/documentation/generate_single_html.rb +733 -0
- data/ext/apex_ext/apex_src/documentation/html/apex-docs.html +17073 -0
- data/ext/apex_ext/apex_src/documentation/shared_scripts.js +64 -0
- data/ext/apex_ext/apex_src/documentation/shared_styles.css +646 -0
- data/ext/apex_ext/apex_src/documentation/transform_for_app.example.md +260 -0
- data/ext/apex_ext/apex_src/examples/bracketed_spans_demo.md +119 -0
- data/ext/apex_ext/apex_src/examples/emoji_span_plugin.yml +11 -0
- data/ext/apex_ext/apex_src/examples/example.html +53 -0
- data/ext/apex_ext/apex_src/examples/example.md +85 -0
- data/ext/apex_ext/apex_src/examples/fenced_divs_demo.md +158 -0
- data/ext/apex_ext/apex_src/examples/kbd.md +8 -0
- data/ext/apex_ext/apex_src/examples/kbd_plugin.rb +250 -0
- data/ext/apex_ext/apex_src/examples/kbd_plugin.yml +9 -0
- data/ext/apex_ext/apex_src/icon/apexicon-outline-black.png +0 -0
- data/ext/apex_ext/apex_src/icon/apexicon-outline-black@2x.png +0 -0
- data/ext/apex_ext/apex_src/icon/apexicon-outline-mark.png +0 -0
- data/ext/apex_ext/apex_src/icon/apexicon-outline-mark@2x.png +0 -0
- data/ext/apex_ext/apex_src/icon/apexicon-outline-white.png +0 -0
- data/ext/apex_ext/apex_src/icon/apexicon-outline-white@2x.png +0 -0
- data/ext/apex_ext/apex_src/icon/apexicon.png +0 -0
- data/ext/apex_ext/apex_src/icon/apexicon@2x.png +0 -0
- data/ext/apex_ext/apex_src/include/apex/apex.h +247 -0
- data/ext/apex_ext/apex_src/include/apex/buffer.h +93 -0
- data/ext/apex_ext/apex_src/include/apex/module.modulemap +16 -0
- data/ext/apex_ext/apex_src/include/apex/parser.h +150 -0
- data/ext/apex_ext/apex_src/include/apex/renderer.h +39 -0
- data/ext/apex_ext/apex_src/man/apex-config.5 +374 -0
- data/ext/apex_ext/apex_src/man/apex-config.5.md +260 -0
- data/ext/apex_ext/apex_src/man/apex-plugins.7 +456 -0
- data/ext/apex_ext/apex_src/man/apex-plugins.7.md +365 -0
- data/ext/apex_ext/apex_src/man/apex.1 +828 -0
- data/ext/apex_ext/apex_src/man/apex.1.md +643 -0
- data/ext/apex_ext/apex_src/man/apex.1.new +338 -0
- data/ext/apex_ext/apex_src/objc/Apex.swift +237 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.h +117 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.m +332 -0
- data/ext/apex_ext/apex_src/src/_README.md +358 -0
- data/ext/apex_ext/apex_src/src/apex.c +6326 -0
- data/ext/apex_ext/apex_src/src/buffer.c +93 -0
- data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +362 -0
- data/ext/apex_ext/apex_src/src/extensions/abbreviations.h +45 -0
- data/ext/apex_ext/apex_src/src/extensions/advanced_footnotes.c +184 -0
- data/ext/apex_ext/apex_src/src/extensions/advanced_footnotes.h +50 -0
- data/ext/apex_ext/apex_src/src/extensions/advanced_tables.c +1897 -0
- data/ext/apex_ext/apex_src/src/extensions/advanced_tables.h +42 -0
- data/ext/apex_ext/apex_src/src/extensions/callouts.c +215 -0
- data/ext/apex_ext/apex_src/src/extensions/callouts.h +53 -0
- data/ext/apex_ext/apex_src/src/extensions/citations.c +2042 -0
- data/ext/apex_ext/apex_src/src/extensions/citations.h +163 -0
- data/ext/apex_ext/apex_src/src/extensions/critic.c +329 -0
- data/ext/apex_ext/apex_src/src/extensions/critic.h +48 -0
- data/ext/apex_ext/apex_src/src/extensions/definition_list.c +1670 -0
- data/ext/apex_ext/apex_src/src/extensions/definition_list.h +42 -0
- data/ext/apex_ext/apex_src/src/extensions/emoji.c +710 -0
- data/ext/apex_ext/apex_src/src/extensions/emoji.h +38 -0
- data/ext/apex_ext/apex_src/src/extensions/emoji_data.h +942 -0
- data/ext/apex_ext/apex_src/src/extensions/fenced_divs.c +925 -0
- data/ext/apex_ext/apex_src/src/extensions/fenced_divs.h +43 -0
- data/ext/apex_ext/apex_src/src/extensions/github-emoji.txt +869 -0
- data/ext/apex_ext/apex_src/src/extensions/grid_tables.c +1121 -0
- data/ext/apex_ext/apex_src/src/extensions/grid_tables.h +33 -0
- data/ext/apex_ext/apex_src/src/extensions/header_ids.c +626 -0
- data/ext/apex_ext/apex_src/src/extensions/header_ids.h +60 -0
- data/ext/apex_ext/apex_src/src/extensions/highlight.c +135 -0
- data/ext/apex_ext/apex_src/src/extensions/highlight.h +16 -0
- data/ext/apex_ext/apex_src/src/extensions/html_markdown.c +408 -0
- data/ext/apex_ext/apex_src/src/extensions/html_markdown.h +42 -0
- data/ext/apex_ext/apex_src/src/extensions/ial.c +4084 -0
- data/ext/apex_ext/apex_src/src/extensions/ial.h +145 -0
- data/ext/apex_ext/apex_src/src/extensions/includes.c +1536 -0
- data/ext/apex_ext/apex_src/src/extensions/includes.h +54 -0
- data/ext/apex_ext/apex_src/src/extensions/index.c +967 -0
- data/ext/apex_ext/apex_src/src/extensions/index.h +90 -0
- data/ext/apex_ext/apex_src/src/extensions/inline_footnotes.c +205 -0
- data/ext/apex_ext/apex_src/src/extensions/inline_footnotes.h +34 -0
- data/ext/apex_ext/apex_src/src/extensions/inline_tables.c +332 -0
- data/ext/apex_ext/apex_src/src/extensions/inline_tables.h +13 -0
- data/ext/apex_ext/apex_src/src/extensions/insert.c +248 -0
- data/ext/apex_ext/apex_src/src/extensions/insert.h +18 -0
- data/ext/apex_ext/apex_src/src/extensions/math.c +279 -0
- data/ext/apex_ext/apex_src/src/extensions/math.h +32 -0
- data/ext/apex_ext/apex_src/src/extensions/metadata.c +3046 -0
- data/ext/apex_ext/apex_src/src/extensions/metadata.h +125 -0
- data/ext/apex_ext/apex_src/src/extensions/relaxed_tables.c +1297 -0
- data/ext/apex_ext/apex_src/src/extensions/relaxed_tables.h +39 -0
- data/ext/apex_ext/apex_src/src/extensions/special_markers.c +194 -0
- data/ext/apex_ext/apex_src/src/extensions/special_markers.h +29 -0
- data/ext/apex_ext/apex_src/src/extensions/sup_sub.c +405 -0
- data/ext/apex_ext/apex_src/src/extensions/sup_sub.h +16 -0
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +468 -0
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.h +44 -0
- data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +2679 -0
- data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.h +23 -0
- data/ext/apex_ext/apex_src/src/extensions/toc.c +255 -0
- data/ext/apex_ext/apex_src/src/extensions/toc.h +34 -0
- data/ext/apex_ext/apex_src/src/extensions/wiki_links.c +624 -0
- data/ext/apex_ext/apex_src/src/extensions/wiki_links.h +58 -0
- data/ext/apex_ext/apex_src/src/html_renderer.c +2762 -0
- data/ext/apex_ext/apex_src/src/html_renderer.h +126 -0
- data/ext/apex_ext/apex_src/src/parser.c +227 -0
- data/ext/apex_ext/apex_src/src/plugins.c +895 -0
- data/ext/apex_ext/apex_src/src/plugins.h +39 -0
- data/ext/apex_ext/apex_src/src/plugins_env.c +187 -0
- data/ext/apex_ext/apex_src/src/plugins_remote.c +263 -0
- data/ext/apex_ext/apex_src/src/pretty_html.c +358 -0
- data/ext/apex_ext/apex_src/src/renderer.c +241 -0
- data/ext/apex_ext/apex_src/src/utf8.c +56 -0
- data/ext/apex_ext/apex_src/test-linux-build.sh +20 -0
- data/ext/apex_ext/apex_src/test.html +103 -0
- data/ext/apex_ext/apex_src/test_coverage.sh +121 -0
- data/ext/apex_ext/apex_src/test_ial_fenced.md +6 -0
- data/ext/apex_ext/apex_src/test_math_norm.py +79 -0
- data/ext/apex_ext/apex_src/test_pandoc_output.html +48 -0
- data/ext/apex_ext/apex_src/test_spm.sh +107 -0
- data/ext/apex_ext/apex_src/tests/ApexSPMTest/main.swift +50 -0
- data/ext/apex_ext/apex_src/tests/BENCHMARK_RESULTS.md +229 -0
- data/ext/apex_ext/apex_src/tests/CMakeLists.txt +24 -0
- data/ext/apex_ext/apex_src/tests/README.md +146 -0
- data/ext/apex_ext/apex_src/tests/benchmark.sh +113 -0
- data/ext/apex_ext/apex_src/tests/benchmark_comparison.sh +166 -0
- data/ext/apex_ext/apex_src/tests/compare_header_ids.sh +31 -0
- data/ext/apex_ext/apex_src/tests/fixtures/basic/headers.md +25 -0
- data/ext/apex_ext/apex_src/tests/fixtures/basic/list-interruption.md +24 -0
- data/ext/apex_ext/apex_src/tests/fixtures/basic/misc_markup.md +33 -0
- data/ext/apex_ext/apex_src/tests/fixtures/basic/test_basic.md +26 -0
- data/ext/apex_ext/apex_src/tests/fixtures/code/code-blocks.md +260 -0
- data/ext/apex_ext/apex_src/tests/fixtures/combine_summary/SUMMARY.md +6 -0
- data/ext/apex_ext/apex_src/tests/fixtures/combine_summary/chapter1.md +7 -0
- data/ext/apex_ext/apex_src/tests/fixtures/combine_summary/index.txt +9 -0
- data/ext/apex_ext/apex_src/tests/fixtures/combine_summary/intro.md +5 -0
- data/ext/apex_ext/apex_src/tests/fixtures/combine_summary/section1_1.md +5 -0
- data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +620 -0
- data/ext/apex_ext/apex_src/tests/fixtures/debug_ref_image_ial.md +3 -0
- data/ext/apex_ext/apex_src/tests/fixtures/demos/ial.md +11 -0
- data/ext/apex_ext/apex_src/tests/fixtures/demos/ial_demo.md +177 -0
- data/ext/apex_ext/apex_src/tests/fixtures/extensions/emoji-autocorrect.md +94 -0
- data/ext/apex_ext/apex_src/tests/fixtures/extensions/emoji_test.md +3 -0
- data/ext/apex_ext/apex_src/tests/fixtures/extensions/kbd_test.md +3 -0
- data/ext/apex_ext/apex_src/tests/fixtures/ial/bracketed_spans_test.md +74 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/image_and_encoding_test.md +27 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/multimarkdown_image_attributes_test.md +60 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/pandoc_ial_image_test.md +27 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/width_height_conversion_test.md +94 -0
- data/ext/apex_ext/apex_src/tests/fixtures/img-in-div.md +16 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/code.py +4 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/data.csv +5 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/data.tsv +5 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/image.png +2 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/metadata_options.yml +11 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/nested.md +8 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/raw.html +4 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/simple.md +7 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/test_image.png +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/large_doc.md +1094 -0
- data/ext/apex_ext/apex_src/tests/fixtures/metadata_options.yml +11 -0
- data/ext/apex_ext/apex_src/tests/fixtures/output/gfm_header_id_test.md +96 -0
- data/ext/apex_ext/apex_src/tests/fixtures/output/test_citations.md +43 -0
- data/ext/apex_ext/apex_src/tests/fixtures/output/test_def_list_links.md +12 -0
- data/ext/apex_ext/apex_src/tests/fixtures/output/test_index_mmark.md +53 -0
- data/ext/apex_ext/apex_src/tests/fixtures/output/test_index_textindex.md +37 -0
- data/ext/apex_ext/apex_src/tests/fixtures/tables/advanced_tables_test.md +93 -0
- data/ext/apex_ext/apex_src/tests/fixtures/tables/inline_tables_test.md +38 -0
- data/ext/apex_ext/apex_src/tests/fixtures/tables/relaxed-table.md +12 -0
- data/ext/apex_ext/apex_src/tests/fixtures/tables/table_cr_line_endings.md +15 -0
- data/ext/apex_ext/apex_src/tests/fixtures/tables/table_no_trailing_newline.md +15 -0
- data/ext/apex_ext/apex_src/tests/generate_gfm_ids.sh +105 -0
- data/ext/apex_ext/apex_src/tests/generate_ial_demo.sh +143 -0
- data/ext/apex_ext/apex_src/tests/gfm_id_comparison_summary.md +96 -0
- data/ext/apex_ext/apex_src/tests/gh_api_test.md +6 -0
- data/ext/apex_ext/apex_src/tests/ial_demo.html +186 -0
- data/ext/apex_ext/apex_src/tests/include_code.py +19 -0
- data/ext/apex_ext/apex_src/tests/include_snippet.md +15 -0
- data/ext/apex_ext/apex_src/tests/multi_file_cli_test.sh +64 -0
- data/ext/apex_ext/apex_src/tests/sample_data.csv +7 -0
- data/ext/apex_ext/apex_src/tests/table_escaped_ltlt.md +4 -0
- data/ext/apex_ext/apex_src/tests/test_basic.c +74 -0
- data/ext/apex_ext/apex_src/tests/test_extensions.c +2116 -0
- data/ext/apex_ext/apex_src/tests/test_helpers.c +183 -0
- data/ext/apex_ext/apex_src/tests/test_helpers.h +91 -0
- data/ext/apex_ext/apex_src/tests/test_ial.c +282 -0
- data/ext/apex_ext/apex_src/tests/test_links.c +418 -0
- data/ext/apex_ext/apex_src/tests/test_marked_integration.c +265 -0
- data/ext/apex_ext/apex_src/tests/test_metadata.c +908 -0
- data/ext/apex_ext/apex_src/tests/test_output.c +1118 -0
- data/ext/apex_ext/apex_src/tests/test_plugins.c +219 -0
- data/ext/apex_ext/apex_src/tests/test_refs.bib +31 -0
- data/ext/apex_ext/apex_src/tests/test_runner.c +244 -0
- data/ext/apex_ext/apex_src/tests/test_syntax_highlight.c +198 -0
- data/ext/apex_ext/apex_src/tests/test_tables.c +862 -0
- data/ext/apex_ext/apex_src/tests/update_benchmarks.sh +9 -0
- data/ext/apex_ext/apex_src/tests/yaml_test.md +13 -0
- data/ext/apex_ext/apex_src/tests.rb +39 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/CMakeLists.txt +48 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/COPYING +170 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/CheckFileOffsetBits.c +14 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/CheckFileOffsetBits.cmake +43 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/FindAsan.cmake +74 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/Makefile.nmake +38 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/README.md +206 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/api_test/CMakeLists.txt +30 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/api_test/cplusplus.cpp +15 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/api_test/cplusplus.h +16 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/api_test/harness.c +111 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/api_test/harness.h +35 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/api_test/main.c +1169 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/appveyor.yml +21 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-bq-flat.md +16 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-bq-nested.md +13 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-code.md +11 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-fences.md +14 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-heading.md +9 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-hr.md +10 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-html.md +32 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-lheading.md +8 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-list-flat.md +67 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-list-nested.md +36 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-ref-flat.md +15 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/block-ref-nested.md +17 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-autolink.md +14 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-backticks.md +3 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-em-flat.md +5 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-em-nested.md +5 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-em-worst.md +5 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-entity.md +11 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-escape.md +15 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-html.md +44 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-links-flat.md +23 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-links-nested.md +13 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/inline-newlines.md +24 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/lorem1.md +13 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/samples/rawtabs.md +18 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/statistics.py +595 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/bench/stats.py +19 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/benchmarks.md +33 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/changelog.txt +1245 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/data/CaseFolding.txt +1495 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/CMakeLists.txt +119 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/autolink.c +508 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/autolink.h +8 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/cmark-gfm-core-extensions.h +54 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/core-extensions.c +27 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/ext_scanners.c +879 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/ext_scanners.h +24 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/ext_scanners.re +92 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/strikethrough.c +167 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/strikethrough.h +9 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/table.c +917 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/table.h +12 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/tagfilter.c +60 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/tagfilter.h +8 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/tasklist.c +156 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/extensions/tasklist.h +8 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/fuzz/CMakeLists.txt +22 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/fuzz/README.md +12 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/fuzz/fuzz_quadratic.c +91 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/fuzz/fuzz_quadratic_brackets.c +110 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/fuzz/fuzzloop.sh +28 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/man/CMakeLists.txt +10 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/man/make_man_page.py +133 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/man/man1/cmark-gfm.1 +78 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/man/man3/cmark-gfm.3 +1041 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/nmake.bat +1 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/CMakeLists.txt +230 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/arena.c +104 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/blocks.c +1622 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/buffer.c +278 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/buffer.h +116 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/case_fold_switch.inc +4327 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/chunk.h +135 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/cmark-gfm-extension_api.h +737 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/cmark-gfm.h +833 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/cmark-gfm_version.h.in +7 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/cmark.c +55 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/cmark_ctype.c +44 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/cmark_ctype.h +33 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/commonmark.c +514 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/config.h.in +76 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/entities.inc +2138 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/footnotes.c +63 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/footnotes.h +27 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/houdini.h +57 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/houdini_href_e.c +100 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/houdini_html_e.c +66 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/houdini_html_u.c +149 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/html.c +502 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/html.h +27 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/inlines.c +1788 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/inlines.h +29 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/iterator.c +159 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/iterator.h +26 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/latex.c +468 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/libcmark-gfm.pc.in +10 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/linked_list.c +37 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/main.c +328 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/man.c +274 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/map.c +129 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/map.h +44 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/node.c +1045 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/node.h +167 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/parser.h +59 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/plaintext.c +218 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/plugin.c +36 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/plugin.h +34 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/references.c +43 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/references.h +26 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/registry.c +63 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/registry.h +24 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/render.c +213 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/render.h +62 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/scanners.c +14056 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/scanners.h +70 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/scanners.re +365 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/syntax_extension.c +149 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/syntax_extension.h +34 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/utf8.c +317 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/utf8.h +35 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/src/xml.c +182 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/suppressions +10 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/CMakeLists.txt +114 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/afl_test_cases/test.md +49 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/cmark-fuzz.c +58 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/cmark.py +105 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/entity_tests.py +67 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/extensions-full-info-string.txt +0 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/extensions-table-prefer-style-attributes.txt +38 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/extensions.txt +920 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/fuzzing_dictionary +67 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/normalize.py +194 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/pathological_tests.py +160 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/regression.txt +375 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/roundtrip_tests.py +50 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/run-cmark-fuzz +4 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/smart_punct.txt +177 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/spec.txt +10212 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/test/spec_tests.py +152 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/toolchain-mingw32.cmake +17 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/tools/Dockerfile +41 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/tools/appveyor-build.bat +13 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/tools/make_entities_inc.py +32 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/tools/mkcasefold.pl +22 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/tools/xml2md.xsl +319 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/tools/xml2md_gfm.xsl +80 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/why-cmark-and-not-x.md +104 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/wrappers/wrapper.js +6 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/wrappers/wrapper.py +37 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/wrappers/wrapper.rb +15 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/wrappers/wrapper.rkt +208 -0
- data/ext/apex_ext/apex_src/vendor/cmark-gfm/wrappers/wrapper_ext.py +109 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/CMakeLists.txt +160 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/Changes +372 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/License +20 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/Makefile.am +51 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/ReadMe.md +46 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/announcement.msg +89 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/bootstrap +3 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/cmake/config.h.in +4 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/configure.ac +73 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/doc/doxygen.cfg +222 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/docker/README.mkd +17 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/docker/alpine-3.7 +26 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/docker/fedora-25 +26 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/docker/ubuntu-14.04 +29 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/docker/ubuntu-16.04 +24 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/examples/anchors.yaml +10 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/examples/array.yaml +2 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/examples/global-tag.yaml +14 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/examples/json.yaml +1 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/examples/mapping.yaml +2 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/examples/numbers.yaml +1 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/examples/strings.yaml +7 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/examples/tags.yaml +7 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/examples/yaml-version.yaml +3 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/include/Makefile.am +17 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/include/yaml.h +1999 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/pkg/ReadMe.md +77 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/pkg/docker/Dockerfile +32 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/pkg/docker/output/ReadMe +1 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/pkg/docker/scripts/libyaml-dist.sh +23 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/regression-inputs/clusterfuzz-testcase-minimized-5607885063061504.yml +1 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/Makefile.am +4 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/api.c +1393 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/dumper.c +394 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/emitter.c +2358 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/loader.c +544 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/parser.c +1416 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/reader.c +469 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/scanner.c +3598 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/writer.c +141 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/src/yaml_private.h +684 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/CMakeLists.txt +27 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/Makefile.am +9 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/ReadMe.md +63 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/example-deconstructor-alt.c +800 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/example-deconstructor.c +1127 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/example-reformatter-alt.c +217 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/example-reformatter.c +202 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/run-all-tests.sh +29 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/run-dumper.c +314 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/run-emitter-test-suite.c +290 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/run-emitter.c +327 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/run-loader.c +63 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/run-parser-test-suite.c +196 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/run-parser.c +88 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/run-scanner.c +63 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/test-reader.c +354 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/tests/test-version.c +29 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/yaml-0.1.pc.in +10 -0
- data/ext/apex_ext/apex_src/vendor/libyaml/yamlConfig.cmake.in +16 -0
- data/ext/apex_ext/extconf.rb +103 -0
- data/lib/apex/configurable.rb +46 -0
- data/lib/apex/document.rb +66 -0
- data/lib/apex/version.rb +15 -0
- data/lib/apex.rb +28 -0
- metadata +544 -0
|
@@ -0,0 +1,1897 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advanced Tables Extension for Apex
|
|
3
|
+
* Implementation
|
|
4
|
+
*
|
|
5
|
+
* Postprocessing approach to add table enhancements:
|
|
6
|
+
* - Column spans (empty cells merge with previous)
|
|
7
|
+
* - Row spans (^^ marker merges with cell above)
|
|
8
|
+
* - Table captions (paragraph before/after table with [Caption] format)
|
|
9
|
+
* - Multi-line support (future)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
#include "advanced_tables.h"
|
|
13
|
+
#include "parser.h"
|
|
14
|
+
#include "node.h"
|
|
15
|
+
#include "render.h"
|
|
16
|
+
#include "table.h"
|
|
17
|
+
#include "ial.h"
|
|
18
|
+
#include <string.h>
|
|
19
|
+
#include <stdlib.h>
|
|
20
|
+
#include <stdbool.h>
|
|
21
|
+
#include <ctype.h>
|
|
22
|
+
#include <stdio.h>
|
|
23
|
+
|
|
24
|
+
/* Global flag for per-cell alignment (set when extension is created) */
|
|
25
|
+
static bool g_per_cell_alignment = false;
|
|
26
|
+
|
|
27
|
+
/* Placeholder for escaped \<< (literal <<). Must match table.c. No underscore so inline parser doesn't treat as emphasis. */
|
|
28
|
+
static const unsigned char ESCAPED_LTLT_PLACEHOLDER[] = "APEXLTLT";
|
|
29
|
+
#define ESCAPED_LTLT_PLACEHOLDER_LEN 8
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Recursively collect all text from a node into a buffer.
|
|
33
|
+
* Caller must free the returned string.
|
|
34
|
+
*/
|
|
35
|
+
static char *get_node_full_text(cmark_node *node) {
|
|
36
|
+
if (!node) return NULL;
|
|
37
|
+
|
|
38
|
+
/* Collect literal from TEXT, CODE, HTML_INLINE so "raw <<" isn't seen as just "<<" */
|
|
39
|
+
cmark_node_type t = cmark_node_get_type(node);
|
|
40
|
+
if (t == CMARK_NODE_TEXT || t == CMARK_NODE_CODE || t == CMARK_NODE_HTML_INLINE) {
|
|
41
|
+
const char *lit = cmark_node_get_literal(node);
|
|
42
|
+
return lit ? strdup(lit) : NULL;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
size_t cap = 64;
|
|
46
|
+
char *buf = malloc(cap);
|
|
47
|
+
if (!buf) return NULL;
|
|
48
|
+
buf[0] = '\0';
|
|
49
|
+
size_t len = 0;
|
|
50
|
+
|
|
51
|
+
cmark_node *child = cmark_node_first_child(node);
|
|
52
|
+
while (child) {
|
|
53
|
+
char *part = get_node_full_text(child);
|
|
54
|
+
if (part) {
|
|
55
|
+
size_t part_len = strlen(part);
|
|
56
|
+
while (len + part_len + 1 > cap) {
|
|
57
|
+
cap *= 2;
|
|
58
|
+
char *new_buf = realloc(buf, cap);
|
|
59
|
+
if (!new_buf) {
|
|
60
|
+
free(part);
|
|
61
|
+
free(buf);
|
|
62
|
+
return NULL;
|
|
63
|
+
}
|
|
64
|
+
buf = new_buf;
|
|
65
|
+
}
|
|
66
|
+
memcpy(buf + len, part, part_len + 1);
|
|
67
|
+
len += part_len;
|
|
68
|
+
free(part);
|
|
69
|
+
}
|
|
70
|
+
child = cmark_node_next(child);
|
|
71
|
+
}
|
|
72
|
+
return buf;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Count TEXT/CODE/HTML_INLINE nodes under n that have literal content. */
|
|
76
|
+
static int count_literal_nodes(cmark_node *n) {
|
|
77
|
+
int count = 0;
|
|
78
|
+
cmark_node_type t = cmark_node_get_type(n);
|
|
79
|
+
if (t == CMARK_NODE_TEXT || t == CMARK_NODE_CODE || t == CMARK_NODE_HTML_INLINE) {
|
|
80
|
+
const char *lit = cmark_node_get_literal(n);
|
|
81
|
+
return (lit && lit[0]) ? 1 : 0;
|
|
82
|
+
}
|
|
83
|
+
for (cmark_node *c = cmark_node_first_child(n); c; c = cmark_node_next(c))
|
|
84
|
+
count += count_literal_nodes(c);
|
|
85
|
+
return count;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a cell should span (contains << marker).
|
|
90
|
+
* Only treat as colspan when the cell contains NOTHING but "<<" and optional whitespace.
|
|
91
|
+
* Any other character (e.g. "raw <<", "**<<**", "x <<") must NOT be interpreted as colspan.
|
|
92
|
+
* Content that is \<< (escaped) is replaced at parse time with a placeholder; we do not treat it as colspan.
|
|
93
|
+
* Prefer raw string content when available so we see the actual characters; otherwise use parsed full text.
|
|
94
|
+
* raw_content_override: if non-NULL, use this as the raw content (e.g. captured before user_data was overwritten).
|
|
95
|
+
*/
|
|
96
|
+
static bool is_colspan_cell(cmark_node *cell, const char *raw_content_override) {
|
|
97
|
+
if (!cell) return false;
|
|
98
|
+
|
|
99
|
+
char *full_text = get_node_full_text(cell);
|
|
100
|
+
if (!full_text) return false;
|
|
101
|
+
|
|
102
|
+
/* Use raw content when available so "raw <<" or "**<<**" etc. are not falsely treated as colspan.
|
|
103
|
+
* Prefer raw_content_override (captured before process_cell_alignment overwrote user_data). */
|
|
104
|
+
const char *raw = raw_content_override;
|
|
105
|
+
if (!raw || !raw[0]) {
|
|
106
|
+
void *ud = cmark_node_get_user_data(cell);
|
|
107
|
+
if (ud) {
|
|
108
|
+
char *s = (char *)ud;
|
|
109
|
+
if (s[0] && !strstr(s, "colspan") && !strstr(s, "rowspan") && !strstr(s, "data-"))
|
|
110
|
+
raw = s;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!raw || !raw[0])
|
|
114
|
+
raw = cmark_node_get_string_content(cell);
|
|
115
|
+
if (!raw || !raw[0]) {
|
|
116
|
+
cmark_node *first = cmark_node_first_child(cell);
|
|
117
|
+
if (first && !cmark_node_next(first) &&
|
|
118
|
+
cmark_node_get_type(first) == CMARK_NODE_PARAGRAPH)
|
|
119
|
+
raw = cmark_node_get_string_content(first);
|
|
120
|
+
}
|
|
121
|
+
if (raw && raw[0]) {
|
|
122
|
+
free(full_text);
|
|
123
|
+
full_text = strdup(raw);
|
|
124
|
+
if (!full_text) return false;
|
|
125
|
+
} else {
|
|
126
|
+
/* No raw content (cleared after parse). If the cell has more than one literal-bearing node
|
|
127
|
+
* (TEXT/CODE/HTML_INLINE), we had e.g. "raw <<" so do not colspan. */
|
|
128
|
+
if (count_literal_nodes(cell) > 1) {
|
|
129
|
+
free(full_text);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const char *start = full_text;
|
|
135
|
+
const char *end = full_text + strlen(full_text);
|
|
136
|
+
if (start == end) {
|
|
137
|
+
free(full_text);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
end--; /* last char */
|
|
141
|
+
|
|
142
|
+
/* Trim leading whitespace */
|
|
143
|
+
while (start <= end && isspace((unsigned char)*start)) start++;
|
|
144
|
+
/* Trim trailing whitespace */
|
|
145
|
+
while (end >= start && isspace((unsigned char)*end)) end--;
|
|
146
|
+
|
|
147
|
+
size_t len = (end >= start) ? (size_t)(end - start + 1) : 0;
|
|
148
|
+
|
|
149
|
+
bool result = false;
|
|
150
|
+
/* Only treat as colspan when the ENTIRE cell content is exactly "<<". */
|
|
151
|
+
if (len == 2 && start[0] == '<' && start[1] == '<') {
|
|
152
|
+
result = true; /* Exactly << → colspan */
|
|
153
|
+
}
|
|
154
|
+
/* Escaped \<< is replaced with placeholder; do not treat as colspan */
|
|
155
|
+
if (len == ESCAPED_LTLT_PLACEHOLDER_LEN &&
|
|
156
|
+
memcmp(start, ESCAPED_LTLT_PLACEHOLDER, ESCAPED_LTLT_PLACEHOLDER_LEN) == 0) {
|
|
157
|
+
result = false;
|
|
158
|
+
}
|
|
159
|
+
/* "raw <<" or any other content that contains << but isn't exactly << → not colspan */
|
|
160
|
+
if (len > 2 && strstr(full_text, "<<") != NULL) {
|
|
161
|
+
result = false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* If we would treat as colspan but node's raw content buffer (string_content) has
|
|
165
|
+
* more than "<<", prefer it so "raw <<" isn't wrongly merged (e.g. if user_data was lost). */
|
|
166
|
+
if (result) {
|
|
167
|
+
const char *raw_buf = cmark_node_get_string_content(cell);
|
|
168
|
+
if (raw_buf && raw_buf[0]) {
|
|
169
|
+
const char *r = raw_buf;
|
|
170
|
+
const char *r_end = r + strlen(r);
|
|
171
|
+
if (r_end > r) r_end--;
|
|
172
|
+
while (r <= r_end && isspace((unsigned char)*r)) r++;
|
|
173
|
+
while (r_end >= r && isspace((unsigned char)*r_end)) r_end--;
|
|
174
|
+
if (r_end >= r && (size_t)(r_end - r + 1) > 2)
|
|
175
|
+
result = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
free(full_text);
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if a cell should rowspan (contains ^^ marker)
|
|
185
|
+
*/
|
|
186
|
+
static bool is_rowspan_cell(cmark_node *cell) {
|
|
187
|
+
if (!cell) return false;
|
|
188
|
+
|
|
189
|
+
cmark_node *child = cmark_node_first_child(cell);
|
|
190
|
+
if (!child || cmark_node_get_type(child) != CMARK_NODE_TEXT) return false;
|
|
191
|
+
|
|
192
|
+
const char *text = cmark_node_get_literal(child);
|
|
193
|
+
if (!text) return false;
|
|
194
|
+
|
|
195
|
+
/* Trim and check for ^^ */
|
|
196
|
+
while (*text && isspace((unsigned char)*text)) text++;
|
|
197
|
+
|
|
198
|
+
if (text[0] == '^' && text[1] == '^') {
|
|
199
|
+
const char *rest = text + 2;
|
|
200
|
+
while (*rest && isspace((unsigned char)*rest)) rest++;
|
|
201
|
+
return (*rest == '\0'); /* Just ^^ */
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Process cell alignment markers (: at start/end) and strip them from content.
|
|
209
|
+
* Returns alignment type: "left", "right", "center", or NULL for default.
|
|
210
|
+
* Modifies the cell's text content by removing the colons.
|
|
211
|
+
*/
|
|
212
|
+
static char *process_cell_alignment(cmark_node *cell) {
|
|
213
|
+
if (!cell) return NULL;
|
|
214
|
+
|
|
215
|
+
/* Recursively find all text nodes in the cell */
|
|
216
|
+
cmark_node *text_node = NULL;
|
|
217
|
+
|
|
218
|
+
/* Try to find the first text node */
|
|
219
|
+
cmark_node *child = cmark_node_first_child(cell);
|
|
220
|
+
while (child && !text_node) {
|
|
221
|
+
if (cmark_node_get_type(child) == CMARK_NODE_TEXT) {
|
|
222
|
+
text_node = child;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
/* Check nested nodes (paragraphs, etc.) */
|
|
226
|
+
cmark_node *nested = cmark_node_first_child(child);
|
|
227
|
+
while (nested && !text_node) {
|
|
228
|
+
if (cmark_node_get_type(nested) == CMARK_NODE_TEXT) {
|
|
229
|
+
text_node = nested;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
nested = cmark_node_next(nested);
|
|
233
|
+
}
|
|
234
|
+
child = cmark_node_next(child);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!text_node) return NULL;
|
|
238
|
+
|
|
239
|
+
const char *original_text = cmark_node_get_literal(text_node);
|
|
240
|
+
if (!original_text) return NULL;
|
|
241
|
+
|
|
242
|
+
/* Check for alignment markers */
|
|
243
|
+
const char *text = original_text;
|
|
244
|
+
const char *start = text;
|
|
245
|
+
const char *end = text + strlen(text) - 1;
|
|
246
|
+
|
|
247
|
+
/* Trim leading whitespace */
|
|
248
|
+
while (start <= end && isspace((unsigned char)*start)) start++;
|
|
249
|
+
/* Trim trailing whitespace */
|
|
250
|
+
while (end >= start && isspace((unsigned char)*end)) end--;
|
|
251
|
+
|
|
252
|
+
if (start > end) return NULL; /* Empty after trimming */
|
|
253
|
+
|
|
254
|
+
bool has_leading_colon = (*start == ':');
|
|
255
|
+
bool has_trailing_colon = (*end == ':');
|
|
256
|
+
|
|
257
|
+
if (!has_leading_colon && !has_trailing_colon) {
|
|
258
|
+
return NULL; /* No alignment markers */
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* Determine alignment */
|
|
262
|
+
char *align = NULL;
|
|
263
|
+
if (has_leading_colon && has_trailing_colon) {
|
|
264
|
+
align = "center";
|
|
265
|
+
} else if (has_leading_colon) {
|
|
266
|
+
align = "left";
|
|
267
|
+
} else if (has_trailing_colon) {
|
|
268
|
+
align = "right";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* Strip the colons from the text */
|
|
272
|
+
const char *new_start = has_leading_colon ? start + 1 : start;
|
|
273
|
+
const char *new_end = has_trailing_colon ? end - 1 : end;
|
|
274
|
+
|
|
275
|
+
/* Also preserve leading/trailing whitespace that was there originally */
|
|
276
|
+
const char *original_start = text;
|
|
277
|
+
const char *original_end = text + strlen(text) - 1;
|
|
278
|
+
|
|
279
|
+
/* Find where the original leading whitespace ends */
|
|
280
|
+
const char *leading_ws_end = original_start;
|
|
281
|
+
while (leading_ws_end < start && isspace((unsigned char)*leading_ws_end)) {
|
|
282
|
+
leading_ws_end++;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Find where the original trailing whitespace starts */
|
|
286
|
+
const char *trailing_ws_start = original_end;
|
|
287
|
+
while (trailing_ws_start > end && isspace((unsigned char)*trailing_ws_start)) {
|
|
288
|
+
trailing_ws_start--;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Build the new text: leading ws + content (without colons) + trailing ws */
|
|
292
|
+
size_t leading_ws_len = leading_ws_end - original_start;
|
|
293
|
+
size_t content_len = (new_end >= new_start) ? (new_end - new_start + 1) : 0;
|
|
294
|
+
size_t trailing_ws_len = original_end - trailing_ws_start;
|
|
295
|
+
|
|
296
|
+
size_t new_len = leading_ws_len + content_len + trailing_ws_len;
|
|
297
|
+
char *new_text = malloc(new_len + 1);
|
|
298
|
+
if (!new_text) return align; /* Return alignment even if we can't modify text */
|
|
299
|
+
|
|
300
|
+
char *write = new_text;
|
|
301
|
+
/* Copy leading whitespace */
|
|
302
|
+
memcpy(write, original_start, leading_ws_len);
|
|
303
|
+
write += leading_ws_len;
|
|
304
|
+
/* Copy content without colons */
|
|
305
|
+
if (content_len > 0) {
|
|
306
|
+
memcpy(write, new_start, content_len);
|
|
307
|
+
write += content_len;
|
|
308
|
+
}
|
|
309
|
+
/* Copy trailing whitespace */
|
|
310
|
+
if (trailing_ws_len > 0) {
|
|
311
|
+
memcpy(write, trailing_ws_start + 1, trailing_ws_len);
|
|
312
|
+
write += trailing_ws_len;
|
|
313
|
+
}
|
|
314
|
+
*write = '\0';
|
|
315
|
+
|
|
316
|
+
/* Update the text node */
|
|
317
|
+
cmark_node_set_literal(text_node, new_text);
|
|
318
|
+
free(new_text);
|
|
319
|
+
|
|
320
|
+
return align;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Check if a row should be in tfoot (contains === markers)
|
|
325
|
+
*/
|
|
326
|
+
static bool is_tfoot_row(cmark_node *row) {
|
|
327
|
+
if (!row || cmark_node_get_type(row) != CMARK_NODE_TABLE_ROW) return false;
|
|
328
|
+
|
|
329
|
+
cmark_node *cell = cmark_node_first_child(row);
|
|
330
|
+
bool has_equals = false;
|
|
331
|
+
|
|
332
|
+
while (cell) {
|
|
333
|
+
if (cmark_node_get_type(cell) == CMARK_NODE_TABLE_CELL) {
|
|
334
|
+
cmark_node *text_node = cmark_node_first_child(cell);
|
|
335
|
+
if (text_node && cmark_node_get_type(text_node) == CMARK_NODE_TEXT) {
|
|
336
|
+
const char *text = cmark_node_get_literal(text_node);
|
|
337
|
+
if (text) {
|
|
338
|
+
/* Trim whitespace */
|
|
339
|
+
while (*text && isspace((unsigned char)*text)) text++;
|
|
340
|
+
/* Check if it's only === (three or more equals) */
|
|
341
|
+
if (*text == '=' && text[1] == '=' && text[2] == '=') {
|
|
342
|
+
const char *after = text + 3;
|
|
343
|
+
while (*after == '=') after++; /* Allow more equals */
|
|
344
|
+
while (*after && isspace((unsigned char)*after)) after++;
|
|
345
|
+
if (*after == '\0') {
|
|
346
|
+
has_equals = true;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
cell = cmark_node_next(cell);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return has_equals;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Check if a row contains only a caption marker (last row with [Caption])
|
|
360
|
+
* Note: This detection works when captions are parsed as table rows, but currently
|
|
361
|
+
* captions immediately following tables without a blank line are not reliably detected
|
|
362
|
+
* because cmark-gfm parses them as table rows which interferes with detection.
|
|
363
|
+
*/
|
|
364
|
+
static bool is_caption_row(cmark_node *row) {
|
|
365
|
+
if (!row || cmark_node_get_type(row) != CMARK_NODE_TABLE_ROW) return false;
|
|
366
|
+
|
|
367
|
+
/* Check if this row has a cell with [Caption] format */
|
|
368
|
+
/* It might be a single cell, or a cell that spans columns */
|
|
369
|
+
cmark_node *cell = cmark_node_first_child(row);
|
|
370
|
+
int cell_count = 0;
|
|
371
|
+
bool has_caption = false;
|
|
372
|
+
int caption_cell_count = 0;
|
|
373
|
+
|
|
374
|
+
while (cell) {
|
|
375
|
+
if (cmark_node_get_type(cell) == CMARK_NODE_TABLE_CELL) {
|
|
376
|
+
cell_count++;
|
|
377
|
+
cmark_node *text_node = cmark_node_first_child(cell);
|
|
378
|
+
if (text_node && cmark_node_get_type(text_node) == CMARK_NODE_TEXT) {
|
|
379
|
+
const char *text = cmark_node_get_literal(text_node);
|
|
380
|
+
if (text && text[0] == '[') {
|
|
381
|
+
const char *end = strchr(text + 1, ']');
|
|
382
|
+
if (end) {
|
|
383
|
+
const char *after = end + 1;
|
|
384
|
+
while (*after && isspace((unsigned char)*after)) after++;
|
|
385
|
+
if (*after == '\0') {
|
|
386
|
+
has_caption = true;
|
|
387
|
+
caption_cell_count++;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
cell = cmark_node_next(cell);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* Caption row: has caption text, and either:
|
|
397
|
+
* - Single cell with caption, OR
|
|
398
|
+
* - All cells contain caption text (shouldn't happen, but be safe) */
|
|
399
|
+
return has_caption && (cell_count == 1 || caption_cell_count == cell_count);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Add colspan/rowspan attributes to table cells
|
|
404
|
+
* This modifies the AST by setting user_data with HTML attributes
|
|
405
|
+
*/
|
|
406
|
+
static void process_table_spans(cmark_node *table) {
|
|
407
|
+
if (!table || cmark_node_get_type(table) != CMARK_NODE_TABLE) return;
|
|
408
|
+
|
|
409
|
+
/* Walk through table rows - start from first TABLE_ROW node */
|
|
410
|
+
cmark_node *row = cmark_node_first_child(table);
|
|
411
|
+
while (row && cmark_node_get_type(row) != CMARK_NODE_TABLE_ROW) {
|
|
412
|
+
row = cmark_node_next(row); /* Skip non-row nodes */
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
cmark_node *prev_row = NULL;
|
|
416
|
+
bool is_first_row = true; /* Track header row */
|
|
417
|
+
bool in_tfoot_section = false; /* Track if we've entered tfoot section */
|
|
418
|
+
|
|
419
|
+
/* Track active rowspan cells per column (inspired by Jekyll Spaceship).
|
|
420
|
+
* active_rowspan[col] points to the cell node that's currently being rowspanned in that column.
|
|
421
|
+
* When we see a ^^ cell, we merge it with the active cell for that column.
|
|
422
|
+
* This persists across rows - when a regular cell appears, it becomes the new active cell.
|
|
423
|
+
* Initialize all to NULL at the start of each table. */
|
|
424
|
+
cmark_node *active_rowspan[50] = {NULL}; /* One per column, max 50 columns, persists across rows */
|
|
425
|
+
|
|
426
|
+
while (row) {
|
|
427
|
+
if (cmark_node_get_type(row) == CMARK_NODE_TABLE_ROW) {
|
|
428
|
+
/* Process span for header row too - don't skip it */
|
|
429
|
+
if (is_first_row) {
|
|
430
|
+
is_first_row = false;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* Check if this row is a tfoot row (contains ===) */
|
|
434
|
+
/* Once we encounter a tfoot row, all subsequent rows are in tfoot */
|
|
435
|
+
if (is_tfoot_row(row)) {
|
|
436
|
+
/* This is the === row itself - mark it as tfoot and set flag */
|
|
437
|
+
in_tfoot_section = true;
|
|
438
|
+
char *existing = (char *)cmark_node_get_user_data(row);
|
|
439
|
+
if (existing) free(existing);
|
|
440
|
+
cmark_node_set_user_data(row, strdup(" data-tfoot=\"true\""));
|
|
441
|
+
} else if (in_tfoot_section) {
|
|
442
|
+
/* We've already encountered the === row, so mark this row as tfoot */
|
|
443
|
+
char *existing = (char *)cmark_node_get_user_data(row);
|
|
444
|
+
if (existing) free(existing);
|
|
445
|
+
cmark_node_set_user_data(row, strdup(" data-tfoot=\"true\""));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* If this row is the === separator row, mark its cells for removal.
|
|
449
|
+
* We no longer skip processing for tfoot rows entirely, so that colspan
|
|
450
|
+
* logic can still run on footer rows. Rowspan logic will simply not
|
|
451
|
+
* trigger unless ^^ markers are present. */
|
|
452
|
+
if (is_tfoot_row(row)) {
|
|
453
|
+
cmark_node *cell = cmark_node_first_child(row);
|
|
454
|
+
while (cell) {
|
|
455
|
+
if (cmark_node_get_type(cell) == CMARK_NODE_TABLE_CELL) {
|
|
456
|
+
cmark_node *text_node = cmark_node_first_child(cell);
|
|
457
|
+
if (text_node && cmark_node_get_type(text_node) == CMARK_NODE_TEXT) {
|
|
458
|
+
const char *text = cmark_node_get_literal(text_node);
|
|
459
|
+
if (text) {
|
|
460
|
+
/* Trim whitespace */
|
|
461
|
+
while (*text && isspace((unsigned char)*text)) text++;
|
|
462
|
+
/* Check if it's === */
|
|
463
|
+
if (text[0] == '=' && text[1] == '=' && text[2] == '=') {
|
|
464
|
+
const char *after = text + 3;
|
|
465
|
+
while (*after == '=') after++; /* Allow more equals */
|
|
466
|
+
while (*after && isspace((unsigned char)*after)) after++;
|
|
467
|
+
if (*after == '\0') {
|
|
468
|
+
/* This is a === cell - mark for removal */
|
|
469
|
+
char *cell_attrs = (char *)cmark_node_get_user_data(cell);
|
|
470
|
+
if (cell_attrs) free(cell_attrs);
|
|
471
|
+
cmark_node_set_user_data(cell, strdup(" data-remove=\"true\""));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
cell = cmark_node_next(cell);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* Check if this row only contains '—' cells (separator row or empty row) */
|
|
482
|
+
cmark_node *check_cell = cmark_node_first_child(row);
|
|
483
|
+
bool all_dash = true;
|
|
484
|
+
bool has_cells = false;
|
|
485
|
+
while (check_cell) {
|
|
486
|
+
if (cmark_node_get_type(check_cell) == CMARK_NODE_TABLE_CELL) {
|
|
487
|
+
has_cells = true;
|
|
488
|
+
cmark_node *check_text = cmark_node_first_child(check_cell);
|
|
489
|
+
if (check_text && cmark_node_get_type(check_text) == CMARK_NODE_TEXT) {
|
|
490
|
+
const char *check_content = cmark_node_get_literal(check_text);
|
|
491
|
+
if (check_content && strcmp(check_content, "—") != 0) {
|
|
492
|
+
all_dash = false;
|
|
493
|
+
break;
|
|
494
|
+
}
|
|
495
|
+
} else {
|
|
496
|
+
/* Empty cell, check if it's really empty */
|
|
497
|
+
if (cmark_node_first_child(check_cell)) {
|
|
498
|
+
all_dash = false;
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
check_cell = cmark_node_next(check_cell);
|
|
504
|
+
}
|
|
505
|
+
/* Skip rows that only contain '—' characters (alignment/separator rows) */
|
|
506
|
+
if (has_cells && all_dash) {
|
|
507
|
+
/* Mark the entire row for removal in HTML output */
|
|
508
|
+
cmark_node *dash_cell = cmark_node_first_child(row);
|
|
509
|
+
while (dash_cell) {
|
|
510
|
+
if (cmark_node_get_type(dash_cell) == CMARK_NODE_TABLE_CELL) {
|
|
511
|
+
char *existing = (char *)cmark_node_get_user_data(dash_cell);
|
|
512
|
+
if (existing) free(existing);
|
|
513
|
+
cmark_node_set_user_data(dash_cell, strdup(" data-remove=\"true\""));
|
|
514
|
+
}
|
|
515
|
+
dash_cell = cmark_node_next(dash_cell);
|
|
516
|
+
}
|
|
517
|
+
/* Don't update prev_row when skipping - keep it pointing to the previous valid row */
|
|
518
|
+
row = cmark_node_next(row);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
cmark_node *cell = cmark_node_first_child(row);
|
|
523
|
+
cmark_node *prev_cell = NULL; /* Always reset to NULL at start of each row */
|
|
524
|
+
int col_index = 0;
|
|
525
|
+
|
|
526
|
+
/* Note: We don't initialize active_rowspan here because it persists across rows.
|
|
527
|
+
* When we process the first data row, active_rowspan will be NULL for all columns,
|
|
528
|
+
* so ^^ cells will find cells from prev_row and set them as active.
|
|
529
|
+
* Regular cells in subsequent rows will update active_rowspan as they're processed. */
|
|
530
|
+
|
|
531
|
+
while (cell) {
|
|
532
|
+
if (cmark_node_get_type(cell) == CMARK_NODE_TABLE_CELL) {
|
|
533
|
+
/* Capture raw cell content before process_cell_alignment may overwrite user_data.
|
|
534
|
+
* Table parser stores raw content in user_data; we need it for colspan check
|
|
535
|
+
* so "raw <<" is not wrongly treated as colspan. */
|
|
536
|
+
const char *raw_content = NULL;
|
|
537
|
+
{
|
|
538
|
+
void *ud = cmark_node_get_user_data(cell);
|
|
539
|
+
if (ud) {
|
|
540
|
+
char *s = (char *)ud;
|
|
541
|
+
if (s[0] && !strstr(s, "colspan") && !strstr(s, "rowspan") && !strstr(s, "data-"))
|
|
542
|
+
raw_content = s;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* Process per-cell alignment markers (:) BEFORE colspan/rowspan processing
|
|
547
|
+
* so that alignment is preserved when cells are merged. */
|
|
548
|
+
if (g_per_cell_alignment) {
|
|
549
|
+
char *cell_attrs_check = (char *)cmark_node_get_user_data(cell);
|
|
550
|
+
if (!cell_attrs_check || !strstr(cell_attrs_check, "data-remove")) {
|
|
551
|
+
char *align = process_cell_alignment(cell);
|
|
552
|
+
if (align) {
|
|
553
|
+
/* Add style attribute for alignment */
|
|
554
|
+
char *existing_attrs = (char *)cmark_node_get_user_data(cell);
|
|
555
|
+
char new_attrs[256];
|
|
556
|
+
|
|
557
|
+
if (existing_attrs && strlen(existing_attrs) > 0) {
|
|
558
|
+
snprintf(new_attrs, sizeof(new_attrs), "%s style=\"text-align: %s\"", existing_attrs, align);
|
|
559
|
+
} else {
|
|
560
|
+
snprintf(new_attrs, sizeof(new_attrs), " style=\"text-align: %s\"", align);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (existing_attrs) free(existing_attrs);
|
|
564
|
+
cmark_node_set_user_data(cell, strdup(new_attrs));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/* Check for colspan (pass raw_content so we see "raw <<" etc. before user_data was overwritten) */
|
|
570
|
+
bool is_colspan = is_colspan_cell(cell, raw_content);
|
|
571
|
+
/* Final safeguard: if we would merge, re-check parsed content length.
|
|
572
|
+
* If the cell has multiple literal nodes (e.g. "raw " + "<<") the full text
|
|
573
|
+
* is longer than "<<"; do not merge so "raw <<" stays a normal cell. */
|
|
574
|
+
if (is_colspan) {
|
|
575
|
+
char *full = get_node_full_text(cell);
|
|
576
|
+
if (full) {
|
|
577
|
+
const char *p = full;
|
|
578
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
579
|
+
const char *q = p + strlen(p);
|
|
580
|
+
if (q > p) q--;
|
|
581
|
+
while (q >= p && isspace((unsigned char)*q)) q--;
|
|
582
|
+
if (q >= p && (size_t)(q - p + 1) > 2)
|
|
583
|
+
is_colspan = false;
|
|
584
|
+
free(full);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (is_colspan) {
|
|
588
|
+
/* Only process colspan if we have a previous cell in the SAME ROW */
|
|
589
|
+
if (!prev_cell || cmark_node_parent(prev_cell) != row) {
|
|
590
|
+
/* No previous cell in same row, can't do colspan.
|
|
591
|
+
* This happens for:
|
|
592
|
+
* 1. First cell in a row (prev_cell is NULL)
|
|
593
|
+
* 2. First non-empty cell after removed cells (prev_cell is from previous row)
|
|
594
|
+
* In these cases, mark empty cells for removal. */
|
|
595
|
+
cmark_node *child = cmark_node_first_child(cell);
|
|
596
|
+
if (child == NULL) {
|
|
597
|
+
/* Empty cell with no previous cell - mark for removal */
|
|
598
|
+
char *existing = (char *)cmark_node_get_user_data(cell);
|
|
599
|
+
if (existing) free(existing);
|
|
600
|
+
cmark_node_set_user_data(cell, strdup(" data-remove=\"true\""));
|
|
601
|
+
}
|
|
602
|
+
prev_cell = cell;
|
|
603
|
+
col_index++;
|
|
604
|
+
cell = cmark_node_next(cell);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
/* Find the first non-empty cell going backwards (skip cells marked for removal) */
|
|
608
|
+
cmark_node *target_cell = prev_cell;
|
|
609
|
+
while (target_cell) {
|
|
610
|
+
/* Verify target_cell is in the same row */
|
|
611
|
+
if (cmark_node_parent(target_cell) != row) {
|
|
612
|
+
break; /* target_cell is not in the same row, stop */
|
|
613
|
+
}
|
|
614
|
+
char *target_attrs = (char *)cmark_node_get_user_data(target_cell);
|
|
615
|
+
/* Skip cells marked for removal */
|
|
616
|
+
if (!target_attrs || !strstr(target_attrs, "data-remove")) {
|
|
617
|
+
break; /* Found a real cell */
|
|
618
|
+
}
|
|
619
|
+
/* Move to previous cell */
|
|
620
|
+
cmark_node *prev = cmark_node_previous(target_cell);
|
|
621
|
+
while (prev && cmark_node_get_type(prev) != CMARK_NODE_TABLE_CELL) {
|
|
622
|
+
prev = cmark_node_previous(prev);
|
|
623
|
+
}
|
|
624
|
+
target_cell = prev;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (target_cell && cmark_node_parent(target_cell) == row) {
|
|
628
|
+
/* Merge empty cells with the previous cell to create colspan.
|
|
629
|
+
* This handles both:
|
|
630
|
+
* - Consecutive empty cells (like |||) merging together
|
|
631
|
+
* - Empty cells after a content cell (like | header |||) merging with the content cell
|
|
632
|
+
*
|
|
633
|
+
* However, we need to be careful: a single empty cell between two content cells
|
|
634
|
+
* (like | Absent | | 92.00 |) should NOT merge, as that's just a missing value.
|
|
635
|
+
*
|
|
636
|
+
* The distinction: if the target_cell is empty, we're merging consecutive empty cells.
|
|
637
|
+
* If the target_cell has content, we're merging an empty cell with content (colspan).
|
|
638
|
+
* Both cases are valid for creating colspan. */
|
|
639
|
+
|
|
640
|
+
/* Check if target_cell is empty (has no content at all).
|
|
641
|
+
* IMPORTANT: We only merge empty cells that are part of consecutive empty cells (|||).
|
|
642
|
+
* Empty cells with whitespace between pipes (| |) should NOT be merged. */
|
|
643
|
+
cmark_node *target_child = cmark_node_first_child(target_cell);
|
|
644
|
+
bool target_is_empty = (target_child == NULL);
|
|
645
|
+
|
|
646
|
+
/* Check if the next cell (after current) has content.
|
|
647
|
+
* If target has content AND next also has content, don't merge (isolated empty cell).
|
|
648
|
+
* Example: | Absent | | 92.00 | - empty cell between two content cells should NOT merge.
|
|
649
|
+
* But if target has content and next is empty or end-of-row, merge (empty cells after content).
|
|
650
|
+
* Example: | header ||| - empty cells after content should merge. */
|
|
651
|
+
cmark_node *next_cell = cmark_node_next(cell);
|
|
652
|
+
while (next_cell && cmark_node_get_type(next_cell) != CMARK_NODE_TABLE_CELL) {
|
|
653
|
+
next_cell = cmark_node_next(next_cell);
|
|
654
|
+
}
|
|
655
|
+
bool next_has_content = false;
|
|
656
|
+
if (next_cell && cmark_node_get_type(next_cell) == CMARK_NODE_TABLE_CELL) {
|
|
657
|
+
cmark_node *next_child = cmark_node_first_child(next_cell);
|
|
658
|
+
next_has_content = (next_child != NULL);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/* Merge if:
|
|
662
|
+
* 1. Current cell has << marker (explicit colspan marker, always merge), OR
|
|
663
|
+
* 2. Target is empty AND next is also empty (consecutive empty cells from |||), OR
|
|
664
|
+
* 3. Target has content AND next is empty/end (empty cells after content from | header |||)
|
|
665
|
+
*
|
|
666
|
+
* Do NOT merge if:
|
|
667
|
+
* - Target has content AND next also has content (isolated empty cell like | A | | B |)
|
|
668
|
+
* - Target is empty but next has content (empty cell before content, like | | A |) */
|
|
669
|
+
/* For << markers (explicit colspan), always merge regardless of other conditions */
|
|
670
|
+
bool should_merge = is_colspan ||
|
|
671
|
+
(target_is_empty && !next_has_content) ||
|
|
672
|
+
(!target_is_empty && !next_has_content);
|
|
673
|
+
|
|
674
|
+
if (should_merge) {
|
|
675
|
+
/* Target cell is empty or has << marker - merge them (colspan) */
|
|
676
|
+
/* Get or create colspan attribute */
|
|
677
|
+
char *prev_attrs = (char *)cmark_node_get_user_data(target_cell);
|
|
678
|
+
int current_colspan = 1;
|
|
679
|
+
|
|
680
|
+
if (prev_attrs && strstr(prev_attrs, "colspan=")) {
|
|
681
|
+
sscanf(strstr(prev_attrs, "colspan="), "colspan=\"%d\"", ¤t_colspan);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/* Extract style attribute if it exists */
|
|
685
|
+
char style_attr[256] = "";
|
|
686
|
+
if (prev_attrs && strstr(prev_attrs, "style=")) {
|
|
687
|
+
const char *style_start = strstr(prev_attrs, "style=");
|
|
688
|
+
const char *style_end = style_start;
|
|
689
|
+
/* Find the opening quote after style= */
|
|
690
|
+
while (*style_end && *style_end != '"') style_end++;
|
|
691
|
+
if (*style_end == '"') {
|
|
692
|
+
style_end++; /* Skip opening quote */
|
|
693
|
+
/* Find the closing quote */
|
|
694
|
+
while (*style_end && *style_end != '"') style_end++;
|
|
695
|
+
if (*style_end == '"') {
|
|
696
|
+
style_end++; /* Include the closing quote */
|
|
697
|
+
/* Now find the end: space or end of string */
|
|
698
|
+
const char *attr_end = style_end;
|
|
699
|
+
while (*attr_end && *attr_end != ' ' && *attr_end != '\t') attr_end++;
|
|
700
|
+
size_t style_len = attr_end - style_start;
|
|
701
|
+
if (style_len < sizeof(style_attr)) {
|
|
702
|
+
strncpy(style_attr, style_start, style_len);
|
|
703
|
+
style_attr[style_len] = '\0';
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/* Build new attributes - combine style (if any) and colspan */
|
|
710
|
+
char new_attrs[512];
|
|
711
|
+
if (style_attr[0] != '\0') {
|
|
712
|
+
snprintf(new_attrs, sizeof(new_attrs), " %s colspan=\"%d\"", style_attr, current_colspan + 1);
|
|
713
|
+
} else {
|
|
714
|
+
snprintf(new_attrs, sizeof(new_attrs), " colspan=\"%d\"", current_colspan + 1);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/* Free old user_data before setting new */
|
|
718
|
+
if (prev_attrs) free(prev_attrs);
|
|
719
|
+
cmark_node_set_user_data(target_cell, strdup(new_attrs));
|
|
720
|
+
|
|
721
|
+
/* Mark current cell for removal */
|
|
722
|
+
cmark_node_set_user_data(cell, strdup(" data-remove=\"true\""));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/* Check for rowspan */
|
|
727
|
+
else if (is_rowspan_cell(cell) && col_index < 50) {
|
|
728
|
+
/* Use Jekyll Spaceship approach: merge with active rowspan cell for this column.
|
|
729
|
+
* If there's an active rowspan cell, increment its rowspan.
|
|
730
|
+
* Otherwise, find the cell in the previous row and make it active. */
|
|
731
|
+
cmark_node *target_cell = active_rowspan[col_index];
|
|
732
|
+
|
|
733
|
+
/* If no active cell, find one in the previous row */
|
|
734
|
+
if (!target_cell && prev_row) {
|
|
735
|
+
cmark_node *candidate = cmark_node_first_child(prev_row);
|
|
736
|
+
int prev_col = 0;
|
|
737
|
+
|
|
738
|
+
/* Find cell at col_index in the previous row */
|
|
739
|
+
while (candidate) {
|
|
740
|
+
if (cmark_node_get_type(candidate) == CMARK_NODE_TABLE_CELL) {
|
|
741
|
+
if (prev_col == col_index) {
|
|
742
|
+
/* Check if this cell is marked for removal */
|
|
743
|
+
char *cand_attrs = (char *)cmark_node_get_user_data(candidate);
|
|
744
|
+
if (!cand_attrs || !strstr(cand_attrs, "data-remove")) {
|
|
745
|
+
/* Found a real cell (not marked for removal) at this column index */
|
|
746
|
+
target_cell = candidate;
|
|
747
|
+
active_rowspan[col_index] = candidate; /* Make it active */
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
prev_col++;
|
|
752
|
+
}
|
|
753
|
+
candidate = cmark_node_next(candidate);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (target_cell && cmark_node_get_type(target_cell) == CMARK_NODE_TABLE_CELL) {
|
|
758
|
+
/* Get or create rowspan attribute */
|
|
759
|
+
char *prev_attrs = (char *)cmark_node_get_user_data(target_cell);
|
|
760
|
+
int current_rowspan = 1;
|
|
761
|
+
|
|
762
|
+
if (prev_attrs && strstr(prev_attrs, "rowspan=")) {
|
|
763
|
+
sscanf(strstr(prev_attrs, "rowspan="), "rowspan=\"%d\"", ¤t_rowspan);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/* Increment rowspan - append only if prev_attrs looks like HTML attributes
|
|
767
|
+
* (contains '='). The table parser may store raw cell content in user_data;
|
|
768
|
+
* we must not prepend that to rowspan or we get <td Engineering rowspan="2">. */
|
|
769
|
+
char new_attrs[256];
|
|
770
|
+
bool prev_looks_like_attrs = (prev_attrs && strchr(prev_attrs, '=') != NULL);
|
|
771
|
+
if (prev_looks_like_attrs && !strstr(prev_attrs, "rowspan=")) {
|
|
772
|
+
snprintf(new_attrs, sizeof(new_attrs), "%s rowspan=\"%d\"", prev_attrs, current_rowspan + 1);
|
|
773
|
+
} else {
|
|
774
|
+
snprintf(new_attrs, sizeof(new_attrs), " rowspan=\"%d\"", current_rowspan + 1);
|
|
775
|
+
}
|
|
776
|
+
/* Free old user_data before setting new */
|
|
777
|
+
if (prev_attrs) free(prev_attrs);
|
|
778
|
+
cmark_node_set_user_data(target_cell, strdup(new_attrs));
|
|
779
|
+
|
|
780
|
+
}
|
|
781
|
+
/* Always mark rowspan cell for removal, even if target not found */
|
|
782
|
+
char *existing = (char *)cmark_node_get_user_data(cell);
|
|
783
|
+
if (existing) free(existing);
|
|
784
|
+
cmark_node_set_user_data(cell, strdup(" data-remove=\"true\""));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/* If this is a regular cell (not rowspan), it becomes the new active cell for this column.
|
|
788
|
+
* This happens AFTER processing rowspan cells, so regular cells replace the previous active cell.
|
|
789
|
+
* This is the Jekyll Spaceship approach: regular cells become the new active cells.
|
|
790
|
+
*
|
|
791
|
+
* IMPORTANT: We set this AFTER processing the cell, so that when we process ^^ cells
|
|
792
|
+
* in the NEXT row, they will use the correct active cell from THIS row. */
|
|
793
|
+
if (!is_rowspan_cell(cell)) {
|
|
794
|
+
char *cell_attrs = (char *)cmark_node_get_user_data(cell);
|
|
795
|
+
if (!cell_attrs || !strstr(cell_attrs, "data-remove")) {
|
|
796
|
+
/* This is a regular cell, so it becomes the new active cell for this column */
|
|
797
|
+
active_rowspan[col_index] = cell;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
prev_cell = cell;
|
|
802
|
+
col_index++;
|
|
803
|
+
}
|
|
804
|
+
cell = cmark_node_next(cell);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* Update prev_row after processing this row */
|
|
808
|
+
prev_row = row;
|
|
809
|
+
}
|
|
810
|
+
row = cmark_node_next(row);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Check if a paragraph is a table caption ([Caption Text])
|
|
816
|
+
*/
|
|
817
|
+
/**
|
|
818
|
+
* Check if a paragraph is adjacent to a table (before or after)
|
|
819
|
+
*/
|
|
820
|
+
static bool is_adjacent_to_table(cmark_node *para) {
|
|
821
|
+
if (!para) return false;
|
|
822
|
+
|
|
823
|
+
/* Check previous sibling, skipping blank paragraphs */
|
|
824
|
+
cmark_node *prev = cmark_node_previous(para);
|
|
825
|
+
while (prev) {
|
|
826
|
+
cmark_node_type prev_type = cmark_node_get_type(prev);
|
|
827
|
+
if (prev_type == CMARK_NODE_TABLE) {
|
|
828
|
+
return true;
|
|
829
|
+
} else if (prev_type == CMARK_NODE_PARAGRAPH) {
|
|
830
|
+
/* Check if paragraph is blank */
|
|
831
|
+
cmark_node *child = cmark_node_first_child(prev);
|
|
832
|
+
bool is_blank = true;
|
|
833
|
+
if (child && cmark_node_get_type(child) == CMARK_NODE_TEXT) {
|
|
834
|
+
const char *text = cmark_node_get_literal(child);
|
|
835
|
+
if (text) {
|
|
836
|
+
const char *p = text;
|
|
837
|
+
while (*p) {
|
|
838
|
+
if (!isspace((unsigned char)*p)) {
|
|
839
|
+
is_blank = false;
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
p++;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
} else if (child) {
|
|
846
|
+
is_blank = false; /* Has non-text content */
|
|
847
|
+
}
|
|
848
|
+
if (!is_blank) {
|
|
849
|
+
break; /* Found non-blank paragraph, stop looking */
|
|
850
|
+
}
|
|
851
|
+
prev = cmark_node_previous(prev); /* Skip blank paragraph */
|
|
852
|
+
} else {
|
|
853
|
+
break; /* Not a table or paragraph, stop looking */
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* Check next sibling, skipping blank paragraphs */
|
|
858
|
+
cmark_node *next = cmark_node_next(para);
|
|
859
|
+
while (next) {
|
|
860
|
+
cmark_node_type next_type = cmark_node_get_type(next);
|
|
861
|
+
if (next_type == CMARK_NODE_TABLE) {
|
|
862
|
+
return true;
|
|
863
|
+
} else if (next_type == CMARK_NODE_PARAGRAPH) {
|
|
864
|
+
/* Check if paragraph is blank */
|
|
865
|
+
cmark_node *child = cmark_node_first_child(next);
|
|
866
|
+
bool is_blank = true;
|
|
867
|
+
if (child && cmark_node_get_type(child) == CMARK_NODE_TEXT) {
|
|
868
|
+
const char *text = cmark_node_get_literal(child);
|
|
869
|
+
if (text) {
|
|
870
|
+
const char *p = text;
|
|
871
|
+
while (*p) {
|
|
872
|
+
if (!isspace((unsigned char)*p)) {
|
|
873
|
+
is_blank = false;
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
p++;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
} else if (child) {
|
|
880
|
+
is_blank = false; /* Has non-text content */
|
|
881
|
+
}
|
|
882
|
+
if (!is_blank) {
|
|
883
|
+
break; /* Found non-blank paragraph, stop looking */
|
|
884
|
+
}
|
|
885
|
+
next = cmark_node_next(next); /* Skip blank paragraph */
|
|
886
|
+
} else {
|
|
887
|
+
break; /* Not a table or paragraph, stop looking */
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Parse IAL attributes from text (supports both {: ...} and {#id .class} formats)
|
|
896
|
+
* Returns attributes structure or NULL if no IAL found
|
|
897
|
+
*/
|
|
898
|
+
static apex_attributes *parse_ial_from_text(const char *text) {
|
|
899
|
+
if (!text) return NULL;
|
|
900
|
+
|
|
901
|
+
/* Look for IAL pattern: {: ... } or {#id .class} */
|
|
902
|
+
/* Search from the end backwards to find the last IAL (in case there are multiple) */
|
|
903
|
+
const char *ial_start = NULL;
|
|
904
|
+
|
|
905
|
+
/* Find the last opening brace that looks like an IAL */
|
|
906
|
+
const char *p = text;
|
|
907
|
+
const char *last_ial = NULL;
|
|
908
|
+
|
|
909
|
+
while (*p) {
|
|
910
|
+
if (*p == '{') {
|
|
911
|
+
/* Check if this looks like an IAL */
|
|
912
|
+
if (p[1] == ':') {
|
|
913
|
+
/* Kramdown format: {: ... } */
|
|
914
|
+
last_ial = p + 2; /* Skip {: */
|
|
915
|
+
} else if (p[1] == '#' || p[1] == '.') {
|
|
916
|
+
/* Pandoc format: {#id .class} */
|
|
917
|
+
last_ial = p + 1; /* Skip { */
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
p++;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (last_ial) {
|
|
924
|
+
ial_start = last_ial;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (!ial_start) return NULL;
|
|
928
|
+
|
|
929
|
+
/* Find closing brace */
|
|
930
|
+
const char *ial_end = strchr(ial_start, '}');
|
|
931
|
+
if (!ial_end) return NULL;
|
|
932
|
+
|
|
933
|
+
/* Parse IAL content */
|
|
934
|
+
int content_len = ial_end - ial_start;
|
|
935
|
+
if (content_len <= 0) return NULL;
|
|
936
|
+
|
|
937
|
+
/* Use parse_ial_content from ial.c - it already handles the content format */
|
|
938
|
+
/* We need to access the static function, so we'll duplicate the logic here */
|
|
939
|
+
/* Actually, we can't access static functions, so we'll create a wrapper */
|
|
940
|
+
apex_attributes *attrs = malloc(sizeof(apex_attributes));
|
|
941
|
+
if (!attrs) return NULL;
|
|
942
|
+
memset(attrs, 0, sizeof(apex_attributes));
|
|
943
|
+
|
|
944
|
+
char buffer[2048];
|
|
945
|
+
if (content_len >= (int)sizeof(buffer)) content_len = (int)sizeof(buffer) - 1;
|
|
946
|
+
memcpy(buffer, ial_start, content_len);
|
|
947
|
+
buffer[content_len] = '\0';
|
|
948
|
+
|
|
949
|
+
/* Parse attributes manually (similar to parse_ial_content) */
|
|
950
|
+
char *buf_p = buffer;
|
|
951
|
+
while (*buf_p) {
|
|
952
|
+
/* Skip whitespace */
|
|
953
|
+
while (isspace((unsigned char)*buf_p)) buf_p++;
|
|
954
|
+
if (!*buf_p) break;
|
|
955
|
+
|
|
956
|
+
/* Check for ID (#id) */
|
|
957
|
+
if (*buf_p == '#') {
|
|
958
|
+
buf_p++;
|
|
959
|
+
char *id_start = buf_p;
|
|
960
|
+
while (*buf_p && !isspace((unsigned char)*buf_p) && *buf_p != '.' && *buf_p != '}') buf_p++;
|
|
961
|
+
if (buf_p > id_start) {
|
|
962
|
+
char saved = *buf_p;
|
|
963
|
+
*buf_p = '\0';
|
|
964
|
+
attrs->id = strdup(id_start);
|
|
965
|
+
*buf_p = saved;
|
|
966
|
+
}
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/* Check for class (.class) */
|
|
971
|
+
if (*buf_p == '.') {
|
|
972
|
+
buf_p++;
|
|
973
|
+
char *class_start = buf_p;
|
|
974
|
+
while (*buf_p && !isspace((unsigned char)*buf_p) && *buf_p != '.' && *buf_p != '#' && *buf_p != '}') buf_p++;
|
|
975
|
+
if (buf_p > class_start) {
|
|
976
|
+
char saved = *buf_p;
|
|
977
|
+
*buf_p = '\0';
|
|
978
|
+
attrs->classes = realloc(attrs->classes, sizeof(char*) * (attrs->class_count + 1));
|
|
979
|
+
attrs->classes[attrs->class_count++] = strdup(class_start);
|
|
980
|
+
*buf_p = saved;
|
|
981
|
+
}
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/* Check for key="value" */
|
|
986
|
+
char *key_start = buf_p;
|
|
987
|
+
while (*buf_p && *buf_p != '=' && *buf_p != ' ' && *buf_p != '\t' && *buf_p != '}') buf_p++;
|
|
988
|
+
|
|
989
|
+
if (*buf_p == '=') {
|
|
990
|
+
char saved = *buf_p;
|
|
991
|
+
*buf_p = '\0';
|
|
992
|
+
char *key = strdup(key_start);
|
|
993
|
+
*buf_p = saved;
|
|
994
|
+
buf_p++; /* Skip = */
|
|
995
|
+
|
|
996
|
+
/* Parse value */
|
|
997
|
+
char *value = NULL;
|
|
998
|
+
if (*buf_p == '"' || *buf_p == '\'') {
|
|
999
|
+
char quote = *buf_p++;
|
|
1000
|
+
char *value_start = buf_p;
|
|
1001
|
+
while (*buf_p && *buf_p != quote) {
|
|
1002
|
+
if (*buf_p == '\\' && *(buf_p+1)) buf_p++;
|
|
1003
|
+
buf_p++;
|
|
1004
|
+
}
|
|
1005
|
+
if (*buf_p == quote) {
|
|
1006
|
+
*buf_p = '\0';
|
|
1007
|
+
value = strdup(value_start);
|
|
1008
|
+
*buf_p = quote;
|
|
1009
|
+
buf_p++;
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
char *value_start = buf_p;
|
|
1013
|
+
while (*buf_p && !isspace((unsigned char)*buf_p) && *buf_p != '}') buf_p++;
|
|
1014
|
+
char saved_val = *buf_p;
|
|
1015
|
+
*buf_p = '\0';
|
|
1016
|
+
value = strdup(value_start);
|
|
1017
|
+
*buf_p = saved_val;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
attrs->keys = realloc(attrs->keys, sizeof(char*) * (attrs->attr_count + 1));
|
|
1021
|
+
attrs->values = realloc(attrs->values, sizeof(char*) * (attrs->attr_count + 1));
|
|
1022
|
+
attrs->keys[attrs->attr_count] = key;
|
|
1023
|
+
attrs->values[attrs->attr_count] = value ? value : strdup("");
|
|
1024
|
+
attrs->attr_count++;
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/* Unknown token, skip */
|
|
1029
|
+
buf_p++;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return attrs;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Convert attributes to HTML attribute string
|
|
1037
|
+
*/
|
|
1038
|
+
static char *attributes_to_html_string(apex_attributes *attrs) {
|
|
1039
|
+
if (!attrs) return NULL;
|
|
1040
|
+
|
|
1041
|
+
char buffer[4096];
|
|
1042
|
+
char *p = buffer;
|
|
1043
|
+
bool first_attr = true;
|
|
1044
|
+
|
|
1045
|
+
#define APPEND(s) do { \
|
|
1046
|
+
size_t len = strlen(s); \
|
|
1047
|
+
if ((size_t)(p - buffer) + len < sizeof(buffer)) { \
|
|
1048
|
+
memcpy(p, s, len); \
|
|
1049
|
+
p += len; \
|
|
1050
|
+
} \
|
|
1051
|
+
} while (0)
|
|
1052
|
+
|
|
1053
|
+
if (attrs->id) {
|
|
1054
|
+
APPEND(" id=\"");
|
|
1055
|
+
APPEND(attrs->id);
|
|
1056
|
+
APPEND("\"");
|
|
1057
|
+
first_attr = false;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (attrs->class_count > 0) {
|
|
1061
|
+
if (first_attr) {
|
|
1062
|
+
APPEND(" class=\"");
|
|
1063
|
+
} else {
|
|
1064
|
+
APPEND(" class=\"");
|
|
1065
|
+
}
|
|
1066
|
+
for (int i = 0; i < attrs->class_count; i++) {
|
|
1067
|
+
if (i > 0) APPEND(" ");
|
|
1068
|
+
APPEND(attrs->classes[i]);
|
|
1069
|
+
}
|
|
1070
|
+
APPEND("\"");
|
|
1071
|
+
first_attr = false;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
1075
|
+
char attr_str[1024];
|
|
1076
|
+
const char *val = attrs->values[i];
|
|
1077
|
+
if (first_attr) {
|
|
1078
|
+
snprintf(attr_str, sizeof(attr_str), "%s=\"%s\"", attrs->keys[i], val);
|
|
1079
|
+
first_attr = false;
|
|
1080
|
+
} else {
|
|
1081
|
+
snprintf(attr_str, sizeof(attr_str), " %s=\"%s\"", attrs->keys[i], val);
|
|
1082
|
+
}
|
|
1083
|
+
APPEND(attr_str);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
#undef APPEND
|
|
1087
|
+
|
|
1088
|
+
*p = '\0';
|
|
1089
|
+
return strdup(buffer);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Check if a paragraph is a table caption format (without adjacency check)
|
|
1094
|
+
* Used when we're already looking backwards from a table and know it's adjacent
|
|
1095
|
+
*/
|
|
1096
|
+
static bool is_table_caption_format(cmark_node *para, char **caption_text, const char **original_text_ptr) {
|
|
1097
|
+
if (!para || cmark_node_get_type(para) != CMARK_NODE_PARAGRAPH) {
|
|
1098
|
+
return false;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/* Collect all text from all text nodes in the paragraph */
|
|
1102
|
+
cmark_node *text_node = cmark_node_first_child(para);
|
|
1103
|
+
if (!text_node) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/* Build full text by concatenating all text nodes */
|
|
1108
|
+
size_t text_len = 0;
|
|
1109
|
+
cmark_node *node = text_node;
|
|
1110
|
+
while (node) {
|
|
1111
|
+
if (cmark_node_get_type(node) == CMARK_NODE_TEXT) {
|
|
1112
|
+
const char *node_text = cmark_node_get_literal(node);
|
|
1113
|
+
if (node_text) {
|
|
1114
|
+
text_len += strlen(node_text);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
node = cmark_node_next(node);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (text_len == 0) return false;
|
|
1121
|
+
|
|
1122
|
+
/* Allocate buffer for full text */
|
|
1123
|
+
char *full_text = malloc(text_len + 1);
|
|
1124
|
+
if (!full_text) return false;
|
|
1125
|
+
full_text[0] = '\0';
|
|
1126
|
+
|
|
1127
|
+
/* Concatenate all text nodes */
|
|
1128
|
+
node = text_node;
|
|
1129
|
+
while (node) {
|
|
1130
|
+
if (cmark_node_get_type(node) == CMARK_NODE_TEXT) {
|
|
1131
|
+
const char *node_text = cmark_node_get_literal(node);
|
|
1132
|
+
if (node_text) {
|
|
1133
|
+
strcat(full_text, node_text);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
node = cmark_node_next(node);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const char *text = full_text;
|
|
1140
|
+
|
|
1141
|
+
/* Check for [Caption] format */
|
|
1142
|
+
if (text[0] == '[') {
|
|
1143
|
+
const char *end = strchr(text + 1, ']');
|
|
1144
|
+
if (end) {
|
|
1145
|
+
const char *after = end + 1;
|
|
1146
|
+
while (*after && isspace((unsigned char)*after)) after++;
|
|
1147
|
+
|
|
1148
|
+
bool has_ial = false;
|
|
1149
|
+
if (*after == '{') {
|
|
1150
|
+
if ((after[1] == ':' || after[1] == '#' || after[1] == '.') &&
|
|
1151
|
+
strchr(after, '}')) {
|
|
1152
|
+
has_ial = true;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (!has_ial && *after == '\0') {
|
|
1157
|
+
size_t len = end - (text + 1);
|
|
1158
|
+
*caption_text = malloc(len + 1);
|
|
1159
|
+
if (*caption_text) {
|
|
1160
|
+
memcpy(*caption_text, text + 1, len);
|
|
1161
|
+
(*caption_text)[len] = '\0';
|
|
1162
|
+
}
|
|
1163
|
+
/* Store full_text pointer in paragraph user_data so we can free it later */
|
|
1164
|
+
char *existing_data = (char *)cmark_node_get_user_data(para);
|
|
1165
|
+
if (existing_data) free(existing_data);
|
|
1166
|
+
cmark_node_set_user_data(para, full_text);
|
|
1167
|
+
if (original_text_ptr) {
|
|
1168
|
+
*original_text_ptr = text;
|
|
1169
|
+
}
|
|
1170
|
+
return true;
|
|
1171
|
+
} else if (has_ial) {
|
|
1172
|
+
size_t len = end - (text + 1);
|
|
1173
|
+
*caption_text = malloc(len + 1);
|
|
1174
|
+
if (*caption_text) {
|
|
1175
|
+
memcpy(*caption_text, text + 1, len);
|
|
1176
|
+
(*caption_text)[len] = '\0';
|
|
1177
|
+
}
|
|
1178
|
+
/* Store full_text pointer in paragraph user_data so we can free it later */
|
|
1179
|
+
char *existing_data = (char *)cmark_node_get_user_data(para);
|
|
1180
|
+
if (existing_data) free(existing_data);
|
|
1181
|
+
cmark_node_set_user_data(para, full_text);
|
|
1182
|
+
if (original_text_ptr) {
|
|
1183
|
+
*original_text_ptr = text;
|
|
1184
|
+
}
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/* Check for : caption format (without adjacency check since we're already looking backwards) */
|
|
1191
|
+
const char *p = text;
|
|
1192
|
+
size_t text_length = strlen(p);
|
|
1193
|
+
int spaces = 0;
|
|
1194
|
+
/* Allow up to 4 spaces (definition list allows 3, so 4+ prevents definition list matching) */
|
|
1195
|
+
while (spaces < 4 && spaces < (int)text_length && p[spaces] == ' ') {
|
|
1196
|
+
spaces++;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if (spaces < (int)text_length && p[spaces] == ':' && (spaces + 1) < (int)text_length) {
|
|
1200
|
+
if (p[spaces + 1] == ' ' || p[spaces + 1] == '\t') {
|
|
1201
|
+
const char *caption_start = p + spaces + 2;
|
|
1202
|
+
const char *caption_end = p + strlen(p);
|
|
1203
|
+
const char *ial_start = NULL;
|
|
1204
|
+
|
|
1205
|
+
/* Look for IAL at the end */
|
|
1206
|
+
const char *text_end = p + text_length;
|
|
1207
|
+
const char *search = caption_end - 1;
|
|
1208
|
+
while (search >= caption_start) {
|
|
1209
|
+
if (*search == '}') {
|
|
1210
|
+
const char *open = search;
|
|
1211
|
+
while (open >= caption_start && *open != '{') {
|
|
1212
|
+
open--;
|
|
1213
|
+
}
|
|
1214
|
+
if (open >= caption_start && *open == '{' && (open + 1) < text_end) {
|
|
1215
|
+
if ((open[1] == ':' || open[1] == '#' || open[1] == '.') &&
|
|
1216
|
+
search > open) {
|
|
1217
|
+
ial_start = open;
|
|
1218
|
+
caption_end = open;
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
search--;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/* Extract caption text (trim whitespace) */
|
|
1227
|
+
while (caption_start < caption_end && isspace((unsigned char)*caption_start)) {
|
|
1228
|
+
caption_start++;
|
|
1229
|
+
}
|
|
1230
|
+
while (caption_end > caption_start && isspace((unsigned char)*(caption_end - 1))) {
|
|
1231
|
+
caption_end--;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (caption_end > caption_start) {
|
|
1235
|
+
size_t len = caption_end - caption_start;
|
|
1236
|
+
*caption_text = malloc(len + 1);
|
|
1237
|
+
if (*caption_text) {
|
|
1238
|
+
memcpy(*caption_text, caption_start, len);
|
|
1239
|
+
(*caption_text)[len] = '\0';
|
|
1240
|
+
}
|
|
1241
|
+
/* Store full_text pointer in paragraph user_data so we can free it later */
|
|
1242
|
+
char *existing_data = (char *)cmark_node_get_user_data(para);
|
|
1243
|
+
if (existing_data) free(existing_data);
|
|
1244
|
+
cmark_node_set_user_data(para, full_text);
|
|
1245
|
+
if (original_text_ptr) {
|
|
1246
|
+
*original_text_ptr = text;
|
|
1247
|
+
}
|
|
1248
|
+
return true;
|
|
1249
|
+
} else if (ial_start) {
|
|
1250
|
+
*caption_text = strdup("");
|
|
1251
|
+
/* Store full_text pointer in paragraph user_data so we can free it later */
|
|
1252
|
+
char *existing_data = (char *)cmark_node_get_user_data(para);
|
|
1253
|
+
if (existing_data) free(existing_data);
|
|
1254
|
+
cmark_node_set_user_data(para, full_text);
|
|
1255
|
+
if (original_text_ptr) {
|
|
1256
|
+
*original_text_ptr = text;
|
|
1257
|
+
}
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/* Not a caption format - free allocated memory */
|
|
1264
|
+
/* Note: We don't modify user_data here because it might contain important state
|
|
1265
|
+
* from previous checks. The caller should handle cleanup if needed. */
|
|
1266
|
+
free(full_text);
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
static bool is_table_caption(cmark_node *para, char **caption_text, const char **original_text_ptr) {
|
|
1271
|
+
if (!para || cmark_node_get_type(para) != CMARK_NODE_PARAGRAPH) {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/* Collect all text from all text nodes in the paragraph */
|
|
1276
|
+
/* This is necessary because IAL might be in a separate text node after whitespace */
|
|
1277
|
+
cmark_node *text_node = cmark_node_first_child(para);
|
|
1278
|
+
if (!text_node) return false;
|
|
1279
|
+
|
|
1280
|
+
/* Build full text by concatenating all text nodes */
|
|
1281
|
+
size_t text_len = 0;
|
|
1282
|
+
cmark_node *node = text_node;
|
|
1283
|
+
while (node) {
|
|
1284
|
+
if (cmark_node_get_type(node) == CMARK_NODE_TEXT) {
|
|
1285
|
+
const char *node_text = cmark_node_get_literal(node);
|
|
1286
|
+
if (node_text) {
|
|
1287
|
+
text_len += strlen(node_text);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
node = cmark_node_next(node);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
if (text_len == 0) return false;
|
|
1294
|
+
|
|
1295
|
+
/* Allocate buffer for full text */
|
|
1296
|
+
char *full_text = malloc(text_len + 1);
|
|
1297
|
+
if (!full_text) return false;
|
|
1298
|
+
full_text[0] = '\0';
|
|
1299
|
+
|
|
1300
|
+
/* Concatenate all text nodes */
|
|
1301
|
+
node = text_node;
|
|
1302
|
+
while (node) {
|
|
1303
|
+
if (cmark_node_get_type(node) == CMARK_NODE_TEXT) {
|
|
1304
|
+
const char *node_text = cmark_node_get_literal(node);
|
|
1305
|
+
if (node_text) {
|
|
1306
|
+
strcat(full_text, node_text);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
node = cmark_node_next(node);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const char *text = full_text;
|
|
1313
|
+
|
|
1314
|
+
/* Check for [Caption] format */
|
|
1315
|
+
if (text[0] == '[') {
|
|
1316
|
+
const char *end = strchr(text + 1, ']');
|
|
1317
|
+
if (end) {
|
|
1318
|
+
/* Check if there's IAL after the closing bracket */
|
|
1319
|
+
const char *after = end + 1;
|
|
1320
|
+
while (*after && isspace((unsigned char)*after)) after++;
|
|
1321
|
+
|
|
1322
|
+
/* Look for IAL pattern after bracket */
|
|
1323
|
+
bool has_ial = false;
|
|
1324
|
+
if (*after == '{') {
|
|
1325
|
+
if ((after[1] == ':' || after[1] == '#' || after[1] == '.') &&
|
|
1326
|
+
strchr(after, '}')) {
|
|
1327
|
+
has_ial = true;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (!has_ial && *after == '\0') {
|
|
1332
|
+
/* No IAL, just [Caption] */
|
|
1333
|
+
size_t len = end - (text + 1);
|
|
1334
|
+
*caption_text = malloc(len + 1);
|
|
1335
|
+
if (*caption_text) {
|
|
1336
|
+
memcpy(*caption_text, text + 1, len);
|
|
1337
|
+
(*caption_text)[len] = '\0';
|
|
1338
|
+
}
|
|
1339
|
+
/* Store full_text pointer in paragraph user_data so we can free it later */
|
|
1340
|
+
char *existing_data = (char *)cmark_node_get_user_data(para);
|
|
1341
|
+
if (existing_data) free(existing_data);
|
|
1342
|
+
cmark_node_set_user_data(para, full_text);
|
|
1343
|
+
if (original_text_ptr) {
|
|
1344
|
+
*original_text_ptr = text;
|
|
1345
|
+
}
|
|
1346
|
+
return true;
|
|
1347
|
+
} else if (has_ial) {
|
|
1348
|
+
/* Has IAL after [Caption] */
|
|
1349
|
+
size_t len = end - (text + 1);
|
|
1350
|
+
*caption_text = malloc(len + 1);
|
|
1351
|
+
if (*caption_text) {
|
|
1352
|
+
memcpy(*caption_text, text + 1, len);
|
|
1353
|
+
(*caption_text)[len] = '\0';
|
|
1354
|
+
}
|
|
1355
|
+
/* Store full_text pointer in paragraph user_data so we can free it later */
|
|
1356
|
+
char *existing_data = (char *)cmark_node_get_user_data(para);
|
|
1357
|
+
if (existing_data) free(existing_data);
|
|
1358
|
+
cmark_node_set_user_data(para, full_text);
|
|
1359
|
+
if (original_text_ptr) {
|
|
1360
|
+
*original_text_ptr = text;
|
|
1361
|
+
}
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/* Check for : caption format (Pandoc-style) */
|
|
1368
|
+
/* Only recognize this if adjacent to a table to avoid conflict with definition lists */
|
|
1369
|
+
if (is_adjacent_to_table(para)) {
|
|
1370
|
+
const char *p = text;
|
|
1371
|
+
|
|
1372
|
+
/* Skip leading whitespace (up to 3 spaces, matching definition list rules) */
|
|
1373
|
+
int spaces = 0;
|
|
1374
|
+
while (spaces < 3 && p[spaces] == ' ') {
|
|
1375
|
+
spaces++;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/* Must start with : */
|
|
1379
|
+
if (p[spaces] == ':') {
|
|
1380
|
+
/* Must be followed by space or tab */
|
|
1381
|
+
if (p[spaces + 1] == ' ' || p[spaces + 1] == '\t') {
|
|
1382
|
+
const char *caption_start = p + spaces + 2; /* Skip : and space */
|
|
1383
|
+
|
|
1384
|
+
/* Find IAL at the end (if any) */
|
|
1385
|
+
const char *ial_start = NULL;
|
|
1386
|
+
const char *caption_end = p + strlen(p);
|
|
1387
|
+
|
|
1388
|
+
/* Look for IAL pattern from the end */
|
|
1389
|
+
const char *search = caption_end - 1;
|
|
1390
|
+
const char *text_end_here = text + strlen(text);
|
|
1391
|
+
while (search >= caption_start) {
|
|
1392
|
+
if (*search == '}') {
|
|
1393
|
+
/* Found closing brace, look backwards for opening brace */
|
|
1394
|
+
const char *open = search;
|
|
1395
|
+
while (open >= caption_start && *open != '{') {
|
|
1396
|
+
open--;
|
|
1397
|
+
}
|
|
1398
|
+
if (open >= caption_start && *open == '{' && (open + 1) < text_end_here) {
|
|
1399
|
+
/* Check if it's a valid IAL pattern */
|
|
1400
|
+
if ((open[1] == ':' || open[1] == '#' || open[1] == '.') &&
|
|
1401
|
+
search > open) {
|
|
1402
|
+
ial_start = open;
|
|
1403
|
+
caption_end = open; /* Caption ends before IAL */
|
|
1404
|
+
break;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
search--;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/* Extract caption text (trim whitespace) */
|
|
1412
|
+
while (caption_start < caption_end && isspace((unsigned char)*caption_start)) {
|
|
1413
|
+
caption_start++;
|
|
1414
|
+
}
|
|
1415
|
+
while (caption_end > caption_start && isspace((unsigned char)*(caption_end - 1))) {
|
|
1416
|
+
caption_end--;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
if (caption_end > caption_start) {
|
|
1420
|
+
size_t len = caption_end - caption_start;
|
|
1421
|
+
*caption_text = malloc(len + 1);
|
|
1422
|
+
if (*caption_text) {
|
|
1423
|
+
memcpy(*caption_text, caption_start, len);
|
|
1424
|
+
(*caption_text)[len] = '\0';
|
|
1425
|
+
}
|
|
1426
|
+
/* Store full_text pointer in paragraph user_data so we can free it later */
|
|
1427
|
+
char *existing_data = (char *)cmark_node_get_user_data(para);
|
|
1428
|
+
if (existing_data) free(existing_data);
|
|
1429
|
+
cmark_node_set_user_data(para, full_text);
|
|
1430
|
+
if (original_text_ptr) {
|
|
1431
|
+
*original_text_ptr = text;
|
|
1432
|
+
}
|
|
1433
|
+
return true;
|
|
1434
|
+
} else if (ial_start) {
|
|
1435
|
+
/* Caption is empty but has IAL - still valid */
|
|
1436
|
+
*caption_text = strdup("");
|
|
1437
|
+
/* Store full_text pointer in paragraph user_data so we can free it later */
|
|
1438
|
+
char *existing_data = (char *)cmark_node_get_user_data(para);
|
|
1439
|
+
if (existing_data) free(existing_data);
|
|
1440
|
+
cmark_node_set_user_data(para, full_text);
|
|
1441
|
+
if (original_text_ptr) {
|
|
1442
|
+
*original_text_ptr = text;
|
|
1443
|
+
}
|
|
1444
|
+
return true;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/* If we got here, no caption format was found - free allocated memory */
|
|
1451
|
+
free(full_text);
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Add caption to table and extract IAL attributes from caption text
|
|
1457
|
+
*/
|
|
1458
|
+
static void add_table_caption(cmark_node *table, const char *caption, const char *original_text) {
|
|
1459
|
+
if (!table || !caption) return;
|
|
1460
|
+
|
|
1461
|
+
/* Extract IAL attributes from original text if present */
|
|
1462
|
+
apex_attributes *ial_attrs = NULL;
|
|
1463
|
+
if (original_text) {
|
|
1464
|
+
ial_attrs = parse_ial_from_text(original_text);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/* Store caption in user_data */
|
|
1468
|
+
char *existing_user_data = (char *)cmark_node_get_user_data(table);
|
|
1469
|
+
|
|
1470
|
+
/* Check if caption is already in existing data */
|
|
1471
|
+
if (existing_user_data && strstr(existing_user_data, "data-caption=")) {
|
|
1472
|
+
if (ial_attrs) {
|
|
1473
|
+
apex_free_attributes(ial_attrs);
|
|
1474
|
+
}
|
|
1475
|
+
return; /* Caption already present */
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
char *attrs = malloc(strlen(caption) + 50);
|
|
1479
|
+
if (attrs) {
|
|
1480
|
+
snprintf(attrs, strlen(caption) + 50, " data-caption=\"%s\"", caption);
|
|
1481
|
+
|
|
1482
|
+
/* Append IAL attributes if found */
|
|
1483
|
+
if (ial_attrs) {
|
|
1484
|
+
char *ial_str = attributes_to_html_string(ial_attrs);
|
|
1485
|
+
if (ial_str) {
|
|
1486
|
+
size_t new_len = strlen(attrs) + strlen(ial_str) + 1;
|
|
1487
|
+
char *new_attrs = realloc(attrs, new_len);
|
|
1488
|
+
if (new_attrs) {
|
|
1489
|
+
attrs = new_attrs;
|
|
1490
|
+
strcat(attrs, ial_str);
|
|
1491
|
+
free(ial_str);
|
|
1492
|
+
} else {
|
|
1493
|
+
free(ial_str);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
apex_free_attributes(ial_attrs);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/* Append to existing user_data if present */
|
|
1500
|
+
if (existing_user_data) {
|
|
1501
|
+
char *combined = malloc(strlen(existing_user_data) + strlen(attrs) + 1);
|
|
1502
|
+
if (combined) {
|
|
1503
|
+
strcpy(combined, existing_user_data);
|
|
1504
|
+
strcat(combined, attrs);
|
|
1505
|
+
free(existing_user_data); /* Free old user_data before replacing */
|
|
1506
|
+
cmark_node_set_user_data(table, combined);
|
|
1507
|
+
free(attrs);
|
|
1508
|
+
} else {
|
|
1509
|
+
free(attrs);
|
|
1510
|
+
}
|
|
1511
|
+
} else {
|
|
1512
|
+
cmark_node_set_user_data(table, attrs);
|
|
1513
|
+
}
|
|
1514
|
+
} else if (ial_attrs) {
|
|
1515
|
+
/* No caption text but IAL attributes found - apply them */
|
|
1516
|
+
char *ial_str = attributes_to_html_string(ial_attrs);
|
|
1517
|
+
if (ial_str) {
|
|
1518
|
+
char *existing = (char *)cmark_node_get_user_data(table);
|
|
1519
|
+
if (existing) {
|
|
1520
|
+
char *combined = malloc(strlen(existing) + strlen(ial_str) + 1);
|
|
1521
|
+
if (combined) {
|
|
1522
|
+
strcpy(combined, existing);
|
|
1523
|
+
strcat(combined, ial_str);
|
|
1524
|
+
free(existing);
|
|
1525
|
+
cmark_node_set_user_data(table, combined);
|
|
1526
|
+
free(ial_str);
|
|
1527
|
+
} else {
|
|
1528
|
+
free(ial_str);
|
|
1529
|
+
}
|
|
1530
|
+
} else {
|
|
1531
|
+
cmark_node_set_user_data(table, ial_str);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
apex_free_attributes(ial_attrs);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* Process tables in document
|
|
1540
|
+
*/
|
|
1541
|
+
cmark_node *apex_process_advanced_tables(cmark_node *root) {
|
|
1542
|
+
if (!root) return root;
|
|
1543
|
+
|
|
1544
|
+
cmark_iter *iter = cmark_iter_new(root);
|
|
1545
|
+
cmark_event_type ev_type;
|
|
1546
|
+
cmark_node *cur;
|
|
1547
|
+
|
|
1548
|
+
while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
|
|
1549
|
+
cur = cmark_iter_get_node(iter);
|
|
1550
|
+
|
|
1551
|
+
if (ev_type == CMARK_EVENT_ENTER) {
|
|
1552
|
+
cmark_node_type type = cmark_node_get_type(cur);
|
|
1553
|
+
|
|
1554
|
+
/* Process table */
|
|
1555
|
+
if (type == CMARK_NODE_TABLE) {
|
|
1556
|
+
/* Check for caption before table */
|
|
1557
|
+
/* Skip blank paragraphs to find the actual caption */
|
|
1558
|
+
cmark_node *prev = cmark_node_previous(cur);
|
|
1559
|
+
while (prev) {
|
|
1560
|
+
/* Validate node before accessing - check if it's a valid node pointer */
|
|
1561
|
+
/* Try to get type - if this segfaults, the node is invalid */
|
|
1562
|
+
cmark_node_type prev_type;
|
|
1563
|
+
/* Use a safe access pattern - get type first to validate node */
|
|
1564
|
+
prev_type = cmark_node_get_type(prev);
|
|
1565
|
+
/* Allow all valid node types - custom node types can be > 100 */
|
|
1566
|
+
/* Only reject if type is 0 (which shouldn't happen) */
|
|
1567
|
+
if (prev_type == 0) {
|
|
1568
|
+
/* Invalid node type - likely corrupted pointer */
|
|
1569
|
+
break;
|
|
1570
|
+
}
|
|
1571
|
+
/* Skip blank paragraphs and other non-paragraph nodes */
|
|
1572
|
+
if (prev_type == CMARK_NODE_PARAGRAPH) {
|
|
1573
|
+
/* Check if paragraph is blank (empty or only whitespace) */
|
|
1574
|
+
cmark_node *child = cmark_node_first_child(prev);
|
|
1575
|
+
bool is_blank = true;
|
|
1576
|
+
if (child) {
|
|
1577
|
+
cmark_node_type child_type = cmark_node_get_type(child);
|
|
1578
|
+
if (child_type == CMARK_NODE_TEXT) {
|
|
1579
|
+
const char *text = cmark_node_get_literal(child);
|
|
1580
|
+
if (text) {
|
|
1581
|
+
const char *p = text;
|
|
1582
|
+
while (*p != '\0') {
|
|
1583
|
+
if (!isspace((unsigned char)*p)) {
|
|
1584
|
+
is_blank = false;
|
|
1585
|
+
break;
|
|
1586
|
+
}
|
|
1587
|
+
p++;
|
|
1588
|
+
}
|
|
1589
|
+
} else {
|
|
1590
|
+
/* No text literal - treat as blank */
|
|
1591
|
+
}
|
|
1592
|
+
} else {
|
|
1593
|
+
is_blank = false; /* Has non-text content */
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (!is_blank) {
|
|
1598
|
+
/* Found a non-blank paragraph - check if it's a caption */
|
|
1599
|
+
char *prev_data = (char *)cmark_node_get_user_data(prev);
|
|
1600
|
+
/* Skip paragraphs that have already been used as captions */
|
|
1601
|
+
if (!prev_data || !strstr(prev_data, "data-remove")) {
|
|
1602
|
+
/* Quick check: does this paragraph start with [ or : (caption indicators)? */
|
|
1603
|
+
/* This avoids calling is_table_caption_format on paragraphs that clearly aren't captions */
|
|
1604
|
+
cmark_node *first_text = cmark_node_first_child(prev);
|
|
1605
|
+
bool might_be_caption = false;
|
|
1606
|
+
if (first_text) {
|
|
1607
|
+
cmark_node_type first_text_type = cmark_node_get_type(first_text);
|
|
1608
|
+
if (first_text_type == CMARK_NODE_TEXT) {
|
|
1609
|
+
const char *first_char = cmark_node_get_literal(first_text);
|
|
1610
|
+
if (first_char && strlen(first_char) > 0) {
|
|
1611
|
+
size_t first_char_len = strlen(first_char);
|
|
1612
|
+
/* Skip leading whitespace (up to 3 spaces) */
|
|
1613
|
+
int check_spaces = 0;
|
|
1614
|
+
while (check_spaces < 3 && check_spaces < (int)first_char_len && first_char[check_spaces] == ' ') {
|
|
1615
|
+
check_spaces++;
|
|
1616
|
+
}
|
|
1617
|
+
if (check_spaces < (int)first_char_len &&
|
|
1618
|
+
(first_char[check_spaces] == '[' || first_char[check_spaces] == ':')) {
|
|
1619
|
+
might_be_caption = true;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
if (might_be_caption) {
|
|
1626
|
+
char *caption = NULL;
|
|
1627
|
+
const char *original_text = NULL;
|
|
1628
|
+
/* We're already looking backwards from a table, so we know this paragraph
|
|
1629
|
+
* is before the table. We can check if it's a caption format without
|
|
1630
|
+
* needing to verify adjacency (which might fail if there are headers
|
|
1631
|
+
* or other nodes between the caption and table). */
|
|
1632
|
+
if (is_table_caption_format(prev, &caption, &original_text)) {
|
|
1633
|
+
add_table_caption(cur, caption, original_text);
|
|
1634
|
+
/* Free the allocated full_text that was stored in user_data */
|
|
1635
|
+
char *stored_text = (char *)cmark_node_get_user_data(prev);
|
|
1636
|
+
if (stored_text && stored_text == original_text) {
|
|
1637
|
+
free(stored_text);
|
|
1638
|
+
}
|
|
1639
|
+
/* Mark caption paragraph for removal so it is not reused */
|
|
1640
|
+
cmark_node_set_user_data(prev, strdup(" data-remove=\"true\""));
|
|
1641
|
+
free(caption);
|
|
1642
|
+
break; /* Found caption, stop looking */
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
/* Found non-blank paragraph that's not a caption - continue looking
|
|
1647
|
+
* backwards in case there's a caption further up (e.g., after a header
|
|
1648
|
+
* and regular paragraph) */
|
|
1649
|
+
prev = cmark_node_previous(prev);
|
|
1650
|
+
/* Continue loop to check the next node (prev is checked at start of while) */
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
/* This paragraph is blank, continue to previous sibling */
|
|
1654
|
+
prev = cmark_node_previous(prev);
|
|
1655
|
+
/* Continue loop to check the next node (prev is checked at start of while) */
|
|
1656
|
+
continue;
|
|
1657
|
+
} else {
|
|
1658
|
+
/* Not a paragraph - could be a header or other block element */
|
|
1659
|
+
/* Headers can appear between caption and table, so continue looking */
|
|
1660
|
+
/* But stop for other block types that shouldn't have captions after them */
|
|
1661
|
+
if (prev_type == CMARK_NODE_HEADING) {
|
|
1662
|
+
/* Header found - continue looking backwards for caption */
|
|
1663
|
+
prev = cmark_node_previous(prev);
|
|
1664
|
+
continue;
|
|
1665
|
+
} else {
|
|
1666
|
+
/* Other block type - stop looking */
|
|
1667
|
+
break;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/* Check for caption after table */
|
|
1673
|
+
cmark_node *next = cmark_node_next(cur);
|
|
1674
|
+
if (next) {
|
|
1675
|
+
cmark_node_type next_type = cmark_node_get_type(next);
|
|
1676
|
+
char *next_data = (char *)cmark_node_get_user_data(next);
|
|
1677
|
+
/* Skip paragraphs that have already been used as captions */
|
|
1678
|
+
if (!next_data || !strstr(next_data, "data-remove")) {
|
|
1679
|
+
char *caption = NULL;
|
|
1680
|
+
const char *original_text = NULL;
|
|
1681
|
+
if (is_table_caption(next, &caption, &original_text)) {
|
|
1682
|
+
add_table_caption(cur, caption, original_text);
|
|
1683
|
+
/* Free the allocated full_text that was stored in user_data */
|
|
1684
|
+
char *stored_text = (char *)cmark_node_get_user_data(next);
|
|
1685
|
+
if (stored_text && stored_text == original_text) {
|
|
1686
|
+
free(stored_text);
|
|
1687
|
+
}
|
|
1688
|
+
/* Mark caption paragraph for removal so it is not reused */
|
|
1689
|
+
cmark_node_set_user_data(next, strdup(" data-remove=\"true\""));
|
|
1690
|
+
free(caption);
|
|
1691
|
+
} else {
|
|
1692
|
+
/* Also check if it's a : Caption format (which is_table_caption might not handle) */
|
|
1693
|
+
if (next_type == CMARK_NODE_PARAGRAPH) {
|
|
1694
|
+
if (is_table_caption_format(next, &caption, &original_text)) {
|
|
1695
|
+
add_table_caption(cur, caption, original_text);
|
|
1696
|
+
char *stored_text = (char *)cmark_node_get_user_data(next);
|
|
1697
|
+
if (stored_text && stored_text == original_text) {
|
|
1698
|
+
/* stored_text is the full_text allocated by is_table_caption_format */
|
|
1699
|
+
free(stored_text);
|
|
1700
|
+
}
|
|
1701
|
+
/* Mark caption paragraph for removal so it is not reused */
|
|
1702
|
+
cmark_node_set_user_data(next, strdup(" data-remove=\"true\""));
|
|
1703
|
+
free(caption);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/* Check for caption rows BEFORE processing spans */
|
|
1711
|
+
/* Caption rows should be detected and removed before colspan processing */
|
|
1712
|
+
cmark_node *row_check = cmark_node_first_child(cur);
|
|
1713
|
+
cmark_node *caption_row = NULL;
|
|
1714
|
+
while (row_check) {
|
|
1715
|
+
if (cmark_node_get_type(row_check) == CMARK_NODE_TABLE_ROW && is_caption_row(row_check)) {
|
|
1716
|
+
caption_row = row_check;
|
|
1717
|
+
/* Continue to find the last caption row if there are multiple */
|
|
1718
|
+
}
|
|
1719
|
+
row_check = cmark_node_next(row_check);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/* If we found a caption row, extract the caption and mark row for removal */
|
|
1723
|
+
if (caption_row) {
|
|
1724
|
+
cmark_node *cell = cmark_node_first_child(caption_row);
|
|
1725
|
+
if (cell) {
|
|
1726
|
+
/* Find the cell with caption text (might be first cell, might span) */
|
|
1727
|
+
while (cell) {
|
|
1728
|
+
if (cmark_node_get_type(cell) == CMARK_NODE_TABLE_CELL) {
|
|
1729
|
+
cmark_node *text_node = cmark_node_first_child(cell);
|
|
1730
|
+
if (text_node && cmark_node_get_type(text_node) == CMARK_NODE_TEXT) {
|
|
1731
|
+
const char *text = cmark_node_get_literal(text_node);
|
|
1732
|
+
if (text && text[0] == '[') {
|
|
1733
|
+
const char *end = strchr(text + 1, ']');
|
|
1734
|
+
if (end) {
|
|
1735
|
+
size_t caption_len = end - text - 1;
|
|
1736
|
+
char *caption = malloc(caption_len + 1);
|
|
1737
|
+
if (caption) {
|
|
1738
|
+
memcpy(caption, text + 1, caption_len);
|
|
1739
|
+
caption[caption_len] = '\0';
|
|
1740
|
+
add_table_caption(cur, caption, text);
|
|
1741
|
+
free(caption);
|
|
1742
|
+
/* Mark the entire row for removal */
|
|
1743
|
+
char *existing = (char *)cmark_node_get_user_data(caption_row);
|
|
1744
|
+
if (existing) free(existing);
|
|
1745
|
+
cmark_node_set_user_data(caption_row, strdup(" data-remove=\"true\""));
|
|
1746
|
+
break; /* Found caption, done */
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
cell = cmark_node_next(cell);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
/* Process spans - this also detects tfoot rows */
|
|
1758
|
+
process_table_spans(cur);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
cmark_iter_free(iter);
|
|
1764
|
+
return root;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/**
|
|
1768
|
+
* Custom HTML renderer for tables with spans and captions
|
|
1769
|
+
*/
|
|
1770
|
+
__attribute__((unused))
|
|
1771
|
+
static void html_render_table(cmark_syntax_extension *ext,
|
|
1772
|
+
struct cmark_html_renderer *renderer,
|
|
1773
|
+
cmark_node *node,
|
|
1774
|
+
cmark_event_type ev_type,
|
|
1775
|
+
int options) {
|
|
1776
|
+
(void)ext;
|
|
1777
|
+
(void)options;
|
|
1778
|
+
cmark_strbuf *html = renderer->html;
|
|
1779
|
+
cmark_node_type type = cmark_node_get_type(node);
|
|
1780
|
+
|
|
1781
|
+
if (ev_type == CMARK_EVENT_ENTER && type == CMARK_NODE_TABLE) {
|
|
1782
|
+
/* Check for caption */
|
|
1783
|
+
char *user_data = (char *)cmark_node_get_user_data(node);
|
|
1784
|
+
if (user_data && strstr(user_data, "data-caption=")) {
|
|
1785
|
+
char caption[512];
|
|
1786
|
+
if (sscanf(user_data, " data-caption=\"%[^\"]\"", caption) == 1) {
|
|
1787
|
+
cmark_strbuf_puts(html, "<figure class=\"table-figure\">\n");
|
|
1788
|
+
cmark_strbuf_puts(html, "<figcaption>");
|
|
1789
|
+
cmark_strbuf_puts(html, caption);
|
|
1790
|
+
cmark_strbuf_puts(html, "</figcaption>\n");
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
/* Let default renderer handle the table tag */
|
|
1794
|
+
return;
|
|
1795
|
+
} else if (ev_type == CMARK_EVENT_EXIT && type == CMARK_NODE_TABLE) {
|
|
1796
|
+
char *user_data = (char *)cmark_node_get_user_data(node);
|
|
1797
|
+
if (user_data && strstr(user_data, "data-caption=")) {
|
|
1798
|
+
cmark_strbuf_puts(html, "</figure>\n");
|
|
1799
|
+
}
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/* Handle ALL table cells to properly handle removal and spans */
|
|
1804
|
+
if (type == CMARK_NODE_TABLE_CELL) {
|
|
1805
|
+
char *attrs = (char *)cmark_node_get_user_data(node);
|
|
1806
|
+
|
|
1807
|
+
/* Skip cells marked for removal entirely (don't render enter or exit) */
|
|
1808
|
+
if (attrs && strstr(attrs, "data-remove")) {
|
|
1809
|
+
return; /* Don't render this cell at all */
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/* If this cell has rowspan/colspan, we need custom rendering */
|
|
1813
|
+
if (ev_type == CMARK_EVENT_ENTER && attrs &&
|
|
1814
|
+
(strstr(attrs, "colspan=") || strstr(attrs, "rowspan="))) {
|
|
1815
|
+
/* Determine if header or data cell by checking parent row */
|
|
1816
|
+
bool is_header = false;
|
|
1817
|
+
cmark_node *row = cmark_node_parent(node);
|
|
1818
|
+
if (row) {
|
|
1819
|
+
cmark_node *parent = cmark_node_parent(row);
|
|
1820
|
+
if (parent && cmark_node_get_type(parent) == CMARK_NODE_TABLE) {
|
|
1821
|
+
/* First row is header in cmark-gfm tables */
|
|
1822
|
+
cmark_node *first_row = cmark_node_first_child(parent);
|
|
1823
|
+
is_header = (first_row == row);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/* Output opening tag */
|
|
1828
|
+
if (is_header) {
|
|
1829
|
+
cmark_strbuf_puts(html, "<th");
|
|
1830
|
+
} else {
|
|
1831
|
+
cmark_strbuf_puts(html, "<td");
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/* Add rowspan/colspan attributes */
|
|
1835
|
+
cmark_strbuf_puts(html, attrs);
|
|
1836
|
+
|
|
1837
|
+
cmark_strbuf_putc(html, '>');
|
|
1838
|
+
return; /* We've handled the opening tag */
|
|
1839
|
+
} else if (ev_type == CMARK_EVENT_EXIT && attrs &&
|
|
1840
|
+
(strstr(attrs, "colspan=") || strstr(attrs, "rowspan="))) {
|
|
1841
|
+
/* Closing tag */
|
|
1842
|
+
bool is_header = false;
|
|
1843
|
+
cmark_node *row = cmark_node_parent(node);
|
|
1844
|
+
if (row) {
|
|
1845
|
+
cmark_node *parent = cmark_node_parent(row);
|
|
1846
|
+
if (parent && cmark_node_get_type(parent) == CMARK_NODE_TABLE) {
|
|
1847
|
+
cmark_node *first_row = cmark_node_first_child(parent);
|
|
1848
|
+
is_header = (first_row == row);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
if (is_header) {
|
|
1853
|
+
cmark_strbuf_puts(html, "</th>\n");
|
|
1854
|
+
} else {
|
|
1855
|
+
cmark_strbuf_puts(html, "</td>\n");
|
|
1856
|
+
}
|
|
1857
|
+
return; /* We've handled the closing tag */
|
|
1858
|
+
}
|
|
1859
|
+
/* For normal cells without spans, let default renderer handle them */
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/**
|
|
1864
|
+
* Postprocess function
|
|
1865
|
+
*/
|
|
1866
|
+
static cmark_node *postprocess(cmark_syntax_extension *ext,
|
|
1867
|
+
cmark_parser *parser,
|
|
1868
|
+
cmark_node *root) {
|
|
1869
|
+
(void)ext;
|
|
1870
|
+
(void)parser;
|
|
1871
|
+
return apex_process_advanced_tables(root);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
/**
|
|
1875
|
+
* Create advanced tables extension
|
|
1876
|
+
*/
|
|
1877
|
+
cmark_syntax_extension *create_advanced_tables_extension(bool per_cell_alignment) {
|
|
1878
|
+
cmark_syntax_extension *ext = cmark_syntax_extension_new("advanced_tables");
|
|
1879
|
+
if (!ext) return NULL;
|
|
1880
|
+
|
|
1881
|
+
/* Store per_cell_alignment flag in static variable */
|
|
1882
|
+
g_per_cell_alignment = per_cell_alignment;
|
|
1883
|
+
|
|
1884
|
+
/* Set postprocess callback to add span/caption attributes to AST */
|
|
1885
|
+
cmark_syntax_extension_set_postprocess_func(ext, postprocess);
|
|
1886
|
+
|
|
1887
|
+
/* NOTE: We don't use html_render_func here because it conflicts with GFM table renderer.
|
|
1888
|
+
* Instead, we do HTML postprocessing in apex.c after rendering.
|
|
1889
|
+
* The post-processing should remove cells with data-remove and inject colspan/rowspan attributes. */
|
|
1890
|
+
/* cmark_syntax_extension_set_html_render_func(ext, html_render_table); */
|
|
1891
|
+
|
|
1892
|
+
/* Register to handle table and table cell rendering */
|
|
1893
|
+
cmark_syntax_extension_set_can_contain_func(ext, NULL);
|
|
1894
|
+
|
|
1895
|
+
return ext;
|
|
1896
|
+
}
|
|
1897
|
+
|