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,4084 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kramdown IAL (Inline Attribute Lists) Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
#include "ial.h"
|
|
6
|
+
#include "table.h" /* For CMARK_NODE_TABLE */
|
|
7
|
+
#include "apex/apex.h" /* For apex_mode_t */
|
|
8
|
+
#include <string.h>
|
|
9
|
+
#include <stdlib.h>
|
|
10
|
+
#include <ctype.h>
|
|
11
|
+
#include <stdio.h>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Free attributes structure
|
|
15
|
+
*/
|
|
16
|
+
void apex_free_attributes(apex_attributes *attrs) {
|
|
17
|
+
if (!attrs) return;
|
|
18
|
+
|
|
19
|
+
free(attrs->id);
|
|
20
|
+
|
|
21
|
+
for (int i = 0; i < attrs->class_count; i++) {
|
|
22
|
+
free(attrs->classes[i]);
|
|
23
|
+
}
|
|
24
|
+
free(attrs->classes);
|
|
25
|
+
|
|
26
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
27
|
+
free(attrs->keys[i]);
|
|
28
|
+
free(attrs->values[i]);
|
|
29
|
+
}
|
|
30
|
+
free(attrs->keys);
|
|
31
|
+
free(attrs->values);
|
|
32
|
+
|
|
33
|
+
free(attrs);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Free ALD list
|
|
38
|
+
*/
|
|
39
|
+
void apex_free_alds(ald_entry *alds) {
|
|
40
|
+
while (alds) {
|
|
41
|
+
ald_entry *next = alds->next;
|
|
42
|
+
free(alds->name);
|
|
43
|
+
apex_free_attributes(alds->attrs);
|
|
44
|
+
free(alds);
|
|
45
|
+
alds = next;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create empty attributes structure
|
|
51
|
+
*/
|
|
52
|
+
static apex_attributes *create_attributes(void) {
|
|
53
|
+
apex_attributes *attrs = calloc(1, sizeof(apex_attributes));
|
|
54
|
+
return attrs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add class to attributes
|
|
59
|
+
*/
|
|
60
|
+
static void add_class(apex_attributes *attrs, const char *class_name) {
|
|
61
|
+
if (!attrs || !class_name) return;
|
|
62
|
+
|
|
63
|
+
attrs->classes = realloc(attrs->classes, sizeof(char*) * (attrs->class_count + 1));
|
|
64
|
+
attrs->classes[attrs->class_count++] = strdup(class_name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Add key-value attribute
|
|
69
|
+
*/
|
|
70
|
+
static void add_attribute(apex_attributes *attrs, const char *key, const char *value) {
|
|
71
|
+
if (!attrs || !key) return;
|
|
72
|
+
|
|
73
|
+
attrs->keys = realloc(attrs->keys, sizeof(char*) * (attrs->attr_count + 1));
|
|
74
|
+
attrs->values = realloc(attrs->values, sizeof(char*) * (attrs->attr_count + 1));
|
|
75
|
+
attrs->keys[attrs->attr_count] = strdup(key);
|
|
76
|
+
attrs->values[attrs->attr_count] = value ? strdup(value) : strdup("");
|
|
77
|
+
attrs->attr_count++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Parse IAL/ALD content
|
|
82
|
+
* Format: #id .class .class2 key="value" key2='value2'
|
|
83
|
+
*/
|
|
84
|
+
apex_attributes *parse_ial_content(const char *content, int len) {
|
|
85
|
+
apex_attributes *attrs = create_attributes();
|
|
86
|
+
if (!attrs) return NULL;
|
|
87
|
+
|
|
88
|
+
char buffer[2048];
|
|
89
|
+
if (len >= (int)sizeof(buffer)) len = (int)sizeof(buffer) - 1;
|
|
90
|
+
memcpy(buffer, content, len);
|
|
91
|
+
buffer[len] = '\0';
|
|
92
|
+
|
|
93
|
+
char *p = buffer;
|
|
94
|
+
while (*p) {
|
|
95
|
+
/* Skip whitespace */
|
|
96
|
+
while (isspace((unsigned char)*p)) p++;
|
|
97
|
+
if (!*p) break;
|
|
98
|
+
|
|
99
|
+
/* Check for ID (#id) */
|
|
100
|
+
if (*p == '#') {
|
|
101
|
+
p++;
|
|
102
|
+
char *id_start = p;
|
|
103
|
+
while (*p && !isspace((unsigned char)*p) && *p != '.' && *p != '}') p++;
|
|
104
|
+
if (p > id_start) {
|
|
105
|
+
char saved = *p;
|
|
106
|
+
*p = '\0';
|
|
107
|
+
if (attrs->id) free(attrs->id);
|
|
108
|
+
attrs->id = strdup(id_start);
|
|
109
|
+
*p = saved;
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Check for class (.class) */
|
|
115
|
+
if (*p == '.') {
|
|
116
|
+
p++;
|
|
117
|
+
char *class_start = p;
|
|
118
|
+
while (*p && !isspace((unsigned char)*p) && *p != '.' && *p != '#' && *p != '}') p++;
|
|
119
|
+
if (p > class_start) {
|
|
120
|
+
char saved = *p;
|
|
121
|
+
*p = '\0';
|
|
122
|
+
add_class(attrs, class_start);
|
|
123
|
+
*p = saved;
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Check for key="value" or key='value' */
|
|
129
|
+
char *key_start = p;
|
|
130
|
+
while (*p && *p != '=' && *p != ' ' && *p != '\t' && *p != '}') p++;
|
|
131
|
+
|
|
132
|
+
if (*p == '=') {
|
|
133
|
+
/* Found key=value */
|
|
134
|
+
char saved = *p;
|
|
135
|
+
*p = '\0';
|
|
136
|
+
char *key = strdup(key_start);
|
|
137
|
+
*p = saved;
|
|
138
|
+
p++; /* Skip = */
|
|
139
|
+
|
|
140
|
+
/* Parse value (could be quoted - handle both straight and curly quotes) */
|
|
141
|
+
char *value = NULL;
|
|
142
|
+
bool is_curly_left = ((unsigned char)*p == 0xE2 && (unsigned char)p[1] == 0x80 && (unsigned char)p[2] == 0x9C); /* " */
|
|
143
|
+
bool is_curly_right = ((unsigned char)*p == 0xE2 && (unsigned char)p[1] == 0x80 && (unsigned char)p[2] == 0x9D); /* " */
|
|
144
|
+
bool is_straight_quote = (*p == '"' || *p == '\'');
|
|
145
|
+
|
|
146
|
+
if (is_straight_quote || is_curly_left || is_curly_right) {
|
|
147
|
+
bool is_curly = (is_curly_left || is_curly_right);
|
|
148
|
+
char *value_start;
|
|
149
|
+
|
|
150
|
+
if (is_curly) {
|
|
151
|
+
/* Skip UTF-8 curly quote (3 bytes) */
|
|
152
|
+
p += 3;
|
|
153
|
+
value_start = p;
|
|
154
|
+
|
|
155
|
+
/* Find closing curly quote (either left or right) */
|
|
156
|
+
char *value_end = p;
|
|
157
|
+
while (*value_end) {
|
|
158
|
+
if ((unsigned char)*value_end == 0xE2 && (unsigned char)value_end[1] == 0x80 &&
|
|
159
|
+
((unsigned char)value_end[2] == 0x9C || (unsigned char)value_end[2] == 0x9D)) {
|
|
160
|
+
break; /* Found closing curly quote */
|
|
161
|
+
}
|
|
162
|
+
value_end++;
|
|
163
|
+
}
|
|
164
|
+
if ((unsigned char)*value_end == 0xE2) {
|
|
165
|
+
/* Extract value (content between curly quotes, excluding quotes) */
|
|
166
|
+
size_t value_len = value_end - value_start;
|
|
167
|
+
value = malloc(value_len + 1);
|
|
168
|
+
if (value) {
|
|
169
|
+
memcpy(value, value_start, value_len);
|
|
170
|
+
value[value_len] = '\0';
|
|
171
|
+
}
|
|
172
|
+
p = value_end + 3; /* Skip closing curly quote */
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
/* Straight quote */
|
|
176
|
+
char quote = *p++;
|
|
177
|
+
value_start = p;
|
|
178
|
+
while (*p && *p != quote) {
|
|
179
|
+
if (*p == '\\' && *(p+1)) p++; /* Skip escaped char */
|
|
180
|
+
p++;
|
|
181
|
+
}
|
|
182
|
+
if (*p == quote) {
|
|
183
|
+
*p = '\0';
|
|
184
|
+
value = strdup(value_start);
|
|
185
|
+
*p = quote;
|
|
186
|
+
p++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
/* Unquoted value */
|
|
191
|
+
char *value_start = p;
|
|
192
|
+
while (*p && !isspace((unsigned char)*p) && *p != '}') p++;
|
|
193
|
+
char saved_val = *p;
|
|
194
|
+
*p = '\0';
|
|
195
|
+
value = strdup(value_start);
|
|
196
|
+
*p = saved_val;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
add_attribute(attrs, key, value);
|
|
200
|
+
free(key);
|
|
201
|
+
free(value);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Check for bare @2x/@3x (retina srcset markers) */
|
|
206
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
207
|
+
key_start[0] == '@' && key_start[1] == '2' && key_start[2] == 'x') {
|
|
208
|
+
add_attribute(attrs, "data-srcset-2x", "1");
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
212
|
+
key_start[0] == '@' && key_start[1] == '3' && key_start[2] == 'x') {
|
|
213
|
+
add_attribute(attrs, "data-srcset-3x", "1");
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* Unknown token, skip */
|
|
218
|
+
p++;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return attrs;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if line is an ALD
|
|
226
|
+
* Pattern: {:ref-name: attributes}
|
|
227
|
+
*/
|
|
228
|
+
static bool is_ald_line(const char *line, char **ref_name, apex_attributes **attrs) {
|
|
229
|
+
const char *p = line;
|
|
230
|
+
|
|
231
|
+
/* Skip leading whitespace */
|
|
232
|
+
while (isspace((unsigned char)*p)) p++;
|
|
233
|
+
|
|
234
|
+
/* Leanpub index syntax {i: term} is not an ALD */
|
|
235
|
+
if (p[0] == '{' && p[1] == 'i' && p[2] == ':') return false;
|
|
236
|
+
|
|
237
|
+
/* Check for {: */
|
|
238
|
+
if (p[0] != '{' || p[1] != ':') return false;
|
|
239
|
+
p += 2;
|
|
240
|
+
|
|
241
|
+
/* Extract reference name */
|
|
242
|
+
const char *name_start = p;
|
|
243
|
+
while (*p && *p != ':' && *p != '}') p++;
|
|
244
|
+
|
|
245
|
+
if (*p != ':') return false; /* Not an ALD, maybe regular IAL */
|
|
246
|
+
|
|
247
|
+
/* Found ALD */
|
|
248
|
+
int name_len = p - name_start;
|
|
249
|
+
if (name_len <= 0) return false;
|
|
250
|
+
|
|
251
|
+
*ref_name = malloc(name_len + 1);
|
|
252
|
+
memcpy(*ref_name, name_start, name_len);
|
|
253
|
+
(*ref_name)[name_len] = '\0';
|
|
254
|
+
|
|
255
|
+
p++; /* Skip second : */
|
|
256
|
+
|
|
257
|
+
/* Find closing } */
|
|
258
|
+
const char *content_start = p;
|
|
259
|
+
const char *close = strchr(p, '}');
|
|
260
|
+
if (!close) {
|
|
261
|
+
free(*ref_name);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Parse attributes */
|
|
266
|
+
*attrs = parse_ial_content(content_start, close - content_start);
|
|
267
|
+
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Extract ALDs from text
|
|
273
|
+
*/
|
|
274
|
+
ald_entry *apex_extract_alds(char **text_ptr) {
|
|
275
|
+
if (!text_ptr || !*text_ptr) return NULL;
|
|
276
|
+
|
|
277
|
+
char *text = *text_ptr;
|
|
278
|
+
ald_entry *alds = NULL;
|
|
279
|
+
ald_entry **tail = &alds;
|
|
280
|
+
|
|
281
|
+
char *line_start = text;
|
|
282
|
+
char *line_end;
|
|
283
|
+
|
|
284
|
+
char *output = malloc(strlen(text) + 1);
|
|
285
|
+
char *output_write = output;
|
|
286
|
+
|
|
287
|
+
if (!output) return NULL;
|
|
288
|
+
|
|
289
|
+
while ((line_end = strchr(line_start, '\n')) != NULL || *line_start) {
|
|
290
|
+
if (!line_end) line_end = line_start + strlen(line_start);
|
|
291
|
+
|
|
292
|
+
size_t line_len = line_end - line_start;
|
|
293
|
+
char line[2048];
|
|
294
|
+
if (line_len >= sizeof(line)) line_len = sizeof(line) - 1;
|
|
295
|
+
memcpy(line, line_start, line_len);
|
|
296
|
+
line[line_len] = '\0';
|
|
297
|
+
|
|
298
|
+
/* Check if this is an ALD */
|
|
299
|
+
char *ref_name = NULL;
|
|
300
|
+
apex_attributes *attrs = NULL;
|
|
301
|
+
|
|
302
|
+
if (is_ald_line(line, &ref_name, &attrs)) {
|
|
303
|
+
/* Found ALD - store it */
|
|
304
|
+
ald_entry *entry = malloc(sizeof(ald_entry));
|
|
305
|
+
if (entry) {
|
|
306
|
+
entry->name = ref_name;
|
|
307
|
+
entry->attrs = attrs;
|
|
308
|
+
entry->next = NULL;
|
|
309
|
+
|
|
310
|
+
*tail = entry;
|
|
311
|
+
tail = &entry->next;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* Skip this line in output */
|
|
315
|
+
line_start = *line_end ? line_end + 1 : line_end;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/* Not an ALD, copy line to output */
|
|
320
|
+
memcpy(output_write, line_start, line_len);
|
|
321
|
+
output_write += line_len;
|
|
322
|
+
if (*line_end) {
|
|
323
|
+
*output_write++ = '\n';
|
|
324
|
+
line_start = line_end + 1;
|
|
325
|
+
} else {
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
*output_write = '\0';
|
|
331
|
+
|
|
332
|
+
/* Use the output buffer as the new text */
|
|
333
|
+
size_t output_len = strlen(output);
|
|
334
|
+
if (output_len <= strlen(*text_ptr)) {
|
|
335
|
+
strcpy(*text_ptr, output);
|
|
336
|
+
} else {
|
|
337
|
+
/* Output is larger, need to reallocate */
|
|
338
|
+
free(*text_ptr);
|
|
339
|
+
*text_ptr = strdup(output);
|
|
340
|
+
}
|
|
341
|
+
free(output);
|
|
342
|
+
|
|
343
|
+
return alds;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Find ALD by name
|
|
348
|
+
*/
|
|
349
|
+
static apex_attributes *find_ald(ald_entry *alds, const char *name) {
|
|
350
|
+
for (ald_entry *entry = alds; entry; entry = entry->next) {
|
|
351
|
+
if (strcmp(entry->name, name) == 0) {
|
|
352
|
+
return entry->attrs;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return NULL;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Check if an attribute key already exists in the attributes structure
|
|
360
|
+
* Returns the index if found, or -1 if not found
|
|
361
|
+
*/
|
|
362
|
+
static int find_attribute_index(apex_attributes *attrs, const char *key) {
|
|
363
|
+
if (!attrs || !key) return -1;
|
|
364
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
365
|
+
if (attrs->keys[i] && strcmp(attrs->keys[i], key) == 0) {
|
|
366
|
+
return i;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return -1;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Merge attributes (for ALD references)
|
|
374
|
+
* Base attributes are copied first, then override attributes are applied.
|
|
375
|
+
* Override attributes replace base attributes with the same key/ID.
|
|
376
|
+
* Classes are appended (duplicates allowed, HTML will handle them).
|
|
377
|
+
*/
|
|
378
|
+
static apex_attributes *merge_attributes(apex_attributes *base, apex_attributes *override) {
|
|
379
|
+
apex_attributes *merged = create_attributes();
|
|
380
|
+
if (!merged) return base;
|
|
381
|
+
|
|
382
|
+
/* Copy base attributes */
|
|
383
|
+
if (base) {
|
|
384
|
+
if (base->id) merged->id = strdup(base->id);
|
|
385
|
+
for (int i = 0; i < base->class_count; i++) {
|
|
386
|
+
add_class(merged, base->classes[i]);
|
|
387
|
+
}
|
|
388
|
+
for (int i = 0; i < base->attr_count; i++) {
|
|
389
|
+
add_attribute(merged, base->keys[i], base->values[i]);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* Override with new attributes */
|
|
394
|
+
if (override) {
|
|
395
|
+
/* Override ID if present */
|
|
396
|
+
if (override->id) {
|
|
397
|
+
free(merged->id);
|
|
398
|
+
merged->id = strdup(override->id);
|
|
399
|
+
}
|
|
400
|
+
/* Append classes (allow duplicates) */
|
|
401
|
+
for (int i = 0; i < override->class_count; i++) {
|
|
402
|
+
add_class(merged, override->classes[i]);
|
|
403
|
+
}
|
|
404
|
+
/* Override key-value attributes (replace if key exists, otherwise add) */
|
|
405
|
+
for (int i = 0; i < override->attr_count; i++) {
|
|
406
|
+
int existing_idx = find_attribute_index(merged, override->keys[i]);
|
|
407
|
+
if (existing_idx >= 0) {
|
|
408
|
+
/* Replace existing attribute */
|
|
409
|
+
free(merged->values[existing_idx]);
|
|
410
|
+
merged->values[existing_idx] = strdup(override->values[i]);
|
|
411
|
+
} else {
|
|
412
|
+
/* Add new attribute */
|
|
413
|
+
add_attribute(merged, override->keys[i], override->values[i]);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return merged;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Check if text ends with IAL pattern
|
|
423
|
+
* Pattern: {: attributes} or {:.class} or {: ref-name} or {: ref-name .class #id}
|
|
424
|
+
*/
|
|
425
|
+
static bool extract_ial_from_text(const char *text, apex_attributes **attrs_out, ald_entry *alds) {
|
|
426
|
+
if (!text) return false;
|
|
427
|
+
|
|
428
|
+
/* Find { from the end - support both {: ...} and {#id .class} formats */
|
|
429
|
+
const char *ial_start = strrchr(text, '{');
|
|
430
|
+
if (!ial_start) return false;
|
|
431
|
+
|
|
432
|
+
/* Check if it's a valid IAL format: {: or {# or {. */
|
|
433
|
+
char second_char = ial_start[1];
|
|
434
|
+
/* Leanpub index syntax {i: term} is not IAL */
|
|
435
|
+
if (second_char == 'i' && ial_start[2] == ':') return false;
|
|
436
|
+
if (second_char != ':' && second_char != '#' && second_char != '.') return false;
|
|
437
|
+
|
|
438
|
+
/* Find closing } */
|
|
439
|
+
const char *ial_end = strchr(ial_start, '}');
|
|
440
|
+
if (!ial_end) return false;
|
|
441
|
+
|
|
442
|
+
/* Check if this is at the end (only whitespace after) */
|
|
443
|
+
const char *p_check = ial_end + 1;
|
|
444
|
+
while (*p_check && isspace((unsigned char)*p_check)) p_check++;
|
|
445
|
+
if (*p_check) return false; /* Not at end */
|
|
446
|
+
|
|
447
|
+
/* Parse IAL content */
|
|
448
|
+
/* For {: format, skip {: (2 chars); for {# or {. format, skip { (1 char) */
|
|
449
|
+
const char *content_start = (second_char == ':') ? ial_start + 2 : ial_start + 1;
|
|
450
|
+
int content_len = ial_end - content_start;
|
|
451
|
+
|
|
452
|
+
if (content_len <= 0) {
|
|
453
|
+
*attrs_out = NULL;
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Copy content to work with */
|
|
458
|
+
char buffer[2048];
|
|
459
|
+
if (content_len >= (int)sizeof(buffer)) content_len = (int)sizeof(buffer) - 1;
|
|
460
|
+
memcpy(buffer, content_start, content_len);
|
|
461
|
+
buffer[content_len] = '\0';
|
|
462
|
+
|
|
463
|
+
char *p = buffer;
|
|
464
|
+
|
|
465
|
+
/* Skip leading whitespace */
|
|
466
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
467
|
+
if (!*p) {
|
|
468
|
+
*attrs_out = NULL;
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* Extract first token */
|
|
473
|
+
char *token_start = p;
|
|
474
|
+
while (*p && !isspace((unsigned char)*p) && *p != '#' && *p != '.' && *p != '=') p++;
|
|
475
|
+
|
|
476
|
+
apex_attributes *ald_attrs = NULL;
|
|
477
|
+
const char *remaining_content = NULL;
|
|
478
|
+
int remaining_len = 0;
|
|
479
|
+
|
|
480
|
+
if (p > token_start) {
|
|
481
|
+
/* Check if first token is a simple word (ALD reference) */
|
|
482
|
+
char saved = *p;
|
|
483
|
+
*p = '\0';
|
|
484
|
+
|
|
485
|
+
bool is_ref = true;
|
|
486
|
+
for (char *c = token_start; *c; c++) {
|
|
487
|
+
if (*c == '#' || *c == '.' || *c == '=') {
|
|
488
|
+
is_ref = false;
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
*p = saved; /* Restore */
|
|
494
|
+
|
|
495
|
+
if (is_ref && *token_start) {
|
|
496
|
+
/* Trim the token */
|
|
497
|
+
char *trimmed = token_start;
|
|
498
|
+
while (isspace((unsigned char)*trimmed)) trimmed++;
|
|
499
|
+
char *end = trimmed + strlen(trimmed) - 1;
|
|
500
|
+
while (end > trimmed && isspace((unsigned char)*end)) *end-- = '\0';
|
|
501
|
+
|
|
502
|
+
/* Look up ALD */
|
|
503
|
+
if (*trimmed) {
|
|
504
|
+
ald_attrs = find_ald(alds, trimmed);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* If we found an ALD, check for remaining content */
|
|
510
|
+
if (ald_attrs) {
|
|
511
|
+
/* Skip whitespace after token */
|
|
512
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
513
|
+
|
|
514
|
+
if (*p) {
|
|
515
|
+
/* There's remaining content - parse it as additional attributes */
|
|
516
|
+
remaining_content = content_start + (p - buffer);
|
|
517
|
+
remaining_len = content_len - (p - buffer);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* Parse additional attributes if any */
|
|
522
|
+
apex_attributes *additional_attrs = NULL;
|
|
523
|
+
if (remaining_content && remaining_len > 0) {
|
|
524
|
+
additional_attrs = parse_ial_content(remaining_content, remaining_len);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* Merge ALD with additional attributes */
|
|
528
|
+
if (ald_attrs) {
|
|
529
|
+
*attrs_out = merge_attributes(ald_attrs, additional_attrs);
|
|
530
|
+
if (additional_attrs) {
|
|
531
|
+
apex_free_attributes(additional_attrs);
|
|
532
|
+
}
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* No ALD found, parse as regular IAL */
|
|
537
|
+
*attrs_out = parse_ial_content(content_start, content_len);
|
|
538
|
+
return *attrs_out != NULL;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Apply attributes to HTML tag
|
|
543
|
+
* Helper function to generate attribute string
|
|
544
|
+
*/
|
|
545
|
+
char *attributes_to_html(apex_attributes *attrs) {
|
|
546
|
+
if (!attrs) return strdup("");
|
|
547
|
+
|
|
548
|
+
char buffer[4096];
|
|
549
|
+
char *p = buffer;
|
|
550
|
+
size_t remaining = sizeof(buffer);
|
|
551
|
+
|
|
552
|
+
#define APPEND(str) do { \
|
|
553
|
+
size_t len = strlen(str); \
|
|
554
|
+
if (len < remaining) { \
|
|
555
|
+
memcpy(p, str, len); \
|
|
556
|
+
p += len; \
|
|
557
|
+
remaining -= len; \
|
|
558
|
+
} \
|
|
559
|
+
} while(0)
|
|
560
|
+
|
|
561
|
+
bool first_attr = true;
|
|
562
|
+
|
|
563
|
+
/* Add ID */
|
|
564
|
+
if (attrs->id) {
|
|
565
|
+
char id_str[512];
|
|
566
|
+
if (first_attr) {
|
|
567
|
+
snprintf(id_str, sizeof(id_str), "id=\"%s\"", attrs->id);
|
|
568
|
+
first_attr = false;
|
|
569
|
+
} else {
|
|
570
|
+
snprintf(id_str, sizeof(id_str), " id=\"%s\"", attrs->id);
|
|
571
|
+
}
|
|
572
|
+
APPEND(id_str);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/* Add classes */
|
|
576
|
+
if (attrs->class_count > 0) {
|
|
577
|
+
if (first_attr) {
|
|
578
|
+
APPEND("class=\"");
|
|
579
|
+
first_attr = false;
|
|
580
|
+
} else {
|
|
581
|
+
APPEND(" class=\"");
|
|
582
|
+
}
|
|
583
|
+
for (int i = 0; i < attrs->class_count; i++) {
|
|
584
|
+
if (i > 0) APPEND(" ");
|
|
585
|
+
APPEND(attrs->classes[i]);
|
|
586
|
+
}
|
|
587
|
+
APPEND("\"");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/* Check for existing style attribute to merge with */
|
|
591
|
+
const char *existing_style = NULL;
|
|
592
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
593
|
+
if (strcmp(attrs->keys[i], "style") == 0) {
|
|
594
|
+
existing_style = attrs->values[i];
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/* Build style string for width/height that need to be in style */
|
|
600
|
+
char style_buffer[1024] = {0};
|
|
601
|
+
bool has_style = false;
|
|
602
|
+
|
|
603
|
+
/* Start with existing style if present */
|
|
604
|
+
if (existing_style && *existing_style) {
|
|
605
|
+
strncpy(style_buffer, existing_style, sizeof(style_buffer) - 1);
|
|
606
|
+
style_buffer[sizeof(style_buffer) - 1] = '\0';
|
|
607
|
+
has_style = true;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/* Process width and height attributes */
|
|
611
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
612
|
+
const char *key = attrs->keys[i];
|
|
613
|
+
const char *val = attrs->values[i];
|
|
614
|
+
|
|
615
|
+
/* Skip style attribute - we'll handle it separately */
|
|
616
|
+
if (strcmp(key, "style") == 0) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (strcmp(key, "width") == 0 || strcmp(key, "height") == 0) {
|
|
621
|
+
size_t val_len = strlen(val);
|
|
622
|
+
bool is_px = (val_len >= 2 && val[val_len - 2] == 'p' && val[val_len - 1] == 'x');
|
|
623
|
+
bool is_percent = (val_len >= 1 && val[val_len - 1] == '%');
|
|
624
|
+
bool is_integer = true;
|
|
625
|
+
bool is_decimal_px = false;
|
|
626
|
+
|
|
627
|
+
/* Check if it's a bare integer or integer pixel value */
|
|
628
|
+
if (is_px) {
|
|
629
|
+
/* Check if the part before 'px' is a pure integer (no decimal) */
|
|
630
|
+
for (size_t j = 0; j < val_len - 2; j++) {
|
|
631
|
+
if (val[j] == '.' || val[j] == ',') {
|
|
632
|
+
is_decimal_px = true;
|
|
633
|
+
is_integer = false;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
if (!isdigit((unsigned char)val[j])) {
|
|
637
|
+
is_integer = false;
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
} else if (is_percent) {
|
|
642
|
+
is_integer = false;
|
|
643
|
+
} else {
|
|
644
|
+
/* Check if all characters are digits */
|
|
645
|
+
for (const char *c = val; *c; c++) {
|
|
646
|
+
if (!isdigit((unsigned char)*c)) {
|
|
647
|
+
is_integer = false;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (is_px && !is_decimal_px && is_integer) {
|
|
654
|
+
/* Convert integer Xpx to integer X for width/height attributes */
|
|
655
|
+
char int_val[64];
|
|
656
|
+
memcpy(int_val, val, val_len - 2);
|
|
657
|
+
int_val[val_len - 2] = '\0';
|
|
658
|
+
|
|
659
|
+
char attr_str[1024];
|
|
660
|
+
if (first_attr) {
|
|
661
|
+
snprintf(attr_str, sizeof(attr_str), "%s=\"%s\"", key, int_val);
|
|
662
|
+
first_attr = false;
|
|
663
|
+
} else {
|
|
664
|
+
snprintf(attr_str, sizeof(attr_str), " %s=\"%s\"", key, int_val);
|
|
665
|
+
}
|
|
666
|
+
APPEND(attr_str);
|
|
667
|
+
} else if (is_integer && !is_px && !is_percent) {
|
|
668
|
+
/* Bare integer - use as width/height attribute */
|
|
669
|
+
char attr_str[1024];
|
|
670
|
+
if (first_attr) {
|
|
671
|
+
snprintf(attr_str, sizeof(attr_str), "%s=\"%s\"", key, val);
|
|
672
|
+
first_attr = false;
|
|
673
|
+
} else {
|
|
674
|
+
snprintf(attr_str, sizeof(attr_str), " %s=\"%s\"", key, val);
|
|
675
|
+
}
|
|
676
|
+
APPEND(attr_str);
|
|
677
|
+
} else {
|
|
678
|
+
/* Percentage, decimal pixel, or other non-integer - add to style */
|
|
679
|
+
if (has_style) {
|
|
680
|
+
strcat(style_buffer, "; ");
|
|
681
|
+
}
|
|
682
|
+
strcat(style_buffer, key);
|
|
683
|
+
strcat(style_buffer, ": ");
|
|
684
|
+
strcat(style_buffer, val);
|
|
685
|
+
has_style = true;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* Add style attribute if we have width/height in style or existing style */
|
|
691
|
+
if (has_style) {
|
|
692
|
+
char style_str[1024];
|
|
693
|
+
if (first_attr) {
|
|
694
|
+
snprintf(style_str, sizeof(style_str), "style=\"%s\"", style_buffer);
|
|
695
|
+
first_attr = false;
|
|
696
|
+
} else {
|
|
697
|
+
snprintf(style_str, sizeof(style_str), " style=\"%s\"", style_buffer);
|
|
698
|
+
}
|
|
699
|
+
APPEND(style_str);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/* Add other attributes (excluding width/height/style and internal srcset markers) */
|
|
703
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
704
|
+
const char *key = attrs->keys[i];
|
|
705
|
+
const char *val = attrs->values[i];
|
|
706
|
+
|
|
707
|
+
/* Skip width, height, style - we already processed them */
|
|
708
|
+
if (strcmp(key, "width") == 0 || strcmp(key, "height") == 0 || strcmp(key, "style") == 0) {
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
/* Skip internal @2x/@3x markers - used by attributes_to_html_for_image to emit srcset */
|
|
712
|
+
if (strcmp(key, "data-srcset-2x") == 0 ||
|
|
713
|
+
strcmp(key, "data-srcset-3x") == 0) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
char attr_str[1024];
|
|
718
|
+
if (first_attr) {
|
|
719
|
+
snprintf(attr_str, sizeof(attr_str), "%s=\"%s\"", key, val);
|
|
720
|
+
first_attr = false;
|
|
721
|
+
} else {
|
|
722
|
+
snprintf(attr_str, sizeof(attr_str), " %s=\"%s\"", key, val);
|
|
723
|
+
}
|
|
724
|
+
APPEND(attr_str);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
#undef APPEND
|
|
728
|
+
|
|
729
|
+
*p = '\0';
|
|
730
|
+
return strdup(buffer);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Check if a paragraph contains IAL (possibly with other content)
|
|
735
|
+
* Returns true if IAL found, extracts attributes, and modifies text to remove IAL
|
|
736
|
+
*/
|
|
737
|
+
/**
|
|
738
|
+
* Extract IAL from a PURE IAL paragraph (only contains "{: ...}")
|
|
739
|
+
* This is ONLY for next-line block IAL that applies to the previous element.
|
|
740
|
+
* Does NOT handle inline paragraph IAL - that's not supported in standard Kramdown.
|
|
741
|
+
*
|
|
742
|
+
* Example:
|
|
743
|
+
* Paragraph text.
|
|
744
|
+
*
|
|
745
|
+
* {: #id .class} <-- This is a pure IAL paragraph
|
|
746
|
+
*/
|
|
747
|
+
static bool extract_ial_from_paragraph(cmark_node *para, apex_attributes **attrs_out, ald_entry *alds) {
|
|
748
|
+
if (cmark_node_get_type(para) != CMARK_NODE_PARAGRAPH) return false;
|
|
749
|
+
|
|
750
|
+
/* Must have one text node, optionally followed by softbreak/linebreak (so "{: .lead}\n" is recognized) */
|
|
751
|
+
cmark_node *text_node = cmark_node_first_child(para);
|
|
752
|
+
if (!text_node) return false;
|
|
753
|
+
if (cmark_node_get_type(text_node) != CMARK_NODE_TEXT) return false;
|
|
754
|
+
cmark_node *after_text = cmark_node_next(text_node);
|
|
755
|
+
if (after_text) {
|
|
756
|
+
cmark_node_type after_type = cmark_node_get_type(after_text);
|
|
757
|
+
if (after_type != CMARK_NODE_SOFTBREAK && after_type != CMARK_NODE_LINEBREAK) return false;
|
|
758
|
+
if (cmark_node_next(after_text) != NULL) return false;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const char *text = cmark_node_get_literal(text_node);
|
|
762
|
+
if (!text) return false;
|
|
763
|
+
|
|
764
|
+
/* Trim leading whitespace */
|
|
765
|
+
while (isspace((unsigned char)*text)) text++;
|
|
766
|
+
if (*text == '\0') return false;
|
|
767
|
+
|
|
768
|
+
/* Must start with {: or {# or {. */
|
|
769
|
+
if (text[0] != '{') return false;
|
|
770
|
+
char second_char = text[1];
|
|
771
|
+
if (second_char != ':' && second_char != '#' && second_char != '.') return false;
|
|
772
|
+
|
|
773
|
+
/* Find closing } */
|
|
774
|
+
/* For {: format, skip {: (2 chars); for {# or {. format, skip { (1 char) */
|
|
775
|
+
const char *close = (second_char == ':') ? strchr(text + 2, '}') : strchr(text + 1, '}');
|
|
776
|
+
if (!close) return false;
|
|
777
|
+
|
|
778
|
+
/* If there is non-whitespace after }, use only the IAL prefix (e.g. "{: .lead}\n</div>" -> extract "{: .lead}") */
|
|
779
|
+
const char *after = close + 1;
|
|
780
|
+
while (*after && isspace((unsigned char)*after)) after++;
|
|
781
|
+
if (*after && *after != '\n') {
|
|
782
|
+
/* Trailing content (e.g. </div>) - extract IAL from prefix only */
|
|
783
|
+
size_t prefix_len = (size_t)(close + 1 - text);
|
|
784
|
+
char *prefix = malloc(prefix_len + 1);
|
|
785
|
+
if (!prefix) return false;
|
|
786
|
+
memcpy(prefix, text, prefix_len);
|
|
787
|
+
prefix[prefix_len] = '\0';
|
|
788
|
+
bool ok = extract_ial_from_text(prefix, attrs_out, alds);
|
|
789
|
+
free(prefix);
|
|
790
|
+
return ok;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/* This is a pure IAL paragraph - extract attributes */
|
|
794
|
+
return extract_ial_from_text(text, attrs_out, alds);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Handle span-level IAL (inline elements with attributes)
|
|
799
|
+
* Example: [Link](url){: .class} or {: #id}
|
|
800
|
+
*
|
|
801
|
+
* The IAL applies to the immediately preceding inline element (link, image, emphasis, etc.)
|
|
802
|
+
* IALs can appear inline within paragraphs, not just at the end.
|
|
803
|
+
* This function processes IALs recursively to handle nested inline elements.
|
|
804
|
+
*/
|
|
805
|
+
static bool process_span_ial_in_container(cmark_node *container, ald_entry *alds) {
|
|
806
|
+
cmark_node_type container_type = cmark_node_get_type(container);
|
|
807
|
+
/* Only process paragraphs and inline elements that can contain other inline elements */
|
|
808
|
+
if (container_type != CMARK_NODE_PARAGRAPH &&
|
|
809
|
+
container_type != CMARK_NODE_STRONG &&
|
|
810
|
+
container_type != CMARK_NODE_EMPH &&
|
|
811
|
+
container_type != CMARK_NODE_LINK) {
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
bool found_ial = false;
|
|
816
|
+
|
|
817
|
+
/* Process all text nodes in the container to find IALs */
|
|
818
|
+
/* Save next sibling before processing in case we need to unlink the node */
|
|
819
|
+
for (cmark_node *child = cmark_node_first_child(container); child; ) {
|
|
820
|
+
cmark_node *next = cmark_node_next(child); /* Save next before potential modification */
|
|
821
|
+
|
|
822
|
+
if (cmark_node_get_type(child) != CMARK_NODE_TEXT) {
|
|
823
|
+
child = next;
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const char *text = cmark_node_get_literal(child);
|
|
828
|
+
if (!text) {
|
|
829
|
+
child = next;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/* Look for IAL pattern: {: ... } or {#id .class} at the start (after optional whitespace) or at the end */
|
|
834
|
+
const char *text_ptr = text;
|
|
835
|
+
const char *ial_start = NULL;
|
|
836
|
+
char second_char = 0;
|
|
837
|
+
|
|
838
|
+
/* First, try at the start (after optional whitespace) */
|
|
839
|
+
const char *start_ptr = text;
|
|
840
|
+
while (*start_ptr && isspace((unsigned char)*start_ptr)) {
|
|
841
|
+
start_ptr++;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (start_ptr[0] == '{') {
|
|
845
|
+
char sc = start_ptr[1];
|
|
846
|
+
if (sc == ':' || sc == '#' || sc == '.') {
|
|
847
|
+
ial_start = start_ptr;
|
|
848
|
+
text_ptr = start_ptr;
|
|
849
|
+
second_char = sc;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/* If not found at start, try at the end (for inline IALs after elements) */
|
|
854
|
+
if (!ial_start) {
|
|
855
|
+
const char *end_ptr = strrchr(text, '{');
|
|
856
|
+
if (end_ptr) {
|
|
857
|
+
char sc = end_ptr[1];
|
|
858
|
+
if (sc == ':' || sc == '#' || sc == '.') {
|
|
859
|
+
/* Check if this is at the end (only whitespace after closing brace) */
|
|
860
|
+
const char *close = strchr(end_ptr, '}');
|
|
861
|
+
if (close) {
|
|
862
|
+
const char *after = close + 1;
|
|
863
|
+
while (*after && isspace((unsigned char)*after)) after++;
|
|
864
|
+
if (!*after) {
|
|
865
|
+
/* IAL is at the end of the text node */
|
|
866
|
+
ial_start = end_ptr;
|
|
867
|
+
text_ptr = text; /* Keep original text_ptr for prefix calculation */
|
|
868
|
+
second_char = sc;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (!ial_start) {
|
|
876
|
+
child = next;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const char *close = strchr(ial_start, '}');
|
|
881
|
+
if (!close) {
|
|
882
|
+
child = next;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/* Extract attributes from IAL - we need a version that doesn't require it to be at end */
|
|
887
|
+
apex_attributes *attrs = NULL;
|
|
888
|
+
|
|
889
|
+
/* Parse IAL content directly (since we know it's valid IAL syntax) */
|
|
890
|
+
/* For {: format, skip {: (2 chars); for {# or {. format, skip { (1 char) */
|
|
891
|
+
const char *content_start = (second_char == ':') ? ial_start + 2 : ial_start + 1;
|
|
892
|
+
int content_len = close - content_start;
|
|
893
|
+
|
|
894
|
+
if (content_len <= 0) {
|
|
895
|
+
child = next;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/* Copy content to work with */
|
|
900
|
+
char buffer[2048];
|
|
901
|
+
if (content_len >= (int)sizeof(buffer)) content_len = (int)sizeof(buffer) - 1;
|
|
902
|
+
memcpy(buffer, content_start, content_len);
|
|
903
|
+
buffer[content_len] = '\0';
|
|
904
|
+
|
|
905
|
+
char *p = buffer;
|
|
906
|
+
|
|
907
|
+
/* Skip leading whitespace */
|
|
908
|
+
while (isspace((unsigned char)*p)) p++;
|
|
909
|
+
if (!*p) {
|
|
910
|
+
child = next;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/* Extract first token */
|
|
915
|
+
char *token_start = p;
|
|
916
|
+
while (*p && !isspace((unsigned char)*p) && *p != '#' && *p != '.' && *p != '=') p++;
|
|
917
|
+
|
|
918
|
+
apex_attributes *ald_attrs = NULL;
|
|
919
|
+
const char *remaining_content = NULL;
|
|
920
|
+
int remaining_len = 0;
|
|
921
|
+
|
|
922
|
+
if (p > token_start) {
|
|
923
|
+
/* Check if first token is a simple word (ALD reference) */
|
|
924
|
+
char saved = *p;
|
|
925
|
+
*p = '\0';
|
|
926
|
+
|
|
927
|
+
bool is_ref = true;
|
|
928
|
+
for (char *c = token_start; *c; c++) {
|
|
929
|
+
if (*c == '#' || *c == '.' || *c == '=') {
|
|
930
|
+
is_ref = false;
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
*p = saved; /* Restore */
|
|
936
|
+
|
|
937
|
+
if (is_ref && *token_start) {
|
|
938
|
+
/* Trim the token */
|
|
939
|
+
char *trimmed = token_start;
|
|
940
|
+
while (isspace((unsigned char)*trimmed)) trimmed++;
|
|
941
|
+
char *end = trimmed + strlen(trimmed) - 1;
|
|
942
|
+
while (end > trimmed && isspace((unsigned char)*end)) *end-- = '\0';
|
|
943
|
+
|
|
944
|
+
/* Look up ALD */
|
|
945
|
+
if (*trimmed) {
|
|
946
|
+
ald_attrs = find_ald(alds, trimmed);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/* If we found an ALD, check for remaining content */
|
|
952
|
+
if (ald_attrs) {
|
|
953
|
+
/* Skip whitespace after token */
|
|
954
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
955
|
+
|
|
956
|
+
if (*p) {
|
|
957
|
+
/* There's remaining content - parse it as additional attributes */
|
|
958
|
+
remaining_content = content_start + (p - buffer);
|
|
959
|
+
remaining_len = content_len - (p - buffer);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/* Parse additional attributes if any */
|
|
964
|
+
apex_attributes *additional_attrs = NULL;
|
|
965
|
+
if (remaining_content && remaining_len > 0) {
|
|
966
|
+
additional_attrs = parse_ial_content(remaining_content, remaining_len);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/* Merge ALD with additional attributes */
|
|
970
|
+
if (ald_attrs) {
|
|
971
|
+
attrs = merge_attributes(ald_attrs, additional_attrs);
|
|
972
|
+
if (additional_attrs) {
|
|
973
|
+
apex_free_attributes(additional_attrs);
|
|
974
|
+
}
|
|
975
|
+
} else {
|
|
976
|
+
/* No ALD found, parse as regular IAL */
|
|
977
|
+
attrs = parse_ial_content(content_start, content_len);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (!attrs) {
|
|
981
|
+
child = next;
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/* Find the inline element immediately before this text node */
|
|
986
|
+
cmark_node *target = NULL;
|
|
987
|
+
cmark_node *prev = cmark_node_previous(child);
|
|
988
|
+
|
|
989
|
+
/* Skip over any text nodes to find the actual inline element */
|
|
990
|
+
while (prev) {
|
|
991
|
+
cmark_node_type prev_type = cmark_node_get_type(prev);
|
|
992
|
+
if (prev_type == CMARK_NODE_LINK ||
|
|
993
|
+
prev_type == CMARK_NODE_IMAGE ||
|
|
994
|
+
prev_type == CMARK_NODE_EMPH ||
|
|
995
|
+
prev_type == CMARK_NODE_STRONG ||
|
|
996
|
+
prev_type == CMARK_NODE_CODE) {
|
|
997
|
+
target = prev;
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
/* If it's a text node, continue walking backwards */
|
|
1001
|
+
if (prev_type == CMARK_NODE_TEXT) {
|
|
1002
|
+
prev = cmark_node_previous(prev);
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
/* If it's some other node type, stop - IAL can't apply to it */
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (!target) {
|
|
1010
|
+
/* No inline element found - if this is a paragraph and there is content before this IAL (so not a pure IAL-only paragraph), apply to the paragraph (block IAL without blank line).
|
|
1011
|
+
* Content before IAL can be: a previous sibling (e.g. text + softbreak + IAL text node), or text before IAL in this node (e.g. single node "Text\n{: .lead }"). */
|
|
1012
|
+
bool has_preceding_content = (cmark_node_previous(child) != NULL) || (ial_start > text);
|
|
1013
|
+
if (container_type == CMARK_NODE_PARAGRAPH && has_preceding_content) {
|
|
1014
|
+
/* IAL was at end of text - apply to paragraph (e.g. "Text\n{: .lead }" on one block) */
|
|
1015
|
+
char *attr_str = attributes_to_html(attrs);
|
|
1016
|
+
cmark_node_set_user_data(container, attr_str);
|
|
1017
|
+
apex_free_attributes(attrs);
|
|
1018
|
+
|
|
1019
|
+
/* Remove the IAL from the text node */
|
|
1020
|
+
size_t prefix_len = ial_start - text;
|
|
1021
|
+
const char *suffix = close + 1;
|
|
1022
|
+
size_t suffix_len = strlen(suffix);
|
|
1023
|
+
size_t new_len = prefix_len + suffix_len;
|
|
1024
|
+
char *new_text = NULL;
|
|
1025
|
+
|
|
1026
|
+
if (new_len > 0) {
|
|
1027
|
+
new_text = malloc(new_len + 1);
|
|
1028
|
+
if (new_text) {
|
|
1029
|
+
if (prefix_len > 0) memcpy(new_text, text, prefix_len);
|
|
1030
|
+
if (suffix_len > 0)
|
|
1031
|
+
strcpy(new_text + prefix_len, suffix);
|
|
1032
|
+
else
|
|
1033
|
+
new_text[prefix_len] = '\0';
|
|
1034
|
+
if (prefix_len > 0 && suffix_len == 0) {
|
|
1035
|
+
char *end = new_text + prefix_len - 1;
|
|
1036
|
+
while (end >= new_text && isspace((unsigned char)*end)) *end-- = '\0';
|
|
1037
|
+
}
|
|
1038
|
+
if (strlen(new_text) == 0) {
|
|
1039
|
+
cmark_node_unlink(child);
|
|
1040
|
+
cmark_node_free(child);
|
|
1041
|
+
free(new_text);
|
|
1042
|
+
new_text = NULL;
|
|
1043
|
+
} else {
|
|
1044
|
+
cmark_node_set_literal(child, new_text);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
} else {
|
|
1048
|
+
cmark_node_unlink(child);
|
|
1049
|
+
cmark_node_free(child);
|
|
1050
|
+
}
|
|
1051
|
+
if (new_text) free(new_text);
|
|
1052
|
+
found_ial = true;
|
|
1053
|
+
} else {
|
|
1054
|
+
apex_free_attributes(attrs);
|
|
1055
|
+
}
|
|
1056
|
+
child = next;
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/* Verify that the target is actually within this container (could be nested) */
|
|
1061
|
+
/* Walk up from target to see if we reach container */
|
|
1062
|
+
cmark_node *target_parent = cmark_node_parent(target);
|
|
1063
|
+
bool target_in_container = false;
|
|
1064
|
+
while (target_parent) {
|
|
1065
|
+
if (target_parent == container) {
|
|
1066
|
+
target_in_container = true;
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
/* If we reach a non-inline element, stop */
|
|
1070
|
+
cmark_node_type parent_type = cmark_node_get_type(target_parent);
|
|
1071
|
+
if (parent_type != CMARK_NODE_STRONG &&
|
|
1072
|
+
parent_type != CMARK_NODE_EMPH &&
|
|
1073
|
+
parent_type != CMARK_NODE_LINK &&
|
|
1074
|
+
parent_type != CMARK_NODE_PARAGRAPH) {
|
|
1075
|
+
break;
|
|
1076
|
+
}
|
|
1077
|
+
target_parent = cmark_node_parent(target_parent);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (!target_in_container) {
|
|
1081
|
+
apex_free_attributes(attrs);
|
|
1082
|
+
child = next;
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/* Apply attributes to the target inline element */
|
|
1087
|
+
char *attr_str = attributes_to_html(attrs);
|
|
1088
|
+
cmark_node_set_user_data(target, attr_str);
|
|
1089
|
+
if (getenv("APEX_DEBUG_PIPELINE") && cmark_node_get_type(target) == CMARK_NODE_LINK) {
|
|
1090
|
+
fprintf(stderr, "[APEX_DEBUG] IAL applied to link (attrs: %.80s%s)\n",
|
|
1091
|
+
attr_str ? attr_str : "(null)", attr_str && strlen(attr_str) > 80 ? "..." : "");
|
|
1092
|
+
}
|
|
1093
|
+
apex_free_attributes(attrs);
|
|
1094
|
+
|
|
1095
|
+
/* Remove the IAL from the text node, preserving any text before/after it */
|
|
1096
|
+
size_t prefix_len;
|
|
1097
|
+
if (ial_start == start_ptr) {
|
|
1098
|
+
/* IAL was at the start - prefix is just whitespace before IAL */
|
|
1099
|
+
prefix_len = text_ptr - text;
|
|
1100
|
+
} else {
|
|
1101
|
+
/* IAL was at the end - prefix is everything before the IAL */
|
|
1102
|
+
prefix_len = ial_start - text;
|
|
1103
|
+
}
|
|
1104
|
+
const char *suffix = close + 1; /* Text after IAL closing brace */
|
|
1105
|
+
|
|
1106
|
+
/* Build new text: prefix (if any) + suffix (if any) */
|
|
1107
|
+
size_t suffix_len = strlen(suffix);
|
|
1108
|
+
size_t new_len = prefix_len + suffix_len;
|
|
1109
|
+
char *new_text = NULL;
|
|
1110
|
+
|
|
1111
|
+
if (new_len > 0) {
|
|
1112
|
+
new_text = malloc(new_len + 1);
|
|
1113
|
+
if (new_text) {
|
|
1114
|
+
/* Copy prefix (leading whitespace before IAL) */
|
|
1115
|
+
if (prefix_len > 0) {
|
|
1116
|
+
memcpy(new_text, text, prefix_len);
|
|
1117
|
+
}
|
|
1118
|
+
/* Copy suffix (text after IAL) */
|
|
1119
|
+
if (suffix_len > 0) {
|
|
1120
|
+
strcpy(new_text + prefix_len, suffix);
|
|
1121
|
+
} else {
|
|
1122
|
+
new_text[prefix_len] = '\0';
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/* Trim trailing whitespace from prefix */
|
|
1126
|
+
if (prefix_len > 0 && suffix_len == 0) {
|
|
1127
|
+
char *end = new_text + prefix_len - 1;
|
|
1128
|
+
while (end >= new_text && isspace((unsigned char)*end)) {
|
|
1129
|
+
*end-- = '\0';
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
/* Remove node if empty, otherwise update it */
|
|
1134
|
+
if (strlen(new_text) == 0) {
|
|
1135
|
+
cmark_node_unlink(child);
|
|
1136
|
+
cmark_node_free(child);
|
|
1137
|
+
free(new_text);
|
|
1138
|
+
new_text = NULL;
|
|
1139
|
+
/* Don't update child here - use saved 'next' */
|
|
1140
|
+
} else {
|
|
1141
|
+
cmark_node_set_literal(child, new_text);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
/* No prefix and no suffix - remove the node */
|
|
1146
|
+
cmark_node_unlink(child);
|
|
1147
|
+
cmark_node_free(child);
|
|
1148
|
+
/* Don't update child here - use saved 'next' */
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (new_text) {
|
|
1152
|
+
free(new_text);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
found_ial = true;
|
|
1156
|
+
/* Continue with next sibling (may be NULL if we unlinked) */
|
|
1157
|
+
child = next;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/* Recursively process inline elements that can contain other inline elements */
|
|
1161
|
+
/* Use a separate loop to avoid modifying the container while iterating */
|
|
1162
|
+
for (cmark_node *inline_child = cmark_node_first_child(container); inline_child; inline_child = cmark_node_next(inline_child)) {
|
|
1163
|
+
cmark_node_type child_type = cmark_node_get_type(inline_child);
|
|
1164
|
+
if (child_type == CMARK_NODE_STRONG ||
|
|
1165
|
+
child_type == CMARK_NODE_EMPH ||
|
|
1166
|
+
child_type == CMARK_NODE_LINK) {
|
|
1167
|
+
if (process_span_ial_in_container(inline_child, alds)) {
|
|
1168
|
+
found_ial = true;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return found_ial;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Handle span-level IAL for paragraphs (wrapper for recursive function)
|
|
1178
|
+
*/
|
|
1179
|
+
static bool process_span_ial(cmark_node *para, ald_entry *alds) {
|
|
1180
|
+
if (cmark_node_get_type(para) != CMARK_NODE_PARAGRAPH) return false;
|
|
1181
|
+
return process_span_ial_in_container(para, alds);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Extract IAL from heading text (inline syntax: ## Heading {: #id})
|
|
1186
|
+
*/
|
|
1187
|
+
static bool extract_ial_from_heading(cmark_node *heading, apex_attributes **attrs_out, ald_entry *alds) {
|
|
1188
|
+
if (cmark_node_get_type(heading) != CMARK_NODE_HEADING) return false;
|
|
1189
|
+
|
|
1190
|
+
/* Get the text node inside the heading */
|
|
1191
|
+
cmark_node *text_node = cmark_node_first_child(heading);
|
|
1192
|
+
if (!text_node || cmark_node_get_type(text_node) != CMARK_NODE_TEXT) return false;
|
|
1193
|
+
|
|
1194
|
+
const char *text = cmark_node_get_literal(text_node);
|
|
1195
|
+
if (!text) return false;
|
|
1196
|
+
|
|
1197
|
+
/* Look for { at the end - support both {: and {# or {. formats */
|
|
1198
|
+
const char *ial_start = strrchr(text, '{');
|
|
1199
|
+
if (!ial_start) return false;
|
|
1200
|
+
char second_char = ial_start[1];
|
|
1201
|
+
if (second_char != ':' && second_char != '#' && second_char != '.') return false;
|
|
1202
|
+
|
|
1203
|
+
/* Find closing } */
|
|
1204
|
+
const char *close = strchr(ial_start, '}');
|
|
1205
|
+
if (!close) return false;
|
|
1206
|
+
|
|
1207
|
+
/* Check nothing after } except whitespace */
|
|
1208
|
+
const char *after = close + 1;
|
|
1209
|
+
while (*after && isspace((unsigned char)*after)) after++;
|
|
1210
|
+
if (*after) return false;
|
|
1211
|
+
|
|
1212
|
+
|
|
1213
|
+
/* Extract attributes */
|
|
1214
|
+
if (!extract_ial_from_text(ial_start, attrs_out, alds)) {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/* Remove IAL from heading text */
|
|
1219
|
+
size_t prefix_len = ial_start - text;
|
|
1220
|
+
|
|
1221
|
+
char *new_text = malloc(prefix_len + 1);
|
|
1222
|
+
if (!new_text) return false;
|
|
1223
|
+
|
|
1224
|
+
if (prefix_len > 0) {
|
|
1225
|
+
memcpy(new_text, text, prefix_len);
|
|
1226
|
+
new_text[prefix_len] = '\0';
|
|
1227
|
+
|
|
1228
|
+
/* Trim trailing whitespace */
|
|
1229
|
+
char *end = new_text + prefix_len - 1;
|
|
1230
|
+
while (end >= new_text && isspace((unsigned char)*end)) *end-- = '\0';
|
|
1231
|
+
} else {
|
|
1232
|
+
/* Heading was only IAL - leave empty string */
|
|
1233
|
+
new_text[0] = '\0';
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
cmark_node_set_literal(text_node, new_text);
|
|
1238
|
+
free(new_text);
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Check if a paragraph is ONLY an IAL (should be removed entirely).
|
|
1244
|
+
* Allows optional trailing softbreak/linebreak so "{: .lead}\n" is recognized when the parser adds a linebreak node.
|
|
1245
|
+
*/
|
|
1246
|
+
static bool is_pure_ial_paragraph(cmark_node *para) {
|
|
1247
|
+
if (cmark_node_get_type(para) != CMARK_NODE_PARAGRAPH) {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
cmark_node *text_node = cmark_node_first_child(para);
|
|
1252
|
+
if (!text_node) {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
if (cmark_node_get_type(text_node) != CMARK_NODE_TEXT) {
|
|
1256
|
+
return false;
|
|
1257
|
+
}
|
|
1258
|
+
cmark_node *after_text = cmark_node_next(text_node);
|
|
1259
|
+
if (after_text) {
|
|
1260
|
+
cmark_node_type after_type = cmark_node_get_type(after_text);
|
|
1261
|
+
if (after_type != CMARK_NODE_SOFTBREAK && after_type != CMARK_NODE_LINEBREAK) {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
if (cmark_node_next(after_text) != NULL) {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const char *text = cmark_node_get_literal(text_node);
|
|
1270
|
+
if (!text) {
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/* Trim leading whitespace */
|
|
1275
|
+
while (isspace((unsigned char)*text)) text++;
|
|
1276
|
+
|
|
1277
|
+
/* Find end of text, trimming trailing whitespace including newlines */
|
|
1278
|
+
const char *text_end = text + strlen(text);
|
|
1279
|
+
while (text_end > text && isspace((unsigned char)*(text_end - 1))) {
|
|
1280
|
+
text_end--;
|
|
1281
|
+
}
|
|
1282
|
+
size_t text_len = text_end - text;
|
|
1283
|
+
|
|
1284
|
+
/* Check if it's ONLY {: ... } or {#id .class} */
|
|
1285
|
+
if (text_len == 0 || text[0] != '{') return false;
|
|
1286
|
+
if (text_len < 2) return false;
|
|
1287
|
+
char second_char = text[1];
|
|
1288
|
+
if (second_char != ':' && second_char != '#' && second_char != '.') {
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/* For {: format, skip {: (2 chars); for {# or {. format, skip { (1 char) */
|
|
1293
|
+
const char *search_start = (second_char == ':') ? text + 2 : text + 1;
|
|
1294
|
+
if (search_start >= text_end) {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
const char *close = strchr(search_start, '}');
|
|
1298
|
+
if (!close) {
|
|
1299
|
+
return false;
|
|
1300
|
+
}
|
|
1301
|
+
if (close >= text_end) {
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/* Allow optional trailing content (e.g. "{: .lead}\n</div>" when IAL is followed by HTML in same block); still treat as pure IAL for previous block */
|
|
1306
|
+
return true;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Process IAL for a single node
|
|
1311
|
+
* Check if node has inline IAL or if next sibling is IAL paragraph
|
|
1312
|
+
*/
|
|
1313
|
+
/**
|
|
1314
|
+
* Process IAL for a node
|
|
1315
|
+
* Returns the node to free (if any), or NULL
|
|
1316
|
+
* Caller must free the returned node after iteration is complete
|
|
1317
|
+
*/
|
|
1318
|
+
static cmark_node *process_node_ial(cmark_node *node, ald_entry *alds) {
|
|
1319
|
+
if (!node) return NULL;
|
|
1320
|
+
|
|
1321
|
+
cmark_node_type type = cmark_node_get_type(node);
|
|
1322
|
+
|
|
1323
|
+
/* Handle heading with inline IAL (## Heading {: #id}) */
|
|
1324
|
+
if (type == CMARK_NODE_HEADING) {
|
|
1325
|
+
apex_attributes *attrs = NULL;
|
|
1326
|
+
bool extracted = extract_ial_from_heading(node, &attrs, alds);
|
|
1327
|
+
if (extracted) {
|
|
1328
|
+
/* Store attributes in heading */
|
|
1329
|
+
char *attr_str = attributes_to_html(attrs);
|
|
1330
|
+
|
|
1331
|
+
/* Merge with existing user_data if present */
|
|
1332
|
+
char *existing = (char *)cmark_node_get_user_data(node);
|
|
1333
|
+
if (existing) {
|
|
1334
|
+
/* Append to existing */
|
|
1335
|
+
char *combined = malloc(strlen(existing) + strlen(attr_str) + 1);
|
|
1336
|
+
if (combined) {
|
|
1337
|
+
strcpy(combined, existing);
|
|
1338
|
+
strcat(combined, attr_str);
|
|
1339
|
+
cmark_node_set_user_data(node, combined);
|
|
1340
|
+
free(attr_str);
|
|
1341
|
+
} else {
|
|
1342
|
+
cmark_node_set_user_data(node, attr_str);
|
|
1343
|
+
}
|
|
1344
|
+
} else {
|
|
1345
|
+
cmark_node_set_user_data(node, attr_str);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
apex_free_attributes(attrs);
|
|
1349
|
+
return NULL; /* No node to free */
|
|
1350
|
+
}
|
|
1351
|
+
/* If no inline IAL, fall through to check for next-line IAL */
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/* Handle span-level IAL (links, images, emphasis, etc. with inline attributes) */
|
|
1355
|
+
if (type == CMARK_NODE_PARAGRAPH) {
|
|
1356
|
+
if (process_span_ial(node, alds)) {
|
|
1357
|
+
return NULL; /* Span IAL processed, no node to free */
|
|
1358
|
+
}
|
|
1359
|
+
/* No span IAL found, fall through to check for next-line IAL */
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/* Only certain block types can have IAL after them */
|
|
1363
|
+
if (type != CMARK_NODE_HEADING &&
|
|
1364
|
+
type != CMARK_NODE_PARAGRAPH &&
|
|
1365
|
+
type != CMARK_NODE_BLOCK_QUOTE &&
|
|
1366
|
+
type != CMARK_NODE_CODE_BLOCK &&
|
|
1367
|
+
type != CMARK_NODE_LIST &&
|
|
1368
|
+
type != CMARK_NODE_ITEM &&
|
|
1369
|
+
type != CMARK_NODE_TABLE) { /* Tables can have IAL */
|
|
1370
|
+
return NULL; /* No node to free */
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
/* Look at next sibling(s) for IAL paragraph (skip over HTML blocks; cmark may put </div> between paragraph and IAL) */
|
|
1375
|
+
cmark_node *next = cmark_node_next(node);
|
|
1376
|
+
if (!next) {
|
|
1377
|
+
return NULL; /* No node to free */
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
/* Skip HTML/custom blocks so we find a following IAL paragraph (e.g. paragraph, </div> html_block, {: .lead} paragraph) */
|
|
1381
|
+
while (next) {
|
|
1382
|
+
cmark_node_type next_type = cmark_node_get_type(next);
|
|
1383
|
+
if (next_type != CMARK_NODE_HTML_BLOCK && next_type != CMARK_NODE_CUSTOM_BLOCK) {
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
next = cmark_node_next(next);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if (!next || cmark_node_get_type(next) != CMARK_NODE_PARAGRAPH) {
|
|
1390
|
+
return NULL; /* No node to free */
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/* Check if it's a pure IAL paragraph */
|
|
1394
|
+
if (is_pure_ial_paragraph(next)) {
|
|
1395
|
+
apex_attributes *attrs = NULL;
|
|
1396
|
+
if (extract_ial_from_paragraph(next, &attrs, alds)) {
|
|
1397
|
+
/* Store attributes in this node */
|
|
1398
|
+
char *attr_str = attributes_to_html(attrs);
|
|
1399
|
+
cmark_node_set_user_data(node, attr_str);
|
|
1400
|
+
apex_free_attributes(attrs);
|
|
1401
|
+
|
|
1402
|
+
/* Return node to be unlinked and freed after iteration completes */
|
|
1403
|
+
/* Don't unlink here - that invalidates the iterator */
|
|
1404
|
+
return next; /* Return node to unlink and free after iteration */
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return NULL; /* No node to free */
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Process IAL in AST
|
|
1413
|
+
*/
|
|
1414
|
+
void apex_process_ial_in_tree(cmark_node *node, ald_entry *alds) {
|
|
1415
|
+
if (!node) return;
|
|
1416
|
+
|
|
1417
|
+
/* Collect nodes to unlink and free after iteration to avoid use-after-free */
|
|
1418
|
+
cmark_node **nodes_to_free = NULL;
|
|
1419
|
+
size_t free_count = 0;
|
|
1420
|
+
size_t free_capacity = 0;
|
|
1421
|
+
|
|
1422
|
+
/* First pass: process IAL and collect nodes to remove */
|
|
1423
|
+
cmark_iter *iter = cmark_iter_new(node);
|
|
1424
|
+
cmark_event_type ev_type;
|
|
1425
|
+
|
|
1426
|
+
while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
|
|
1427
|
+
cmark_node *cur = cmark_iter_get_node(iter);
|
|
1428
|
+
|
|
1429
|
+
/* Only process on ENTER events */
|
|
1430
|
+
if (ev_type == CMARK_EVENT_ENTER) {
|
|
1431
|
+
/* Process node and collect any nodes that need to be freed */
|
|
1432
|
+
cmark_node *node_to_free = process_node_ial(cur, alds);
|
|
1433
|
+
if (node_to_free) {
|
|
1434
|
+
/* Expand array if needed */
|
|
1435
|
+
if (free_count >= free_capacity) {
|
|
1436
|
+
free_capacity = free_capacity ? free_capacity * 2 : 16;
|
|
1437
|
+
cmark_node **new_array = realloc(nodes_to_free, free_capacity * sizeof(cmark_node*));
|
|
1438
|
+
if (new_array) {
|
|
1439
|
+
nodes_to_free = new_array;
|
|
1440
|
+
} else {
|
|
1441
|
+
/* If realloc fails, unlink and free immediately */
|
|
1442
|
+
cmark_node_unlink(node_to_free);
|
|
1443
|
+
cmark_node_free(node_to_free);
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
nodes_to_free[free_count++] = node_to_free;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
cmark_iter_free(iter);
|
|
1453
|
+
|
|
1454
|
+
/* Second pass: unlink and free collected nodes after iteration is complete */
|
|
1455
|
+
for (size_t i = 0; i < free_count; i++) {
|
|
1456
|
+
cmark_node_unlink(nodes_to_free[i]);
|
|
1457
|
+
cmark_node_free(nodes_to_free[i]);
|
|
1458
|
+
}
|
|
1459
|
+
free(nodes_to_free);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/**
|
|
1463
|
+
* Check if a line is a pure IAL (starts with {: or {# or {. and ends with })
|
|
1464
|
+
*/
|
|
1465
|
+
static bool is_ial_line(const char *line, size_t len) {
|
|
1466
|
+
const char *p = line;
|
|
1467
|
+
const char *end = line + len;
|
|
1468
|
+
|
|
1469
|
+
/* Skip leading whitespace */
|
|
1470
|
+
while (p < end && isspace((unsigned char)*p)) p++;
|
|
1471
|
+
|
|
1472
|
+
/* Must start with {: or {# or {. */
|
|
1473
|
+
if (p + 2 > end || p[0] != '{') return false;
|
|
1474
|
+
/* Leanpub index syntax {i: term} is not IAL */
|
|
1475
|
+
if (p[1] == 'i' && p[2] == ':') return false;
|
|
1476
|
+
char second_char = p[1];
|
|
1477
|
+
if (second_char != ':' && second_char != '#' && second_char != '.') return false;
|
|
1478
|
+
|
|
1479
|
+
/* Find closing } */
|
|
1480
|
+
/* For {: format, skip {: (2 chars); for {# or {. format, skip { (1 char) */
|
|
1481
|
+
const char *search_start = (second_char == ':') ? p + 2 : p + 1;
|
|
1482
|
+
const char *close = memchr(search_start, '}', end - search_start);
|
|
1483
|
+
if (!close) return false;
|
|
1484
|
+
|
|
1485
|
+
/* Check nothing substantial after the } */
|
|
1486
|
+
const char *after = close + 1;
|
|
1487
|
+
while (after < end && isspace((unsigned char)*after)) after++;
|
|
1488
|
+
|
|
1489
|
+
return (after >= end);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Preprocess text to separate IAL markers from preceding content.
|
|
1494
|
+
* Kramdown allows IAL on the line immediately following content,
|
|
1495
|
+
* but cmark-gfm treats that as part of the same paragraph.
|
|
1496
|
+
* This inserts blank lines before IAL markers.
|
|
1497
|
+
*/
|
|
1498
|
+
char *apex_preprocess_ial(const char *text) {
|
|
1499
|
+
if (!text) return NULL;
|
|
1500
|
+
|
|
1501
|
+
size_t text_len = strlen(text);
|
|
1502
|
+
/* Worst case: we add a newline before every line */
|
|
1503
|
+
size_t capacity = text_len * 2 + 1;
|
|
1504
|
+
char *output = malloc(capacity);
|
|
1505
|
+
if (!output) return NULL;
|
|
1506
|
+
|
|
1507
|
+
char *out = output;
|
|
1508
|
+
const char *p = text;
|
|
1509
|
+
bool prev_line_was_content = false;
|
|
1510
|
+
bool prev_line_was_blank = true; /* Start as if there was a blank line before */
|
|
1511
|
+
|
|
1512
|
+
while (*p) {
|
|
1513
|
+
/* Find end of current line */
|
|
1514
|
+
const char *line_start = p;
|
|
1515
|
+
const char *line_end = strchr(p, '\n');
|
|
1516
|
+
if (!line_end) {
|
|
1517
|
+
line_end = p + strlen(p);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
size_t line_len = line_end - line_start;
|
|
1521
|
+
|
|
1522
|
+
/* Check if this line is blank */
|
|
1523
|
+
bool is_blank = true;
|
|
1524
|
+
for (size_t i = 0; i < line_len; i++) {
|
|
1525
|
+
if (!isspace((unsigned char)line_start[i])) {
|
|
1526
|
+
is_blank = false;
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/* Check if this line is an IAL */
|
|
1532
|
+
bool is_ial = is_ial_line(line_start, line_len);
|
|
1533
|
+
|
|
1534
|
+
/* Special case: Kramdown-style TOC marker "{:toc ...}".
|
|
1535
|
+
*
|
|
1536
|
+
* In Kramdown/Jekyll, a pure IAL paragraph containing only "{:toc}"
|
|
1537
|
+
* (optionally with additional parameters) is replaced with a
|
|
1538
|
+
* generated table of contents. We map this syntax to Apex's
|
|
1539
|
+
* existing TOC marker "<!--TOC ...-->" so it is handled by the
|
|
1540
|
+
* TOC extension.
|
|
1541
|
+
*/
|
|
1542
|
+
bool handled_toc_marker = false;
|
|
1543
|
+
if (is_ial) {
|
|
1544
|
+
const char *q = line_start;
|
|
1545
|
+
const char *end = line_start + line_len;
|
|
1546
|
+
|
|
1547
|
+
/* Skip leading whitespace */
|
|
1548
|
+
while (q < end && isspace((unsigned char)*q)) q++;
|
|
1549
|
+
|
|
1550
|
+
/* Must start with "{:" */
|
|
1551
|
+
if (q + 2 <= end && q[0] == '{' && q[1] == ':') {
|
|
1552
|
+
q += 2;
|
|
1553
|
+
|
|
1554
|
+
/* Find closing '}' */
|
|
1555
|
+
const char *close = memchr(q, '}', (size_t)(end - q));
|
|
1556
|
+
if (close) {
|
|
1557
|
+
/* Extract and trim inner content */
|
|
1558
|
+
const char *inner_start = q;
|
|
1559
|
+
const char *inner_end = close;
|
|
1560
|
+
while (inner_start < inner_end && isspace((unsigned char)*inner_start)) {
|
|
1561
|
+
inner_start++;
|
|
1562
|
+
}
|
|
1563
|
+
while (inner_end > inner_start && isspace((unsigned char)inner_end[-1])) {
|
|
1564
|
+
inner_end--;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (inner_start < inner_end) {
|
|
1568
|
+
/* Check for leading "toc" (case-insensitive) */
|
|
1569
|
+
const char *toc_start = inner_start;
|
|
1570
|
+
const char *toc_end = toc_start + 3;
|
|
1571
|
+
if ((size_t)(inner_end - inner_start) >= 3 &&
|
|
1572
|
+
(toc_start[0] == 't' || toc_start[0] == 'T') &&
|
|
1573
|
+
(toc_start[1] == 'o' || toc_start[1] == 'O') &&
|
|
1574
|
+
(toc_start[2] == 'c' || toc_start[2] == 'C') &&
|
|
1575
|
+
(toc_end == inner_end || isspace((unsigned char)*toc_end))) {
|
|
1576
|
+
/* Everything after "toc" (including any whitespace) is
|
|
1577
|
+
* treated as TOC options and passed through to the
|
|
1578
|
+
* marker. This allows syntax like "{:toc max=3 min=2}". */
|
|
1579
|
+
const char *options_start = toc_end;
|
|
1580
|
+
while (options_start < inner_end &&
|
|
1581
|
+
isspace((unsigned char)*options_start)) {
|
|
1582
|
+
options_start++;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/* If this is a TOC marker, optionally insert a blank
|
|
1586
|
+
* line before it to keep block structure consistent
|
|
1587
|
+
* with normal IAL handling. */
|
|
1588
|
+
if (prev_line_was_content && !prev_line_was_blank) {
|
|
1589
|
+
*out++ = '\n';
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/* Build "<!--TOC [options]-->" */
|
|
1593
|
+
const char *marker_prefix = "<!--TOC";
|
|
1594
|
+
size_t prefix_len = strlen(marker_prefix);
|
|
1595
|
+
memcpy(out, marker_prefix, prefix_len);
|
|
1596
|
+
out += prefix_len;
|
|
1597
|
+
|
|
1598
|
+
if (options_start < inner_end) {
|
|
1599
|
+
*out++ = ' ';
|
|
1600
|
+
size_t opts_len = (size_t)(inner_end - options_start);
|
|
1601
|
+
memcpy(out, options_start, opts_len);
|
|
1602
|
+
out += opts_len;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
*out++ = '-';
|
|
1606
|
+
*out++ = '-';
|
|
1607
|
+
*out++ = '>';
|
|
1608
|
+
|
|
1609
|
+
/* Preserve original newline if present */
|
|
1610
|
+
if (*line_end == '\n') {
|
|
1611
|
+
*out++ = '\n';
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
handled_toc_marker = true;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (!handled_toc_marker) {
|
|
1622
|
+
/* If this is an IAL and previous line was content (not blank, not IAL),
|
|
1623
|
+
* insert a blank line before it */
|
|
1624
|
+
if (is_ial && prev_line_was_content && !prev_line_was_blank) {
|
|
1625
|
+
*out++ = '\n';
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/* Copy the line */
|
|
1629
|
+
memcpy(out, line_start, line_len);
|
|
1630
|
+
out += line_len;
|
|
1631
|
+
|
|
1632
|
+
/* Copy the newline if present */
|
|
1633
|
+
if (*line_end == '\n') {
|
|
1634
|
+
*out++ = '\n';
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/* Advance input pointer */
|
|
1639
|
+
p = (*line_end == '\n') ? line_end + 1 : line_end;
|
|
1640
|
+
|
|
1641
|
+
/* Track state for next iteration */
|
|
1642
|
+
prev_line_was_blank = is_blank;
|
|
1643
|
+
prev_line_was_content = !is_blank && !is_ial;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
*out = '\0';
|
|
1647
|
+
return output;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* URL encode a string (percent encoding)
|
|
1652
|
+
* Only encodes unsafe characters (space, control chars, non-ASCII, etc.)
|
|
1653
|
+
* Preserves valid URL characters like /, :, ?, #, etc.
|
|
1654
|
+
* Returns newly allocated string, caller must free
|
|
1655
|
+
*/
|
|
1656
|
+
static char *url_encode(const char *url) {
|
|
1657
|
+
if (!url) return NULL;
|
|
1658
|
+
|
|
1659
|
+
/* Calculate size needed (worst case: 3 chars per byte) */
|
|
1660
|
+
size_t len = strlen(url);
|
|
1661
|
+
size_t capacity = len * 3 + 1;
|
|
1662
|
+
char *encoded = malloc(capacity);
|
|
1663
|
+
if (!encoded) return NULL;
|
|
1664
|
+
|
|
1665
|
+
char *out = encoded;
|
|
1666
|
+
for (const char *p = url; *p; p++) {
|
|
1667
|
+
unsigned char c = (unsigned char)*p;
|
|
1668
|
+
/* Unreserved characters (always safe): A-Z, a-z, 0-9, -, _, ., ~ */
|
|
1669
|
+
/* Reserved characters that are safe in URL paths: /, :, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, = */
|
|
1670
|
+
/* Also preserve % if it's part of already-encoded content */
|
|
1671
|
+
if ((c >= 'A' && c <= 'Z') ||
|
|
1672
|
+
(c >= 'a' && c <= 'z') ||
|
|
1673
|
+
(c >= '0' && c <= '9') ||
|
|
1674
|
+
c == '-' || c == '_' || c == '.' || c == '~' ||
|
|
1675
|
+
c == '/' || c == ':' || c == '?' || c == '#' ||
|
|
1676
|
+
c == '[' || c == ']' || c == '@' || c == '!' ||
|
|
1677
|
+
c == '$' || c == '&' || c == '\'' || c == '(' ||
|
|
1678
|
+
c == ')' || c == '*' || c == '+' || c == ',' ||
|
|
1679
|
+
c == ';' || c == '=' || c == '%') {
|
|
1680
|
+
*out++ = c;
|
|
1681
|
+
} else {
|
|
1682
|
+
/* Encode unsafe characters: space, control chars, non-ASCII, etc. */
|
|
1683
|
+
snprintf(out, 4, "%%%02X", c);
|
|
1684
|
+
out += 3;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
*out = '\0';
|
|
1688
|
+
return encoded;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Parse attributes from a string (similar to parse_ial_content but for image attributes)
|
|
1693
|
+
* Handles: width=300 style="float:left" "title"
|
|
1694
|
+
*/
|
|
1695
|
+
static apex_attributes *parse_image_attributes(const char *attr_str, int len) {
|
|
1696
|
+
apex_attributes *attrs = create_attributes();
|
|
1697
|
+
if (!attrs) return NULL;
|
|
1698
|
+
|
|
1699
|
+
if (len <= 0 || !attr_str) return attrs;
|
|
1700
|
+
|
|
1701
|
+
char buffer[2048];
|
|
1702
|
+
if (len >= (int)sizeof(buffer)) len = (int)sizeof(buffer) - 1;
|
|
1703
|
+
memcpy(buffer, attr_str, len);
|
|
1704
|
+
buffer[len] = '\0';
|
|
1705
|
+
|
|
1706
|
+
char *p = buffer;
|
|
1707
|
+
while (*p) {
|
|
1708
|
+
/* Skip whitespace */
|
|
1709
|
+
while (isspace((unsigned char)*p)) p++;
|
|
1710
|
+
if (!*p) break;
|
|
1711
|
+
|
|
1712
|
+
/* Check for quoted title at the end ("title" or 'title') */
|
|
1713
|
+
if (*p == '"' || *p == '\'') {
|
|
1714
|
+
char quote = *p++;
|
|
1715
|
+
char *title_start = p;
|
|
1716
|
+
while (*p && *p != quote) {
|
|
1717
|
+
if (*p == '\\' && *(p+1)) p++; /* Skip escaped char */
|
|
1718
|
+
p++;
|
|
1719
|
+
}
|
|
1720
|
+
if (*p == quote) {
|
|
1721
|
+
*p = '\0';
|
|
1722
|
+
add_attribute(attrs, "title", title_start);
|
|
1723
|
+
*p = quote;
|
|
1724
|
+
p++;
|
|
1725
|
+
}
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/* Check for key=value */
|
|
1730
|
+
char *key_start = p;
|
|
1731
|
+
while (*p && *p != '=' && !isspace((unsigned char)*p)) p++;
|
|
1732
|
+
|
|
1733
|
+
if (*p == '=') {
|
|
1734
|
+
/* Found key=value */
|
|
1735
|
+
char saved = *p;
|
|
1736
|
+
*p = '\0';
|
|
1737
|
+
char *key = strdup(key_start);
|
|
1738
|
+
*p = saved;
|
|
1739
|
+
p++; /* Skip = */
|
|
1740
|
+
|
|
1741
|
+
/* Parse value (could be quoted or unquoted) */
|
|
1742
|
+
char *value = NULL;
|
|
1743
|
+
if (*p == '"' || *p == '\'') {
|
|
1744
|
+
char quote = *p++;
|
|
1745
|
+
char *value_start = p;
|
|
1746
|
+
while (*p && *p != quote) {
|
|
1747
|
+
if (*p == '\\' && *(p+1)) p++; /* Skip escaped char */
|
|
1748
|
+
p++;
|
|
1749
|
+
}
|
|
1750
|
+
if (*p == quote) {
|
|
1751
|
+
*p = '\0';
|
|
1752
|
+
value = strdup(value_start);
|
|
1753
|
+
*p = quote;
|
|
1754
|
+
p++;
|
|
1755
|
+
}
|
|
1756
|
+
} else {
|
|
1757
|
+
/* Unquoted value */
|
|
1758
|
+
char *value_start = p;
|
|
1759
|
+
while (*p && !isspace((unsigned char)*p)) p++;
|
|
1760
|
+
char saved_val = *p;
|
|
1761
|
+
*p = '\0';
|
|
1762
|
+
value = strdup(value_start);
|
|
1763
|
+
*p = saved_val;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
if (value) {
|
|
1767
|
+
add_attribute(attrs, key, value);
|
|
1768
|
+
free(value);
|
|
1769
|
+
}
|
|
1770
|
+
free(key);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/* Check for bare @2x/@3x (retina srcset markers) */
|
|
1775
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
1776
|
+
key_start[0] == '@' && key_start[1] == '2' && key_start[2] == 'x') {
|
|
1777
|
+
add_attribute(attrs, "data-srcset-2x", "1");
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
if (p > key_start && (size_t)(p - key_start) == 3 &&
|
|
1781
|
+
key_start[0] == '@' && key_start[1] == '3' && key_start[2] == 'x') {
|
|
1782
|
+
add_attribute(attrs, "data-srcset-3x", "1");
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/* Unknown token, skip */
|
|
1787
|
+
p++;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
return attrs;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Build the @2x version of a URL.
|
|
1795
|
+
*
|
|
1796
|
+
* Rules:
|
|
1797
|
+
* - Never modify the domain portion of a URL (everything up to the first '/' after the scheme).
|
|
1798
|
+
* - Only insert "@2x" before a file-style extension in the path, e.g.:
|
|
1799
|
+
* "img/icon.png" -> "img/icon@2x.png"
|
|
1800
|
+
* "https://host/img/icon.png?x=1" -> "https://host/img/icon@2x.png?x=1"
|
|
1801
|
+
* - If there is no '.' in the path segment (before query/fragment), we skip srcset and
|
|
1802
|
+
* return NULL rather than trying to synthesize a bogus "@2x" URL.
|
|
1803
|
+
*
|
|
1804
|
+
* Caller must free the returned string when non-NULL.
|
|
1805
|
+
*/
|
|
1806
|
+
static char *url_with_2x_suffix(const char *url) {
|
|
1807
|
+
if (!url || !*url) return NULL;
|
|
1808
|
+
|
|
1809
|
+
/* Skip scheme (e.g. "http://", "https://") so we don't treat dots in the scheme. */
|
|
1810
|
+
const char *p = strstr(url, "://");
|
|
1811
|
+
if (p) {
|
|
1812
|
+
p += 3; /* move past "://" */
|
|
1813
|
+
} else {
|
|
1814
|
+
p = url;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/* Find first '/' after scheme to separate domain from path. */
|
|
1818
|
+
const char *first_slash = strchr(p, '/');
|
|
1819
|
+
|
|
1820
|
+
/* Path search should start at first_slash (if any), otherwise at url. */
|
|
1821
|
+
const char *path_start = first_slash ? first_slash : url;
|
|
1822
|
+
|
|
1823
|
+
/* Identify end of path segment before query ('?') or fragment ('#'). */
|
|
1824
|
+
const char *qmark = strchr(path_start, '?');
|
|
1825
|
+
const char *hash = strchr(path_start, '#');
|
|
1826
|
+
const char *path_end = NULL;
|
|
1827
|
+
if (qmark && hash) {
|
|
1828
|
+
path_end = (qmark < hash) ? qmark : hash;
|
|
1829
|
+
} else if (qmark) {
|
|
1830
|
+
path_end = qmark;
|
|
1831
|
+
} else if (hash) {
|
|
1832
|
+
path_end = hash;
|
|
1833
|
+
} else {
|
|
1834
|
+
path_end = url + strlen(url);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/* Search for last '.' in the path portion only (do not look in the domain). */
|
|
1838
|
+
const char *scan_start = path_start;
|
|
1839
|
+
const char *scan_end = path_end;
|
|
1840
|
+
const char *last_dot = NULL;
|
|
1841
|
+
for (const char *c = scan_start; c < scan_end; c++) {
|
|
1842
|
+
if (*c == '.') {
|
|
1843
|
+
last_dot = c;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/* If there is no '.' in the path (i.e. no obvious extension), skip @2x. */
|
|
1848
|
+
if (!last_dot) {
|
|
1849
|
+
return NULL;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/* Build: prefix (up to dot) + "@2x" + suffix (from dot to end of URL). */
|
|
1853
|
+
size_t prefix_len = (size_t)(last_dot - url);
|
|
1854
|
+
size_t suffix_len = strlen(last_dot);
|
|
1855
|
+
char *out = malloc(prefix_len + 3 + suffix_len + 1); /* 3 for "@2x" */
|
|
1856
|
+
if (!out) return NULL;
|
|
1857
|
+
|
|
1858
|
+
memcpy(out, url, prefix_len);
|
|
1859
|
+
memcpy(out + prefix_len, "@2x", 3);
|
|
1860
|
+
memcpy(out + prefix_len + 3, last_dot, suffix_len + 1); /* include NUL */
|
|
1861
|
+
return out;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
/**
|
|
1865
|
+
* Check if attributes contain the @2x/@3x srcset markers
|
|
1866
|
+
*/
|
|
1867
|
+
static bool attrs_have_srcset_2x(apex_attributes *attrs) {
|
|
1868
|
+
if (!attrs) return false;
|
|
1869
|
+
return find_attribute_index(attrs, "data-srcset-2x") >= 0;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
static bool attrs_have_srcset_3x(apex_attributes *attrs) {
|
|
1873
|
+
if (!attrs) return false;
|
|
1874
|
+
return find_attribute_index(attrs, "data-srcset-3x") >= 0;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
/**
|
|
1878
|
+
* Build the @3x version of a URL.
|
|
1879
|
+
* Uses the same domain-safe rules as url_with_2x_suffix.
|
|
1880
|
+
*/
|
|
1881
|
+
static char *url_with_3x_suffix(const char *url) {
|
|
1882
|
+
if (!url || !*url) return NULL;
|
|
1883
|
+
|
|
1884
|
+
/* Skip scheme (e.g. "http://", "https://") so we don't treat dots in the scheme. */
|
|
1885
|
+
const char *p = strstr(url, "://");
|
|
1886
|
+
if (p) {
|
|
1887
|
+
p += 3; /* move past "://" */
|
|
1888
|
+
} else {
|
|
1889
|
+
p = url;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
/* Find first '/' after scheme to separate domain from path. */
|
|
1893
|
+
const char *first_slash = strchr(p, '/');
|
|
1894
|
+
|
|
1895
|
+
/* Path search should start at first_slash (if any), otherwise at url. */
|
|
1896
|
+
const char *path_start = first_slash ? first_slash : url;
|
|
1897
|
+
|
|
1898
|
+
/* Identify end of path segment before query ('?') or fragment ('#'). */
|
|
1899
|
+
const char *qmark = strchr(path_start, '?');
|
|
1900
|
+
const char *hash = strchr(path_start, '#');
|
|
1901
|
+
const char *path_end = NULL;
|
|
1902
|
+
if (qmark && hash) {
|
|
1903
|
+
path_end = (qmark < hash) ? qmark : hash;
|
|
1904
|
+
} else if (qmark) {
|
|
1905
|
+
path_end = qmark;
|
|
1906
|
+
} else if (hash) {
|
|
1907
|
+
path_end = hash;
|
|
1908
|
+
} else {
|
|
1909
|
+
path_end = url + strlen(url);
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
/* Search for last '.' in the path portion only (do not look in the domain). */
|
|
1913
|
+
const char *scan_start = path_start;
|
|
1914
|
+
const char *scan_end = path_end;
|
|
1915
|
+
const char *last_dot = NULL;
|
|
1916
|
+
for (const char *c = scan_start; c < scan_end; c++) {
|
|
1917
|
+
if (*c == '.') {
|
|
1918
|
+
last_dot = c;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
if (!last_dot) {
|
|
1923
|
+
return NULL;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
size_t prefix_len = (size_t)(last_dot - url);
|
|
1927
|
+
size_t suffix_len = strlen(last_dot);
|
|
1928
|
+
char *out = malloc(prefix_len + 3 + suffix_len + 1); /* 3 for "@3x" */
|
|
1929
|
+
if (!out) return NULL;
|
|
1930
|
+
|
|
1931
|
+
memcpy(out, url, prefix_len);
|
|
1932
|
+
memcpy(out + prefix_len, "@3x", 3);
|
|
1933
|
+
memcpy(out + prefix_len + 3, last_dot, suffix_len + 1); /* include NUL */
|
|
1934
|
+
return out;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
/**
|
|
1938
|
+
* Convert image attributes to HTML string, including srcset when @2x/@3x is present.
|
|
1939
|
+
* When data-srcset-2x/data-srcset-3x are in attrs, emits srcset="url 1x, url@2x 2x[, url@3x 3x]"
|
|
1940
|
+
* and omits the internal markers from the output attributes.
|
|
1941
|
+
* Caller must free the returned string.
|
|
1942
|
+
*/
|
|
1943
|
+
static char *attributes_to_html_for_image(const char *url, apex_attributes *attrs) {
|
|
1944
|
+
if (!attrs) return strdup("");
|
|
1945
|
+
|
|
1946
|
+
bool have_2x = attrs_have_srcset_2x(attrs);
|
|
1947
|
+
bool have_3x = attrs_have_srcset_3x(attrs);
|
|
1948
|
+
|
|
1949
|
+
/* @3x implies we should also emit a 2x entry, even if @2x was not explicitly set. */
|
|
1950
|
+
bool want_2x = have_2x || have_3x;
|
|
1951
|
+
bool want_3x = have_3x;
|
|
1952
|
+
|
|
1953
|
+
char *url_2x = (want_2x && url) ? url_with_2x_suffix(url) : NULL;
|
|
1954
|
+
char *url_3x = (want_3x && url) ? url_with_3x_suffix(url) : NULL;
|
|
1955
|
+
|
|
1956
|
+
char *base_attrs = attributes_to_html(attrs);
|
|
1957
|
+
if (!base_attrs) {
|
|
1958
|
+
free(url_2x);
|
|
1959
|
+
free(url_3x);
|
|
1960
|
+
return strdup("");
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
/* If no valid srcset needed, return base_attrs (attributes_to_html already omits data-srcset-*). */
|
|
1964
|
+
if (!want_2x || !url_2x) {
|
|
1965
|
+
free(url_2x);
|
|
1966
|
+
free(url_3x);
|
|
1967
|
+
return base_attrs;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/* Build srcset string. */
|
|
1971
|
+
size_t len = strlen(url) + strlen(url_2x) + 32;
|
|
1972
|
+
if (want_3x && url_3x) {
|
|
1973
|
+
len += strlen(url_3x) + 16;
|
|
1974
|
+
}
|
|
1975
|
+
char *srcset_attr = malloc(len);
|
|
1976
|
+
if (!srcset_attr) {
|
|
1977
|
+
free(base_attrs);
|
|
1978
|
+
free(url_2x);
|
|
1979
|
+
free(url_3x);
|
|
1980
|
+
return base_attrs;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
if (want_3x && url_3x) {
|
|
1984
|
+
/* url 1x, url@2x 2x, url@3x 3x */
|
|
1985
|
+
snprintf(srcset_attr, len, " srcset=\"%s 1x, %s 2x, %s 3x\"", url, url_2x, url_3x);
|
|
1986
|
+
} else {
|
|
1987
|
+
/* url 1x, url@2x 2x */
|
|
1988
|
+
snprintf(srcset_attr, len, " srcset=\"%s 1x, %s 2x\"", url, url_2x);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
free(url_2x);
|
|
1992
|
+
free(url_3x);
|
|
1993
|
+
|
|
1994
|
+
/* Prepend srcset to base_attrs */
|
|
1995
|
+
size_t base_len = strlen(base_attrs);
|
|
1996
|
+
while (base_len > 0 && (base_attrs[base_len - 1] == ' ' || base_attrs[base_len - 1] == '\t')) {
|
|
1997
|
+
base_attrs[--base_len] = '\0';
|
|
1998
|
+
}
|
|
1999
|
+
size_t srcset_attr_len = strlen(srcset_attr);
|
|
2000
|
+
char *result = malloc(srcset_attr_len + (base_len ? base_len + 2 : 0) + 1);
|
|
2001
|
+
if (!result) {
|
|
2002
|
+
free(srcset_attr);
|
|
2003
|
+
return base_attrs;
|
|
2004
|
+
}
|
|
2005
|
+
char *w = result;
|
|
2006
|
+
memcpy(w, srcset_attr, srcset_attr_len);
|
|
2007
|
+
w += srcset_attr_len;
|
|
2008
|
+
if (base_len > 0) {
|
|
2009
|
+
*w++ = ' ';
|
|
2010
|
+
memcpy(w, base_attrs, base_len + 1);
|
|
2011
|
+
} else {
|
|
2012
|
+
*w = '\0';
|
|
2013
|
+
}
|
|
2014
|
+
free(srcset_attr);
|
|
2015
|
+
free(base_attrs);
|
|
2016
|
+
return result;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Create a new image attribute entry (always creates a new entry, doesn't reuse)
|
|
2021
|
+
*/
|
|
2022
|
+
static image_attr_entry *create_image_attr_entry(image_attr_entry **list, const char *url, int index) {
|
|
2023
|
+
if (!list || !url) return NULL;
|
|
2024
|
+
|
|
2025
|
+
/* Create new entry - always create new, don't reuse by URL */
|
|
2026
|
+
image_attr_entry *entry = calloc(1, sizeof(image_attr_entry));
|
|
2027
|
+
if (entry) {
|
|
2028
|
+
entry->url = strdup(url);
|
|
2029
|
+
entry->attrs = create_attributes();
|
|
2030
|
+
entry->index = index;
|
|
2031
|
+
entry->ref_name = NULL;
|
|
2032
|
+
entry->next = *list;
|
|
2033
|
+
*list = entry;
|
|
2034
|
+
}
|
|
2035
|
+
return entry;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* Create image attribute entry with reference name (for reference-style definitions)
|
|
2040
|
+
*/
|
|
2041
|
+
static image_attr_entry *create_image_attr_entry_with_ref(image_attr_entry **list, const char *url, const char *ref_name) {
|
|
2042
|
+
if (!list || !url) return NULL;
|
|
2043
|
+
|
|
2044
|
+
image_attr_entry *entry = calloc(1, sizeof(image_attr_entry));
|
|
2045
|
+
if (entry) {
|
|
2046
|
+
entry->url = strdup(url);
|
|
2047
|
+
entry->attrs = create_attributes();
|
|
2048
|
+
entry->index = -1;
|
|
2049
|
+
entry->ref_name = ref_name ? strdup(ref_name) : NULL;
|
|
2050
|
+
entry->next = *list;
|
|
2051
|
+
*list = entry;
|
|
2052
|
+
}
|
|
2053
|
+
return entry;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* Free image attribute list
|
|
2058
|
+
*/
|
|
2059
|
+
void apex_free_image_attributes(image_attr_entry *img_attrs) {
|
|
2060
|
+
while (img_attrs) {
|
|
2061
|
+
image_attr_entry *next = img_attrs->next;
|
|
2062
|
+
free(img_attrs->url);
|
|
2063
|
+
free(img_attrs->ref_name);
|
|
2064
|
+
apex_free_attributes(img_attrs->attrs);
|
|
2065
|
+
free(img_attrs);
|
|
2066
|
+
img_attrs = next;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
/**
|
|
2071
|
+
* Convert attributes to inline image format (key=value, for use inside parentheses)
|
|
2072
|
+
* This converts IAL attributes (ID, classes) to key=value format that parse_image_attributes can understand
|
|
2073
|
+
*/
|
|
2074
|
+
static char *attributes_to_inline_format(apex_attributes *attrs) {
|
|
2075
|
+
if (!attrs || (!attrs->id && attrs->class_count == 0 && attrs->attr_count == 0)) {
|
|
2076
|
+
return strdup("");
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
char buffer[4096];
|
|
2080
|
+
char *p = buffer;
|
|
2081
|
+
size_t remaining = sizeof(buffer);
|
|
2082
|
+
|
|
2083
|
+
#define APPEND_INLINE(str) do { \
|
|
2084
|
+
size_t len = strlen(str); \
|
|
2085
|
+
if (len < remaining) { \
|
|
2086
|
+
memcpy(p, str, len); \
|
|
2087
|
+
p += len; \
|
|
2088
|
+
remaining -= len; \
|
|
2089
|
+
} \
|
|
2090
|
+
} while(0)
|
|
2091
|
+
|
|
2092
|
+
bool first = true;
|
|
2093
|
+
|
|
2094
|
+
/* Add ID as id="value" */
|
|
2095
|
+
if (attrs->id) {
|
|
2096
|
+
char id_str[512];
|
|
2097
|
+
snprintf(id_str, sizeof(id_str), "%sid=\"%s\"", first ? "" : " ", attrs->id);
|
|
2098
|
+
first = false;
|
|
2099
|
+
APPEND_INLINE(id_str);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/* Add classes as class="class1 class2" */
|
|
2103
|
+
if (attrs->class_count > 0) {
|
|
2104
|
+
char class_str[1024];
|
|
2105
|
+
char *class_p = class_str;
|
|
2106
|
+
size_t class_remaining = sizeof(class_str);
|
|
2107
|
+
snprintf(class_p, class_remaining, "%sclass=\"", first ? "" : " ");
|
|
2108
|
+
class_p += strlen(class_p);
|
|
2109
|
+
class_remaining -= strlen(class_p);
|
|
2110
|
+
|
|
2111
|
+
for (int i = 0; i < attrs->class_count; i++) {
|
|
2112
|
+
if (i > 0 && class_remaining > 0) {
|
|
2113
|
+
*class_p++ = ' ';
|
|
2114
|
+
class_remaining--;
|
|
2115
|
+
}
|
|
2116
|
+
size_t class_len = strlen(attrs->classes[i]);
|
|
2117
|
+
if (class_len < class_remaining) {
|
|
2118
|
+
memcpy(class_p, attrs->classes[i], class_len);
|
|
2119
|
+
class_p += class_len;
|
|
2120
|
+
class_remaining -= class_len;
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
if (class_remaining > 0) {
|
|
2124
|
+
*class_p++ = '"';
|
|
2125
|
+
class_remaining--;
|
|
2126
|
+
}
|
|
2127
|
+
*class_p = '\0';
|
|
2128
|
+
first = false;
|
|
2129
|
+
APPEND_INLINE(class_str);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
/* Add other attributes - format as key=value or key="value" */
|
|
2133
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
2134
|
+
char attr_str[1024];
|
|
2135
|
+
const char *val = attrs->values[i];
|
|
2136
|
+
/* Quote value if it contains spaces, semicolons, or special characters */
|
|
2137
|
+
bool need_quotes = (strchr(val, ' ') != NULL || strchr(val, ';') != NULL || strchr(val, '"') != NULL);
|
|
2138
|
+
if (need_quotes) {
|
|
2139
|
+
snprintf(attr_str, sizeof(attr_str), "%s%s=\"%s\"", first ? "" : " ", attrs->keys[i], val);
|
|
2140
|
+
} else {
|
|
2141
|
+
snprintf(attr_str, sizeof(attr_str), "%s%s=%s", first ? "" : " ", attrs->keys[i], val);
|
|
2142
|
+
}
|
|
2143
|
+
first = false;
|
|
2144
|
+
APPEND_INLINE(attr_str);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
#undef APPEND_INLINE
|
|
2148
|
+
|
|
2149
|
+
*p = '\0';
|
|
2150
|
+
return strdup(buffer);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/**
|
|
2154
|
+
* Convert attributes back to markdown attribute string (for expanding reference-style images)
|
|
2155
|
+
* Uses inline format (key=value) for compatibility with parse_image_attributes
|
|
2156
|
+
*/
|
|
2157
|
+
static char *attributes_to_markdown(apex_attributes *attrs) {
|
|
2158
|
+
return attributes_to_inline_format(attrs);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
/**
|
|
2162
|
+
* Find image attribute entry by reference name
|
|
2163
|
+
*/
|
|
2164
|
+
static image_attr_entry *find_image_attr_by_ref(image_attr_entry *list, const char *ref_name) {
|
|
2165
|
+
for (image_attr_entry *entry = list; entry; entry = entry->next) {
|
|
2166
|
+
if (entry->ref_name && ref_name && strcmp(entry->ref_name, ref_name) == 0) {
|
|
2167
|
+
return entry;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
return NULL;
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
/**
|
|
2174
|
+
* Check if text starting at 'p' looks like the start of attributes (key=value pattern)
|
|
2175
|
+
*/
|
|
2176
|
+
static bool looks_like_attribute_start(const char *p, const char *end) {
|
|
2177
|
+
if (!p || p >= end) return false;
|
|
2178
|
+
|
|
2179
|
+
/* Skip whitespace */
|
|
2180
|
+
while (p < end && (*p == ' ' || *p == '\t')) p++;
|
|
2181
|
+
if (p >= end) return false;
|
|
2182
|
+
|
|
2183
|
+
/* Look for key= pattern (attribute name followed by =) */
|
|
2184
|
+
const char *key_start = p;
|
|
2185
|
+
while (p < end && *p != '=' && *p != ' ' && *p != '\t' && *p != ')') {
|
|
2186
|
+
p++;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
if (p < end && *p == '=' && p > key_start) {
|
|
2190
|
+
/* Found key= pattern - this looks like attributes */
|
|
2191
|
+
return true;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
/* Also check for quoted title at the end ("title" or 'title') */
|
|
2195
|
+
if (p < end && (*p == '"' || *p == '\'')) {
|
|
2196
|
+
return true;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
return false;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Check if URL has a protocol (scheme followed by ://). Such URLs are assumed
|
|
2204
|
+
* already percent-encoded; we do not encode them. Scheme = letter then *( letter / digit / "+" / "-" / "." ) per URI spec.
|
|
2205
|
+
*/
|
|
2206
|
+
static bool has_protocol(const char *url) {
|
|
2207
|
+
if (!url || !*url) return false;
|
|
2208
|
+
const char *p = url;
|
|
2209
|
+
if (!(*p >= 'a' && *p <= 'z') && !(*p >= 'A' && *p <= 'Z'))
|
|
2210
|
+
return false;
|
|
2211
|
+
p++;
|
|
2212
|
+
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') ||
|
|
2213
|
+
*p == '+' || *p == '-' || *p == '.')
|
|
2214
|
+
p++;
|
|
2215
|
+
return (p[0] == ':' && p[1] == '/' && p[2] == '/');
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
/**
|
|
2219
|
+
* Known attribute names (for splitting URL from attributes; avoids splitting on query params).
|
|
2220
|
+
*/
|
|
2221
|
+
/**
|
|
2222
|
+
* Check if attributes look like image-specific (width, height, style, class, id, rel, data-srcset-2x, data-srcset-3x).
|
|
2223
|
+
* Used to decide whether a reference definition with attributes should be treated as image ref.
|
|
2224
|
+
*/
|
|
2225
|
+
static bool attrs_are_image_specific(apex_attributes *attrs) {
|
|
2226
|
+
if (!attrs) return false;
|
|
2227
|
+
if (attrs->id || (attrs->class_count > 0)) return true;
|
|
2228
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
2229
|
+
const char *key = attrs->keys[i];
|
|
2230
|
+
if (strcmp(key, "width") == 0 || strcmp(key, "height") == 0 ||
|
|
2231
|
+
strcmp(key, "style") == 0 || strcmp(key, "class") == 0 ||
|
|
2232
|
+
strcmp(key, "id") == 0 || strcmp(key, "rel") == 0 ||
|
|
2233
|
+
strcmp(key, "data-srcset-2x") == 0 ||
|
|
2234
|
+
strcmp(key, "data-srcset-3x") == 0) {
|
|
2235
|
+
return true;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
return false;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* Check if text at 'p' (after optional space) looks like the start of attributes:
|
|
2243
|
+
* a key with no spaces followed immediately by '=' (\w+=), or a quoted title (" or ').
|
|
2244
|
+
* Used to split URL from attributes without treating URL query params as attributes.
|
|
2245
|
+
*/
|
|
2246
|
+
static bool looks_like_attr_key_equals(const char *p, const char *end) {
|
|
2247
|
+
if (!p || p >= end) return false;
|
|
2248
|
+
while (p < end && (*p == ' ' || *p == '\t')) p++;
|
|
2249
|
+
if (p >= end) return false;
|
|
2250
|
+
/* Quoted title at end */
|
|
2251
|
+
if (*p == '"' || *p == '\'') return true;
|
|
2252
|
+
/* key= : one or more word chars (letter, digit, underscore) then = */
|
|
2253
|
+
const char *key_start = p;
|
|
2254
|
+
while (p < end && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
|
|
2255
|
+
(*p >= '0' && *p <= '9') || *p == '_'))
|
|
2256
|
+
p++;
|
|
2257
|
+
return (p > key_start && p < end && *p == '=');
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
/**
|
|
2261
|
+
* Preprocess markdown to extract image attributes and URL-encode all link URLs
|
|
2262
|
+
*/
|
|
2263
|
+
char *apex_preprocess_image_attributes(const char *text, image_attr_entry **img_attrs, apex_mode_t mode) {
|
|
2264
|
+
if (!text) return NULL;
|
|
2265
|
+
|
|
2266
|
+
if (getenv("APEX_DEBUG_PIPELINE")) {
|
|
2267
|
+
size_t len = strlen(text);
|
|
2268
|
+
fprintf(stderr, "[APEX_DEBUG] preprocess_image_attributes in (len=%zu): %.300s%s\n",
|
|
2269
|
+
len, text, len > 300 ? "..." : "");
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
/* Check if we should do URL encoding */
|
|
2273
|
+
bool do_url_encoding = (mode == APEX_MODE_UNIFIED ||
|
|
2274
|
+
mode == APEX_MODE_MULTIMARKDOWN ||
|
|
2275
|
+
mode == APEX_MODE_KRAMDOWN);
|
|
2276
|
+
|
|
2277
|
+
/* Check if we should process image attributes.
|
|
2278
|
+
* Enabled for Unified, MultiMarkdown, and GFM modes so that width/height/style
|
|
2279
|
+
* and @2x markers on images and reference definitions are honored consistently.
|
|
2280
|
+
*/
|
|
2281
|
+
bool do_image_attrs = (mode == APEX_MODE_UNIFIED ||
|
|
2282
|
+
mode == APEX_MODE_MULTIMARKDOWN ||
|
|
2283
|
+
mode == APEX_MODE_GFM);
|
|
2284
|
+
|
|
2285
|
+
if (!do_url_encoding && !do_image_attrs) {
|
|
2286
|
+
/* Nothing to do */
|
|
2287
|
+
return NULL;
|
|
2288
|
+
}
|
|
2289
|
+
size_t text_len = strlen(text);
|
|
2290
|
+
size_t capacity = text_len * 3 + 1; /* Extra space for URL encoding expansion */
|
|
2291
|
+
char *output = malloc(capacity);
|
|
2292
|
+
if (!output) return NULL;
|
|
2293
|
+
|
|
2294
|
+
const char *read = text;
|
|
2295
|
+
char *write = output;
|
|
2296
|
+
size_t remaining = capacity;
|
|
2297
|
+
image_attr_entry *local_img_attrs = NULL;
|
|
2298
|
+
/* Track position of images in document so inline attributes
|
|
2299
|
+
* are applied only to the specific image that declared them.
|
|
2300
|
+
* This must count all inline images, including those without
|
|
2301
|
+
* any attributes, to keep it in sync with the AST walk in
|
|
2302
|
+
* apex_apply_image_attributes.
|
|
2303
|
+
*/
|
|
2304
|
+
int image_index = 0;
|
|
2305
|
+
|
|
2306
|
+
while (*read) {
|
|
2307
|
+
/* Look for inline images:  */
|
|
2308
|
+
if (*read == '!' && read[1] == '[') {
|
|
2309
|
+
const char *img_start = read;
|
|
2310
|
+
const char *check_pos = read + 2; /* After ![ */
|
|
2311
|
+
|
|
2312
|
+
/* Find closing ] for alt text */
|
|
2313
|
+
const char *alt_end = strchr(check_pos, ']');
|
|
2314
|
+
if (alt_end && alt_end[1] == '(') {
|
|
2315
|
+
/* This is an inline image  - process it */
|
|
2316
|
+
const char *url_start = alt_end + 2; /* After ]( */
|
|
2317
|
+
const char *p = url_start;
|
|
2318
|
+
const char *url_end = NULL;
|
|
2319
|
+
const char *attr_start = NULL;
|
|
2320
|
+
const char *paren_end = NULL;
|
|
2321
|
+
|
|
2322
|
+
/* Find the closing paren first */
|
|
2323
|
+
p = url_start;
|
|
2324
|
+
while (*p && *p != ')' && *p != '\n') p++;
|
|
2325
|
+
if (*p == ')') {
|
|
2326
|
+
paren_end = p;
|
|
2327
|
+
} else {
|
|
2328
|
+
/* Malformed - skip just the ! and continue */
|
|
2329
|
+
read = img_start + 1;
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
/* Scan forward from url_start.
|
|
2334
|
+
*
|
|
2335
|
+
* For images (do_image_attrs == true) we treat everything after the
|
|
2336
|
+
* first whitespace as an "attribute string" and hand it to
|
|
2337
|
+
* parse_image_attributes(). That function understands both quoted
|
|
2338
|
+
* titles ("title" or 'title') and key=value pairs (width=200),
|
|
2339
|
+
* so constructs like:
|
|
2340
|
+
*
|
|
2341
|
+
* 
|
|
2342
|
+
* 
|
|
2343
|
+
*
|
|
2344
|
+
* are all parsed correctly:
|
|
2345
|
+
* - "Title" -> title attribute
|
|
2346
|
+
* - width=200 -> width attribute
|
|
2347
|
+
*
|
|
2348
|
+
* We then remove that attribute string from the markdown we pass
|
|
2349
|
+
* to cmark, leaving only the URL inside the parentheses so the
|
|
2350
|
+
* core parser still sees a valid inline image.
|
|
2351
|
+
*
|
|
2352
|
+
* In modes without image attributes, we keep the original
|
|
2353
|
+
* behavior and try to detect a standard Markdown title so that
|
|
2354
|
+
* cmark can parse it.
|
|
2355
|
+
*/
|
|
2356
|
+
p = url_start;
|
|
2357
|
+
while (p < paren_end) {
|
|
2358
|
+
if (*p == ' ' || *p == '\t') {
|
|
2359
|
+
/* Found a space - check what follows */
|
|
2360
|
+
const char *after_space = p;
|
|
2361
|
+
while (after_space < paren_end && (*after_space == ' ' || *after_space == '\t')) after_space++;
|
|
2362
|
+
|
|
2363
|
+
if (after_space < paren_end) {
|
|
2364
|
+
/* Space + key= or bare @2x/@3x: always split so we don't encode into URL */
|
|
2365
|
+
if (looks_like_attr_key_equals(after_space, paren_end)) {
|
|
2366
|
+
attr_start = after_space;
|
|
2367
|
+
url_end = p;
|
|
2368
|
+
break;
|
|
2369
|
+
}
|
|
2370
|
+
if ((size_t)(paren_end - after_space) >= 3 &&
|
|
2371
|
+
after_space[0] == '@' &&
|
|
2372
|
+
((after_space[1] == '2' && after_space[2] == 'x') ||
|
|
2373
|
+
(after_space[1] == '3' && after_space[2] == 'x')) &&
|
|
2374
|
+
(after_space + 3 >= paren_end || isspace((unsigned char)after_space[3]))) {
|
|
2375
|
+
attr_start = after_space;
|
|
2376
|
+
url_end = p;
|
|
2377
|
+
break;
|
|
2378
|
+
}
|
|
2379
|
+
if (do_image_attrs) {
|
|
2380
|
+
/* For images with MMD6-style parentheses titles,
|
|
2381
|
+
* keep using the core parser's title handling:
|
|
2382
|
+
*
|
|
2383
|
+
* )
|
|
2384
|
+
*
|
|
2385
|
+
* In this case we should NOT treat the tail as
|
|
2386
|
+
* attributes, otherwise we lose the title and
|
|
2387
|
+
* leave a stray closing parenthesis in output.
|
|
2388
|
+
*/
|
|
2389
|
+
if (*after_space == '(') {
|
|
2390
|
+
url_end = p; /* Let cmark handle the title */
|
|
2391
|
+
break;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
/* For everything else, treat the tail as an
|
|
2395
|
+
* attribute string (including quoted title).
|
|
2396
|
+
*/
|
|
2397
|
+
attr_start = after_space;
|
|
2398
|
+
url_end = p;
|
|
2399
|
+
break;
|
|
2400
|
+
} else {
|
|
2401
|
+
/* No image attributes: preserve standard Markdown
|
|
2402
|
+
* title parsing so cmark can handle it.
|
|
2403
|
+
*/
|
|
2404
|
+
/* Check if it's a quoted title */
|
|
2405
|
+
if (*after_space == '"' || *after_space == '\'') {
|
|
2406
|
+
/* Found a title - URL ends before this space */
|
|
2407
|
+
url_end = p;
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/* Check if it's a parentheses title: space followed by '(' */
|
|
2412
|
+
if (*after_space == '(') {
|
|
2413
|
+
/* This is a title in parentheses - URL ends before the space */
|
|
2414
|
+
url_end = p;
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
p++;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
/* If no title or attributes found, URL goes to closing paren */
|
|
2424
|
+
if (!url_end) {
|
|
2425
|
+
url_end = paren_end;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
/* If image attributes disabled and we didn't split on known attributes, treat everything as URL */
|
|
2429
|
+
if (!do_image_attrs && !attr_start) {
|
|
2430
|
+
attr_start = NULL;
|
|
2431
|
+
url_end = paren_end;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
if (url_end && url_end > url_start) {
|
|
2435
|
+
/* Extract URL */
|
|
2436
|
+
size_t url_len = url_end - url_start;
|
|
2437
|
+
char *url = malloc(url_len + 1);
|
|
2438
|
+
if (url) {
|
|
2439
|
+
memcpy(url, url_start, url_len);
|
|
2440
|
+
url[url_len] = '\0';
|
|
2441
|
+
|
|
2442
|
+
/* Extract attributes if present (from do_image_attrs split or known-attribute split) */
|
|
2443
|
+
apex_attributes *attrs = NULL;
|
|
2444
|
+
if (attr_start && attr_start < paren_end) {
|
|
2445
|
+
size_t attr_len = paren_end - attr_start;
|
|
2446
|
+
attrs = parse_image_attributes(attr_start, attr_len);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
/* Check for IAL syntax after closing paren: {#id .class} or { width=50% } */
|
|
2450
|
+
const char *ial_end_pos = NULL;
|
|
2451
|
+
const char *after_paren = paren_end + 1;
|
|
2452
|
+
/* Skip whitespace before checking for IAL, but keep original position for skipping */
|
|
2453
|
+
const char *check_pos = after_paren;
|
|
2454
|
+
while (check_pos[0] == ' ' || check_pos[0] == '\t') check_pos++;
|
|
2455
|
+
if (do_image_attrs && check_pos[0] == '{') {
|
|
2456
|
+
/* Find the closing brace */
|
|
2457
|
+
const char *ial_end = strchr(check_pos + 1, '}');
|
|
2458
|
+
if (ial_end) {
|
|
2459
|
+
char second_char = check_pos[1];
|
|
2460
|
+
/* Check if it's a valid IAL format: {: or {# or {. or { (with space/attributes) */
|
|
2461
|
+
bool is_ial = false;
|
|
2462
|
+
const char *content_start = NULL;
|
|
2463
|
+
|
|
2464
|
+
if (second_char == ':' || second_char == '#' || second_char == '.') {
|
|
2465
|
+
/* Kramdown/Pandoc IAL format: {: or {# or {. */
|
|
2466
|
+
is_ial = true;
|
|
2467
|
+
content_start = (second_char == ':') ? check_pos + 2 : check_pos + 1;
|
|
2468
|
+
} else if (second_char == ' ' || second_char == '\t' ||
|
|
2469
|
+
(second_char >= 'a' && second_char <= 'z') ||
|
|
2470
|
+
(second_char >= 'A' && second_char <= 'Z')) {
|
|
2471
|
+
/* Pandoc-style: { width=50% } or {key=val} */
|
|
2472
|
+
is_ial = true;
|
|
2473
|
+
content_start = check_pos + 1;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
if (is_ial && content_start) {
|
|
2477
|
+
int content_len = ial_end - content_start;
|
|
2478
|
+
if (content_len > 0) {
|
|
2479
|
+
/* Try parsing as IAL first (handles #id .class key=val) */
|
|
2480
|
+
apex_attributes *ial_attrs = parse_ial_content(content_start, content_len);
|
|
2481
|
+
if (!ial_attrs || (ial_attrs->attr_count == 0 && !ial_attrs->id && ial_attrs->class_count == 0)) {
|
|
2482
|
+
/* If IAL parsing didn't work, try as image attributes (handles width=50%) */
|
|
2483
|
+
if (ial_attrs) apex_free_attributes(ial_attrs);
|
|
2484
|
+
ial_attrs = parse_image_attributes(content_start, content_len);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
if (ial_attrs && (ial_attrs->attr_count > 0 || ial_attrs->id || ial_attrs->class_count > 0)) {
|
|
2488
|
+
/* Merge IAL attributes with existing attributes */
|
|
2489
|
+
if (attrs) {
|
|
2490
|
+
apex_attributes *merged = merge_attributes(attrs, ial_attrs);
|
|
2491
|
+
apex_free_attributes(attrs);
|
|
2492
|
+
apex_free_attributes(ial_attrs);
|
|
2493
|
+
attrs = merged;
|
|
2494
|
+
} else {
|
|
2495
|
+
attrs = ial_attrs;
|
|
2496
|
+
}
|
|
2497
|
+
} else if (ial_attrs) {
|
|
2498
|
+
apex_free_attributes(ial_attrs);
|
|
2499
|
+
}
|
|
2500
|
+
/* Always skip IAL syntax even if parsing failed, to prevent it appearing in output */
|
|
2501
|
+
ial_end_pos = ial_end;
|
|
2502
|
+
} else {
|
|
2503
|
+
/* Empty IAL - still skip it */
|
|
2504
|
+
ial_end_pos = ial_end;
|
|
2505
|
+
}
|
|
2506
|
+
} else {
|
|
2507
|
+
/* Not a valid IAL format, but if it looks like one (starts with { and ends with }), skip it anyway */
|
|
2508
|
+
/* This handles edge cases where parsing might fail but we still want to skip the syntax */
|
|
2509
|
+
ial_end_pos = ial_end;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/* URL encode the URL only when enabled and URL has no known protocol (http/https/file/x-marked) */
|
|
2515
|
+
bool skip_encode = has_protocol(url);
|
|
2516
|
+
char *encoded_url = (do_url_encoding && !skip_encode) ? url_encode(url) : strdup(url);
|
|
2517
|
+
if (encoded_url) {
|
|
2518
|
+
/* Store attributes with URL - create entry whenever we have attrs (from do_image_attrs or known-attribute split) */
|
|
2519
|
+
image_attr_entry *entry = NULL;
|
|
2520
|
+
if (attrs) {
|
|
2521
|
+
/* Use the running image_index so attributes are
|
|
2522
|
+
* bound to the correct inline image position,
|
|
2523
|
+
* even when some images have no attributes.
|
|
2524
|
+
*/
|
|
2525
|
+
entry = create_image_attr_entry(&local_img_attrs, encoded_url, image_index);
|
|
2526
|
+
if (entry) {
|
|
2527
|
+
/* Copy attributes (don't merge) */
|
|
2528
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
2529
|
+
add_attribute(entry->attrs, attrs->keys[i], attrs->values[i]);
|
|
2530
|
+
}
|
|
2531
|
+
if (attrs->id) {
|
|
2532
|
+
entry->attrs->id = strdup(attrs->id);
|
|
2533
|
+
}
|
|
2534
|
+
for (int i = 0; i < attrs->class_count; i++) {
|
|
2535
|
+
add_class(entry->attrs, attrs->classes[i]);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
/* Write the image syntax up to URL */
|
|
2541
|
+
size_t prefix_len = url_start - img_start;
|
|
2542
|
+
if (prefix_len < remaining) {
|
|
2543
|
+
memcpy(write, img_start, prefix_len);
|
|
2544
|
+
write += prefix_len;
|
|
2545
|
+
remaining -= prefix_len;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
/* Write encoded URL */
|
|
2549
|
+
size_t encoded_len = strlen(encoded_url);
|
|
2550
|
+
if (encoded_len < remaining) {
|
|
2551
|
+
memcpy(write, encoded_url, encoded_len);
|
|
2552
|
+
write += encoded_len;
|
|
2553
|
+
remaining -= encoded_len;
|
|
2554
|
+
} else {
|
|
2555
|
+
/* Buffer too small, expand */
|
|
2556
|
+
size_t written = write - output;
|
|
2557
|
+
capacity = (written + encoded_len + 1) * 2;
|
|
2558
|
+
char *new_output = realloc(output, capacity);
|
|
2559
|
+
if (!new_output) {
|
|
2560
|
+
free(output);
|
|
2561
|
+
free(url);
|
|
2562
|
+
free(encoded_url);
|
|
2563
|
+
if (attrs) apex_free_attributes(attrs);
|
|
2564
|
+
apex_free_image_attributes(local_img_attrs);
|
|
2565
|
+
return NULL;
|
|
2566
|
+
}
|
|
2567
|
+
output = new_output;
|
|
2568
|
+
write = output + written;
|
|
2569
|
+
remaining = capacity - written;
|
|
2570
|
+
memcpy(write, encoded_url, encoded_len);
|
|
2571
|
+
write += encoded_len;
|
|
2572
|
+
remaining -= encoded_len;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
/* Write the rest: when we split URL from attributes (do_image_attrs or known-attribute),
|
|
2576
|
+
* omit the attribute tail so cmark only sees . Otherwise preserve tail for cmark.
|
|
2577
|
+
*/
|
|
2578
|
+
const char *rest_start = url_end;
|
|
2579
|
+
const char *rest_end = paren_end;
|
|
2580
|
+
if (attr_start && attr_start < paren_end) {
|
|
2581
|
+
/* Drop everything from attr_start onward */
|
|
2582
|
+
rest_end = attr_start;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
while (rest_start < rest_end) {
|
|
2586
|
+
if (remaining > 0) {
|
|
2587
|
+
*write++ = *rest_start++;
|
|
2588
|
+
remaining--;
|
|
2589
|
+
} else {
|
|
2590
|
+
/* Buffer too small, need to expand */
|
|
2591
|
+
size_t written = write - output;
|
|
2592
|
+
size_t rest_len = paren_end - rest_start;
|
|
2593
|
+
capacity = (written + rest_len + 1) * 2;
|
|
2594
|
+
char *new_output = realloc(output, capacity);
|
|
2595
|
+
if (!new_output) {
|
|
2596
|
+
free(output);
|
|
2597
|
+
free(url);
|
|
2598
|
+
free(encoded_url);
|
|
2599
|
+
if (attrs) apex_free_attributes(attrs);
|
|
2600
|
+
apex_free_image_attributes(local_img_attrs);
|
|
2601
|
+
return NULL;
|
|
2602
|
+
}
|
|
2603
|
+
output = new_output;
|
|
2604
|
+
write = output + written;
|
|
2605
|
+
remaining = capacity - written;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
/* Write closing ) */
|
|
2610
|
+
if (remaining > 0 && paren_end && *paren_end == ')') {
|
|
2611
|
+
*write++ = ')';
|
|
2612
|
+
remaining--;
|
|
2613
|
+
|
|
2614
|
+
/* Skip IAL if it was found and processed */
|
|
2615
|
+
if (ial_end_pos) {
|
|
2616
|
+
/* Skip from after_paren (which includes any whitespace before IAL) to after the closing brace */
|
|
2617
|
+
/* ial_end_pos points to the closing '}', so we skip to after it */
|
|
2618
|
+
read = ial_end_pos + 1;
|
|
2619
|
+
} else {
|
|
2620
|
+
read = paren_end + 1;
|
|
2621
|
+
}
|
|
2622
|
+
} else {
|
|
2623
|
+
/* No closing paren found, but still need to skip IAL if present */
|
|
2624
|
+
if (ial_end_pos) {
|
|
2625
|
+
read = ial_end_pos + 1;
|
|
2626
|
+
} else {
|
|
2627
|
+
read = paren_end;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
/* We successfully processed an inline image (with or
|
|
2632
|
+
* without attributes). Bump image_index so subsequent
|
|
2633
|
+
* images get distinct positions that match the AST.
|
|
2634
|
+
*/
|
|
2635
|
+
image_index++;
|
|
2636
|
+
|
|
2637
|
+
free(encoded_url);
|
|
2638
|
+
if (attrs) apex_free_attributes(attrs);
|
|
2639
|
+
}
|
|
2640
|
+
free(url);
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
} else {
|
|
2644
|
+
/* Malformed inline image - skip just the ! and continue */
|
|
2645
|
+
read = img_start + 1;
|
|
2646
|
+
continue;
|
|
2647
|
+
}
|
|
2648
|
+
} else {
|
|
2649
|
+
/* Not an inline image - might be reference-style ![ref][id] */
|
|
2650
|
+
/* Pass through unchanged by copying the ![ */
|
|
2651
|
+
if (remaining > 0) {
|
|
2652
|
+
*write++ = *read++;
|
|
2653
|
+
remaining--;
|
|
2654
|
+
if (remaining > 0 && *read == '[') {
|
|
2655
|
+
*write++ = *read++;
|
|
2656
|
+
remaining--;
|
|
2657
|
+
}
|
|
2658
|
+
} else {
|
|
2659
|
+
read++;
|
|
2660
|
+
}
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
/* Look for reference-style link definitions: [ref]: url attributes */
|
|
2666
|
+
/* Process to URL-encode URLs and extract image attributes if present */
|
|
2667
|
+
if (*read == '[') {
|
|
2668
|
+
const char *ref_start = read;
|
|
2669
|
+
const char *ref_end = strchr(ref_start, ']');
|
|
2670
|
+
if (ref_end && ref_end[1] == ':' && (ref_end[2] == ' ' || ref_end[2] == '\t')) {
|
|
2671
|
+
/* Found [ref]: */
|
|
2672
|
+
const char *url_start = ref_end + 2;
|
|
2673
|
+
/* Skip whitespace */
|
|
2674
|
+
while (*url_start && (*url_start == ' ' || *url_start == '\t')) url_start++;
|
|
2675
|
+
|
|
2676
|
+
const char *p = url_start;
|
|
2677
|
+
const char *url_end = NULL;
|
|
2678
|
+
const char *attr_start = NULL;
|
|
2679
|
+
|
|
2680
|
+
/* Find the end of the line first */
|
|
2681
|
+
const char *line_end = p;
|
|
2682
|
+
while (*line_end && *line_end != '\n' && *line_end != '\r') line_end++;
|
|
2683
|
+
|
|
2684
|
+
/* For reference definitions, we need to check for:
|
|
2685
|
+
* 1. Image attributes (if do_image_attrs): key=value pattern
|
|
2686
|
+
* 2. Title: quoted string ("title" or 'title')
|
|
2687
|
+
* We should stop at whichever comes first (attributes or title)
|
|
2688
|
+
*/
|
|
2689
|
+
p = url_start;
|
|
2690
|
+
while (p < line_end) {
|
|
2691
|
+
if (*p == ' ' || *p == '\t') {
|
|
2692
|
+
const char *after_space = p;
|
|
2693
|
+
while (after_space < line_end && (*after_space == ' ' || *after_space == '\t')) after_space++;
|
|
2694
|
+
|
|
2695
|
+
if (after_space < line_end) {
|
|
2696
|
+
/* Space + key= or bare @2x/@3x: split so attributes are applied (regardless of do_image_attrs) */
|
|
2697
|
+
if (looks_like_attr_key_equals(after_space, line_end)) {
|
|
2698
|
+
attr_start = after_space;
|
|
2699
|
+
url_end = p;
|
|
2700
|
+
break;
|
|
2701
|
+
}
|
|
2702
|
+
/* Bare @2x/@3x (retina srcset) - not \w+= but we treat as attribute */
|
|
2703
|
+
if ((size_t)(line_end - after_space) >= 3 &&
|
|
2704
|
+
after_space[0] == '@' &&
|
|
2705
|
+
((after_space[1] == '2' && after_space[2] == 'x') ||
|
|
2706
|
+
(after_space[1] == '3' && after_space[2] == 'x')) &&
|
|
2707
|
+
(after_space + 3 >= line_end || isspace((unsigned char)after_space[3]))) {
|
|
2708
|
+
attr_start = after_space;
|
|
2709
|
+
url_end = p;
|
|
2710
|
+
break;
|
|
2711
|
+
}
|
|
2712
|
+
/* Check if it's a quoted title */
|
|
2713
|
+
if (*after_space == '"' || *after_space == '\'') {
|
|
2714
|
+
/* Found a title - URL ends before this space */
|
|
2715
|
+
url_end = p;
|
|
2716
|
+
break;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
/* Check if it's a parentheses title: space followed by '(' */
|
|
2720
|
+
if (*after_space == '(') {
|
|
2721
|
+
/* This is a title in parentheses - URL ends before the space */
|
|
2722
|
+
url_end = p;
|
|
2723
|
+
break;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
/* Check if it's attributes (for images) */
|
|
2727
|
+
/* Check for IAL syntax: { */
|
|
2728
|
+
if (do_image_attrs && *after_space == '{') {
|
|
2729
|
+
/* Found IAL syntax - URL ends before this space */
|
|
2730
|
+
attr_start = after_space;
|
|
2731
|
+
url_end = p;
|
|
2732
|
+
break;
|
|
2733
|
+
}
|
|
2734
|
+
/* Check for key= pattern (inline attributes) */
|
|
2735
|
+
if (do_image_attrs && looks_like_attribute_start(after_space, line_end)) {
|
|
2736
|
+
/* This looks like attributes */
|
|
2737
|
+
attr_start = after_space;
|
|
2738
|
+
url_end = p;
|
|
2739
|
+
break;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
p++;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
if (!url_end) {
|
|
2747
|
+
url_end = line_end; /* URL ends at newline (no title or attributes found) */
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
if (url_end > url_start) {
|
|
2751
|
+
/* Extract URL */
|
|
2752
|
+
size_t url_len = url_end - url_start;
|
|
2753
|
+
char *url = malloc(url_len + 1);
|
|
2754
|
+
if (url) {
|
|
2755
|
+
memcpy(url, url_start, url_len);
|
|
2756
|
+
url[url_len] = '\0';
|
|
2757
|
+
|
|
2758
|
+
/* Extract attributes if present (from do_image_attrs or known-attribute split) */
|
|
2759
|
+
apex_attributes *attrs = NULL;
|
|
2760
|
+
const char *title_end = NULL;
|
|
2761
|
+
char *title_text = NULL;
|
|
2762
|
+
bool found_ial = false;
|
|
2763
|
+
if (attr_start) {
|
|
2764
|
+
const char *attr_end = p;
|
|
2765
|
+
while (attr_end < line_end && *attr_end != '\n' && *attr_end != '\r') attr_end++;
|
|
2766
|
+
size_t attr_len = attr_end - attr_start;
|
|
2767
|
+
attrs = parse_image_attributes(attr_start, attr_len);
|
|
2768
|
+
title_end = attr_end;
|
|
2769
|
+
} else if (url_end < line_end) {
|
|
2770
|
+
/* Check if there's a title after the URL */
|
|
2771
|
+
const char *after_url = url_end;
|
|
2772
|
+
while (after_url < line_end && (*after_url == ' ' || *after_url == '\t')) after_url++;
|
|
2773
|
+
|
|
2774
|
+
/* Check for quoted title */
|
|
2775
|
+
if (after_url < line_end && (*after_url == '"' || *after_url == '\'')) {
|
|
2776
|
+
char quote = *after_url;
|
|
2777
|
+
const char *title_start = after_url + 1;
|
|
2778
|
+
const char *title_close = title_start;
|
|
2779
|
+
while (title_close < line_end && *title_close != quote) {
|
|
2780
|
+
if (*title_close == '\\' && title_close + 1 < line_end) title_close++;
|
|
2781
|
+
title_close++;
|
|
2782
|
+
}
|
|
2783
|
+
if (title_close < line_end && *title_close == quote) {
|
|
2784
|
+
/* Extract title text */
|
|
2785
|
+
size_t title_len = title_close - title_start;
|
|
2786
|
+
if (title_len > 0) {
|
|
2787
|
+
title_text = malloc(title_len + 1);
|
|
2788
|
+
if (title_text) {
|
|
2789
|
+
memcpy(title_text, title_start, title_len);
|
|
2790
|
+
title_text[title_len] = '\0';
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
title_end = title_close + 1;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
/* Check for IAL after title: {#id .class} or { width=50% } */
|
|
2798
|
+
if (title_end && do_image_attrs && title_end < line_end) {
|
|
2799
|
+
const char *after_title = title_end;
|
|
2800
|
+
while (after_title < line_end && (*after_title == ' ' || *after_title == '\t')) after_title++;
|
|
2801
|
+
|
|
2802
|
+
if (after_title < line_end && *after_title == '{') {
|
|
2803
|
+
/* Find the closing brace */
|
|
2804
|
+
const char *ial_end = strchr(after_title + 1, '}');
|
|
2805
|
+
if (ial_end && ial_end <= line_end) {
|
|
2806
|
+
char second_char = after_title[1];
|
|
2807
|
+
/* Check if it's a valid IAL format: {: or {# or {. or { (with space/attributes) */
|
|
2808
|
+
bool is_ial = false;
|
|
2809
|
+
const char *content_start = NULL;
|
|
2810
|
+
|
|
2811
|
+
if (second_char == ':' || second_char == '#' || second_char == '.') {
|
|
2812
|
+
/* Kramdown/Pandoc IAL format: {: or {# or {. */
|
|
2813
|
+
is_ial = true;
|
|
2814
|
+
content_start = (second_char == ':') ? after_title + 2 : after_title + 1;
|
|
2815
|
+
} else if (second_char == ' ' || second_char == '\t' ||
|
|
2816
|
+
(second_char >= 'a' && second_char <= 'z') ||
|
|
2817
|
+
(second_char >= 'A' && second_char <= 'Z')) {
|
|
2818
|
+
/* Pandoc-style: { width=50% } or {key=val} */
|
|
2819
|
+
is_ial = true;
|
|
2820
|
+
content_start = after_title + 1;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
if (is_ial && content_start) {
|
|
2824
|
+
int content_len = ial_end - content_start;
|
|
2825
|
+
if (content_len > 0) {
|
|
2826
|
+
/* Try parsing as IAL first (handles #id .class key=val) */
|
|
2827
|
+
apex_attributes *ial_attrs = parse_ial_content(content_start, content_len);
|
|
2828
|
+
if (!ial_attrs || (ial_attrs->attr_count == 0 && !ial_attrs->id && ial_attrs->class_count == 0)) {
|
|
2829
|
+
/* If IAL parsing didn't work, try as image attributes (handles width=50%) */
|
|
2830
|
+
if (ial_attrs) apex_free_attributes(ial_attrs);
|
|
2831
|
+
ial_attrs = parse_image_attributes(content_start, content_len);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
if (ial_attrs && (ial_attrs->attr_count > 0 || ial_attrs->id || ial_attrs->class_count > 0)) {
|
|
2835
|
+
/* Create or merge with existing attributes */
|
|
2836
|
+
if (!attrs) {
|
|
2837
|
+
attrs = ial_attrs;
|
|
2838
|
+
} else {
|
|
2839
|
+
apex_attributes *merged = merge_attributes(attrs, ial_attrs);
|
|
2840
|
+
apex_free_attributes(attrs);
|
|
2841
|
+
apex_free_attributes(ial_attrs);
|
|
2842
|
+
attrs = merged;
|
|
2843
|
+
}
|
|
2844
|
+
title_end = ial_end + 1; /* Update end position to skip IAL */
|
|
2845
|
+
found_ial = true;
|
|
2846
|
+
} else if (ial_attrs) {
|
|
2847
|
+
apex_free_attributes(ial_attrs);
|
|
2848
|
+
title_end = ial_end + 1;
|
|
2849
|
+
found_ial = true;
|
|
2850
|
+
} else {
|
|
2851
|
+
/* Even if parsing failed, skip the IAL syntax to prevent it appearing in output */
|
|
2852
|
+
title_end = ial_end + 1;
|
|
2853
|
+
found_ial = true;
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
/* Check for MultiMarkdown-style attributes after title:
|
|
2862
|
+
* [id]: url "Title" class=center width=300
|
|
2863
|
+
* These are parsed with the same image-attribute parser used for inline images.
|
|
2864
|
+
*/
|
|
2865
|
+
if (title_end && do_image_attrs && title_end < line_end) {
|
|
2866
|
+
const char *after_title_attrs = title_end;
|
|
2867
|
+
while (after_title_attrs < line_end &&
|
|
2868
|
+
(*after_title_attrs == ' ' || *after_title_attrs == '\t')) {
|
|
2869
|
+
after_title_attrs++;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
/* If there's remaining content and it doesn't start an IAL block,
|
|
2873
|
+
* treat it as a sequence of key=value attributes (MultiMarkdown style).
|
|
2874
|
+
*/
|
|
2875
|
+
if (after_title_attrs < line_end && *after_title_attrs != '{') {
|
|
2876
|
+
const char *attr_end = line_end;
|
|
2877
|
+
size_t attr_len = attr_end - after_title_attrs;
|
|
2878
|
+
apex_attributes *mmd_attrs = parse_image_attributes(after_title_attrs, (int)attr_len);
|
|
2879
|
+
|
|
2880
|
+
if (mmd_attrs &&
|
|
2881
|
+
(mmd_attrs->attr_count > 0 || mmd_attrs->id || mmd_attrs->class_count > 0)) {
|
|
2882
|
+
if (!attrs) {
|
|
2883
|
+
attrs = mmd_attrs;
|
|
2884
|
+
} else {
|
|
2885
|
+
apex_attributes *merged = merge_attributes(attrs, mmd_attrs);
|
|
2886
|
+
apex_free_attributes(attrs);
|
|
2887
|
+
apex_free_attributes(mmd_attrs);
|
|
2888
|
+
attrs = merged;
|
|
2889
|
+
}
|
|
2890
|
+
/* We consumed the rest of the line as attributes */
|
|
2891
|
+
title_end = line_end;
|
|
2892
|
+
found_ial = true;
|
|
2893
|
+
} else if (mmd_attrs) {
|
|
2894
|
+
apex_free_attributes(mmd_attrs);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
/* Also check for IAL directly after URL if no title was found */
|
|
2900
|
+
if (!title_end && do_image_attrs && url_end < line_end) {
|
|
2901
|
+
const char *after_url = url_end;
|
|
2902
|
+
while (after_url < line_end && (*after_url == ' ' || *after_url == '\t')) after_url++;
|
|
2903
|
+
|
|
2904
|
+
if (after_url < line_end && *after_url == '{') {
|
|
2905
|
+
/* Find the closing brace */
|
|
2906
|
+
const char *ial_end = strchr(after_url + 1, '}');
|
|
2907
|
+
if (ial_end && ial_end <= line_end) {
|
|
2908
|
+
char second_char = after_url[1];
|
|
2909
|
+
bool is_ial = false;
|
|
2910
|
+
const char *content_start = NULL;
|
|
2911
|
+
|
|
2912
|
+
if (second_char == ':' || second_char == '#' || second_char == '.') {
|
|
2913
|
+
is_ial = true;
|
|
2914
|
+
content_start = (second_char == ':') ? after_url + 2 : after_url + 1;
|
|
2915
|
+
} else if (second_char == ' ' || second_char == '\t' ||
|
|
2916
|
+
(second_char >= 'a' && second_char <= 'z') ||
|
|
2917
|
+
(second_char >= 'A' && second_char <= 'Z')) {
|
|
2918
|
+
is_ial = true;
|
|
2919
|
+
content_start = after_url + 1;
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
if (is_ial && content_start) {
|
|
2923
|
+
int content_len = ial_end - content_start;
|
|
2924
|
+
if (content_len > 0) {
|
|
2925
|
+
apex_attributes *ial_attrs = parse_ial_content(content_start, content_len);
|
|
2926
|
+
if (!ial_attrs || (ial_attrs->attr_count == 0 && !ial_attrs->id && ial_attrs->class_count == 0)) {
|
|
2927
|
+
if (ial_attrs) apex_free_attributes(ial_attrs);
|
|
2928
|
+
ial_attrs = parse_image_attributes(content_start, content_len);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
if (ial_attrs && (ial_attrs->attr_count > 0 || ial_attrs->id || ial_attrs->class_count > 0)) {
|
|
2932
|
+
attrs = ial_attrs;
|
|
2933
|
+
title_end = ial_end + 1;
|
|
2934
|
+
found_ial = true;
|
|
2935
|
+
} else if (ial_attrs) {
|
|
2936
|
+
apex_free_attributes(ial_attrs);
|
|
2937
|
+
title_end = ial_end + 1;
|
|
2938
|
+
found_ial = true;
|
|
2939
|
+
} else {
|
|
2940
|
+
title_end = ial_end + 1;
|
|
2941
|
+
found_ial = true;
|
|
2942
|
+
}
|
|
2943
|
+
} else {
|
|
2944
|
+
title_end = ial_end + 1;
|
|
2945
|
+
}
|
|
2946
|
+
} else {
|
|
2947
|
+
title_end = ial_end + 1;
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
/* Extract reference name */
|
|
2955
|
+
size_t ref_name_len = ref_end - ref_start - 1; /* Exclude [ and ] */
|
|
2956
|
+
char *ref_name = malloc(ref_name_len + 1);
|
|
2957
|
+
if (ref_name) {
|
|
2958
|
+
memcpy(ref_name, ref_start + 1, ref_name_len);
|
|
2959
|
+
ref_name[ref_name_len] = '\0';
|
|
2960
|
+
/* Trim whitespace from reference name */
|
|
2961
|
+
char *p = ref_name;
|
|
2962
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
2963
|
+
if (p > ref_name) {
|
|
2964
|
+
memmove(ref_name, p, strlen(p) + 1);
|
|
2965
|
+
}
|
|
2966
|
+
p = ref_name + strlen(ref_name) - 1;
|
|
2967
|
+
while (p >= ref_name && isspace((unsigned char)*p)) {
|
|
2968
|
+
*p = '\0';
|
|
2969
|
+
p--;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
/* Detect footnote-style reference: [^id]: ... */
|
|
2974
|
+
bool is_footnote_ref = false;
|
|
2975
|
+
if (ref_name && ref_name_len > 0) {
|
|
2976
|
+
const char *name_p = ref_name;
|
|
2977
|
+
while (*name_p == ' ' || *name_p == '\t') name_p++;
|
|
2978
|
+
if (*name_p == '^') {
|
|
2979
|
+
is_footnote_ref = true;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
/* Footnote definitions should not have their "URL" (the footnote text)
|
|
2984
|
+
* percent-encoded. Copy the line as-is and skip URL encoding.
|
|
2985
|
+
*/
|
|
2986
|
+
if (is_footnote_ref) {
|
|
2987
|
+
/* Write the entire definition line unchanged */
|
|
2988
|
+
size_t line_len = line_end - ref_start;
|
|
2989
|
+
if (*line_end == '\n') {
|
|
2990
|
+
line_len++; /* Include newline */
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
if (line_len > remaining) {
|
|
2994
|
+
size_t written = write - output;
|
|
2995
|
+
size_t new_capacity = (written + line_len + 1) * 2;
|
|
2996
|
+
char *new_output = realloc(output, new_capacity);
|
|
2997
|
+
if (!new_output) {
|
|
2998
|
+
free(output);
|
|
2999
|
+
free(url);
|
|
3000
|
+
free(ref_name);
|
|
3001
|
+
if (attrs) apex_free_attributes(attrs);
|
|
3002
|
+
apex_free_image_attributes(local_img_attrs);
|
|
3003
|
+
return NULL;
|
|
3004
|
+
}
|
|
3005
|
+
output = new_output;
|
|
3006
|
+
write = output + written;
|
|
3007
|
+
remaining = new_capacity - written;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
memcpy(write, ref_start, line_len);
|
|
3011
|
+
write += line_len;
|
|
3012
|
+
remaining -= line_len;
|
|
3013
|
+
|
|
3014
|
+
/* Advance read past this line (including newline if present) */
|
|
3015
|
+
const char *next = line_end;
|
|
3016
|
+
if (*next == '\n') {
|
|
3017
|
+
next++;
|
|
3018
|
+
} else if (*next == '\r') {
|
|
3019
|
+
if (next[1] == '\n') {
|
|
3020
|
+
next += 2;
|
|
3021
|
+
} else {
|
|
3022
|
+
next++;
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
read = next;
|
|
3026
|
+
|
|
3027
|
+
free(url);
|
|
3028
|
+
free(ref_name);
|
|
3029
|
+
if (title_text) free(title_text);
|
|
3030
|
+
if (attrs) apex_free_attributes(attrs);
|
|
3031
|
+
continue;
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
/* URL encode the URL only when enabled and URL has no known protocol */
|
|
3035
|
+
bool skip_encode_ref = has_protocol(url);
|
|
3036
|
+
char *encoded_url = (do_url_encoding && !skip_encode_ref) ? url_encode(url) : strdup(url);
|
|
3037
|
+
if (encoded_url) {
|
|
3038
|
+
bool has_image_attrs = (attrs != NULL && attrs_are_image_specific(attrs));
|
|
3039
|
+
/* If has image-specific attributes (width, height, etc.), store with reference name.
|
|
3040
|
+
* Don't create entry for link refs with only a title (e.g. [ref]: url "title"). */
|
|
3041
|
+
if (ref_name) {
|
|
3042
|
+
if (has_image_attrs || (do_image_attrs && found_ial)) {
|
|
3043
|
+
image_attr_entry *entry = create_image_attr_entry_with_ref(&local_img_attrs, encoded_url, ref_name);
|
|
3044
|
+
if (entry) {
|
|
3045
|
+
if (attrs) {
|
|
3046
|
+
/* Copy attributes (don't merge) */
|
|
3047
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
3048
|
+
add_attribute(entry->attrs, attrs->keys[i], attrs->values[i]);
|
|
3049
|
+
}
|
|
3050
|
+
if (attrs->id) {
|
|
3051
|
+
entry->attrs->id = strdup(attrs->id);
|
|
3052
|
+
}
|
|
3053
|
+
for (int i = 0; i < attrs->class_count; i++) {
|
|
3054
|
+
add_class(entry->attrs, attrs->classes[i]);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
/* Add title if present */
|
|
3058
|
+
if (title_text) {
|
|
3059
|
+
add_attribute(entry->attrs, "title", title_text);
|
|
3060
|
+
}
|
|
3061
|
+
/* Entry created - will be used for expansion */
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
/* If this reference definition has image-specific attributes, remove it so we expand and apply attrs */
|
|
3066
|
+
bool created_entry = (ref_name && (has_image_attrs || (do_image_attrs && found_ial)));
|
|
3067
|
+
bool should_remove = created_entry;
|
|
3068
|
+
|
|
3069
|
+
if (should_remove) {
|
|
3070
|
+
/* Reference definitions with attributes are removed from output (like ALDs) */
|
|
3071
|
+
/* Skip the entire line - don't write anything back */
|
|
3072
|
+
free(ref_name);
|
|
3073
|
+
if (title_text) free(title_text);
|
|
3074
|
+
const char *p = line_end;
|
|
3075
|
+
/* Skip the newline */
|
|
3076
|
+
if (*p == '\n') {
|
|
3077
|
+
p++;
|
|
3078
|
+
} else if (*p == '\r') {
|
|
3079
|
+
if (p[1] == '\n') {
|
|
3080
|
+
p += 2;
|
|
3081
|
+
} else {
|
|
3082
|
+
p++;
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
read = p;
|
|
3086
|
+
} else {
|
|
3087
|
+
/* Write back the reference definition with encoded URL (so cmark can resolve it) */
|
|
3088
|
+
/* Write the reference up to URL */
|
|
3089
|
+
size_t prefix_len = url_start - ref_start;
|
|
3090
|
+
if (prefix_len < remaining) {
|
|
3091
|
+
memcpy(write, ref_start, prefix_len);
|
|
3092
|
+
write += prefix_len;
|
|
3093
|
+
remaining -= prefix_len;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
/* Write encoded URL */
|
|
3097
|
+
size_t encoded_len = strlen(encoded_url);
|
|
3098
|
+
if (encoded_len < remaining) {
|
|
3099
|
+
memcpy(write, encoded_url, encoded_len);
|
|
3100
|
+
write += encoded_len;
|
|
3101
|
+
remaining -= encoded_len;
|
|
3102
|
+
} else {
|
|
3103
|
+
size_t written = write - output;
|
|
3104
|
+
capacity = (written + encoded_len + 1) * 2;
|
|
3105
|
+
char *new_output = realloc(output, capacity);
|
|
3106
|
+
if (!new_output) {
|
|
3107
|
+
free(output);
|
|
3108
|
+
free(url);
|
|
3109
|
+
free(encoded_url);
|
|
3110
|
+
free(ref_name);
|
|
3111
|
+
if (attrs) apex_free_attributes(attrs);
|
|
3112
|
+
apex_free_image_attributes(local_img_attrs);
|
|
3113
|
+
return NULL;
|
|
3114
|
+
}
|
|
3115
|
+
output = new_output;
|
|
3116
|
+
write = output + written;
|
|
3117
|
+
remaining = capacity - written;
|
|
3118
|
+
memcpy(write, encoded_url, encoded_len);
|
|
3119
|
+
write += encoded_len;
|
|
3120
|
+
remaining -= encoded_len;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/* Write the rest (title if present, but skip IAL if it was processed) */
|
|
3124
|
+
const char *rest_end = title_end ? title_end : line_end;
|
|
3125
|
+
const char *rest_start = url_end;
|
|
3126
|
+
while (rest_start < rest_end) {
|
|
3127
|
+
if (remaining > 0) {
|
|
3128
|
+
*write++ = *rest_start++;
|
|
3129
|
+
remaining--;
|
|
3130
|
+
} else {
|
|
3131
|
+
size_t written = write - output;
|
|
3132
|
+
size_t rest_len = rest_end - rest_start;
|
|
3133
|
+
capacity = (written + rest_len + 1) * 2;
|
|
3134
|
+
char *new_output = realloc(output, capacity);
|
|
3135
|
+
if (!new_output) {
|
|
3136
|
+
free(output);
|
|
3137
|
+
free(url);
|
|
3138
|
+
free(encoded_url);
|
|
3139
|
+
free(ref_name);
|
|
3140
|
+
if (attrs) apex_free_attributes(attrs);
|
|
3141
|
+
apex_free_image_attributes(local_img_attrs);
|
|
3142
|
+
return NULL;
|
|
3143
|
+
}
|
|
3144
|
+
output = new_output;
|
|
3145
|
+
write = output + written;
|
|
3146
|
+
remaining = capacity - written;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
/* Advance read past the line (including IAL if it was processed) */
|
|
3151
|
+
const char *p = title_end ? title_end : line_end;
|
|
3152
|
+
|
|
3153
|
+
/* Write newline */
|
|
3154
|
+
if (*p == '\n' && remaining > 0) {
|
|
3155
|
+
*write++ = *p++;
|
|
3156
|
+
remaining--;
|
|
3157
|
+
} else if (*p == '\n') {
|
|
3158
|
+
p++;
|
|
3159
|
+
} else if (*p == '\r') {
|
|
3160
|
+
if (p[1] == '\n' && remaining >= 2) {
|
|
3161
|
+
*write++ = *p++;
|
|
3162
|
+
*write++ = *p++;
|
|
3163
|
+
remaining -= 2;
|
|
3164
|
+
} else if (remaining > 0) {
|
|
3165
|
+
*write++ = *p++;
|
|
3166
|
+
remaining--;
|
|
3167
|
+
} else {
|
|
3168
|
+
p++;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
read = p;
|
|
3173
|
+
free(ref_name);
|
|
3174
|
+
}
|
|
3175
|
+
free(encoded_url);
|
|
3176
|
+
if (attrs) apex_free_attributes(attrs);
|
|
3177
|
+
}
|
|
3178
|
+
free(url);
|
|
3179
|
+
continue;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
/* Look for regular links: [text](url) or [text](url "title") - URL encode only the URL */
|
|
3186
|
+
/* Skip when link text starts with "![": that's [![image]...], process as image when we hit '!' */
|
|
3187
|
+
if (*read == '[' && (read == text || read[-1] != '!')) {
|
|
3188
|
+
const char *link_start = read;
|
|
3189
|
+
const char *link_text_end = strchr(link_start, ']');
|
|
3190
|
+
if (link_text_end && link_text_end[1] == '(' &&
|
|
3191
|
+
!(link_text_end > link_start + 2 && link_start[1] == '!' && link_start[2] == '[')) {
|
|
3192
|
+
/* Found [text]\( and not [![image]...] */
|
|
3193
|
+
const char *url_start = link_text_end + 2; /* After ]( */
|
|
3194
|
+
const char *p = url_start;
|
|
3195
|
+
const char *url_end = NULL;
|
|
3196
|
+
const char *paren_end = NULL;
|
|
3197
|
+
|
|
3198
|
+
/* Find the closing paren first */
|
|
3199
|
+
while (*p && *p != ')' && *p != '\n') p++;
|
|
3200
|
+
if (*p == ')') {
|
|
3201
|
+
paren_end = p;
|
|
3202
|
+
} else {
|
|
3203
|
+
paren_end = p; /* End at newline or end of string */
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
/* Scan forward looking for titles: "title", 'title', or (title) */
|
|
3207
|
+
/* Key insight: (title) has a space before the '(', while URL parentheses don't */
|
|
3208
|
+
p = url_start;
|
|
3209
|
+
while (p < paren_end) {
|
|
3210
|
+
if (*p == ' ' || *p == '\t') {
|
|
3211
|
+
/* Found a space - check what follows */
|
|
3212
|
+
const char *after_space = p;
|
|
3213
|
+
while (after_space < paren_end && (*after_space == ' ' || *after_space == '\t')) after_space++;
|
|
3214
|
+
|
|
3215
|
+
if (after_space < paren_end) {
|
|
3216
|
+
/* Check if it's a quoted title */
|
|
3217
|
+
if (*after_space == '"' || *after_space == '\'') {
|
|
3218
|
+
/* Found a title - URL ends before this space */
|
|
3219
|
+
url_end = p;
|
|
3220
|
+
break;
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
/* Check if it's a parentheses title: space followed by '(' */
|
|
3224
|
+
if (*after_space == '(') {
|
|
3225
|
+
/* This is a title in parentheses - URL ends before the space */
|
|
3226
|
+
url_end = p;
|
|
3227
|
+
break;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
p++;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
/* If no title found, URL goes to closing paren */
|
|
3235
|
+
if (!url_end) {
|
|
3236
|
+
url_end = paren_end;
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
if (url_end > url_start) {
|
|
3240
|
+
/* Extract URL */
|
|
3241
|
+
size_t url_len = url_end - url_start;
|
|
3242
|
+
char *url = malloc(url_len + 1);
|
|
3243
|
+
if (url) {
|
|
3244
|
+
memcpy(url, url_start, url_len);
|
|
3245
|
+
url[url_len] = '\0';
|
|
3246
|
+
|
|
3247
|
+
/* URL encode (if enabled) */
|
|
3248
|
+
char *encoded_url = do_url_encoding ? url_encode(url) : strdup(url);
|
|
3249
|
+
if (encoded_url) {
|
|
3250
|
+
/* Write link prefix */
|
|
3251
|
+
size_t prefix_len = url_start - link_start;
|
|
3252
|
+
if (prefix_len < remaining) {
|
|
3253
|
+
memcpy(write, link_start, prefix_len);
|
|
3254
|
+
write += prefix_len;
|
|
3255
|
+
remaining -= prefix_len;
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
/* Write encoded URL */
|
|
3259
|
+
size_t encoded_len = strlen(encoded_url);
|
|
3260
|
+
if (encoded_len < remaining) {
|
|
3261
|
+
memcpy(write, encoded_url, encoded_len);
|
|
3262
|
+
write += encoded_len;
|
|
3263
|
+
remaining -= encoded_len;
|
|
3264
|
+
} else {
|
|
3265
|
+
size_t written = write - output;
|
|
3266
|
+
capacity = (written + encoded_len + 1) * 2;
|
|
3267
|
+
char *new_output = realloc(output, capacity);
|
|
3268
|
+
if (!new_output) {
|
|
3269
|
+
free(output);
|
|
3270
|
+
free(url);
|
|
3271
|
+
free(encoded_url);
|
|
3272
|
+
apex_free_image_attributes(local_img_attrs);
|
|
3273
|
+
return NULL;
|
|
3274
|
+
}
|
|
3275
|
+
output = new_output;
|
|
3276
|
+
write = output + written;
|
|
3277
|
+
remaining = capacity - written;
|
|
3278
|
+
memcpy(write, encoded_url, encoded_len);
|
|
3279
|
+
write += encoded_len;
|
|
3280
|
+
remaining -= encoded_len;
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
/* Write the rest (title if present) */
|
|
3284
|
+
const char *rest_start = url_end;
|
|
3285
|
+
while (rest_start < paren_end) {
|
|
3286
|
+
if (remaining > 0) {
|
|
3287
|
+
*write++ = *rest_start++;
|
|
3288
|
+
remaining--;
|
|
3289
|
+
} else {
|
|
3290
|
+
/* Buffer too small, need to expand */
|
|
3291
|
+
size_t written = write - output;
|
|
3292
|
+
size_t rest_len = paren_end - rest_start;
|
|
3293
|
+
capacity = (written + rest_len + 1) * 2;
|
|
3294
|
+
char *new_output = realloc(output, capacity);
|
|
3295
|
+
if (!new_output) {
|
|
3296
|
+
free(output);
|
|
3297
|
+
free(url);
|
|
3298
|
+
free(encoded_url);
|
|
3299
|
+
apex_free_image_attributes(local_img_attrs);
|
|
3300
|
+
return NULL;
|
|
3301
|
+
}
|
|
3302
|
+
output = new_output;
|
|
3303
|
+
write = output + written;
|
|
3304
|
+
remaining = capacity - written;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
/* Write closing paren */
|
|
3309
|
+
if (remaining > 0 && paren_end && *paren_end == ')') {
|
|
3310
|
+
*write++ = ')';
|
|
3311
|
+
remaining--;
|
|
3312
|
+
read = paren_end + 1;
|
|
3313
|
+
} else if (paren_end) {
|
|
3314
|
+
read = paren_end;
|
|
3315
|
+
} else {
|
|
3316
|
+
read = p;
|
|
3317
|
+
}
|
|
3318
|
+
free(encoded_url);
|
|
3319
|
+
}
|
|
3320
|
+
free(url);
|
|
3321
|
+
continue;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
/* Regular character - copy as-is */
|
|
3328
|
+
if (remaining > 0) {
|
|
3329
|
+
*write++ = *read++;
|
|
3330
|
+
remaining--;
|
|
3331
|
+
} else {
|
|
3332
|
+
/* Expand buffer */
|
|
3333
|
+
size_t written = write - output;
|
|
3334
|
+
capacity = (written + 1) * 2;
|
|
3335
|
+
char *new_output = realloc(output, capacity);
|
|
3336
|
+
if (!new_output) {
|
|
3337
|
+
free(output);
|
|
3338
|
+
apex_free_image_attributes(local_img_attrs);
|
|
3339
|
+
return NULL;
|
|
3340
|
+
}
|
|
3341
|
+
output = new_output;
|
|
3342
|
+
write = output + written;
|
|
3343
|
+
remaining = capacity - written;
|
|
3344
|
+
*write++ = *read++;
|
|
3345
|
+
remaining--;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
*write = '\0';
|
|
3350
|
+
|
|
3351
|
+
if (getenv("APEX_DEBUG_PIPELINE") && local_img_attrs) {
|
|
3352
|
+
fprintf(stderr, "[APEX_DEBUG] image_attr_entries:\n");
|
|
3353
|
+
for (image_attr_entry *e = local_img_attrs; e; e = e->next) {
|
|
3354
|
+
bool has_2x = attrs_have_srcset_2x(e->attrs);
|
|
3355
|
+
fprintf(stderr,
|
|
3356
|
+
" - url=\"%s\" index=%d ref=\"%s\" has_2x=%s\n",
|
|
3357
|
+
e->url ? e->url : "(null)",
|
|
3358
|
+
e->index,
|
|
3359
|
+
e->ref_name ? e->ref_name : "",
|
|
3360
|
+
has_2x ? "yes" : "no");
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
/* Second pass: expand reference-style images that have attributes */
|
|
3365
|
+
/* We need to expand ![ref][img1] to  for definitions with attributes */
|
|
3366
|
+
/* Run when we have ref_name entries (from do_image_attrs or known-attribute split on ref defs) */
|
|
3367
|
+
bool has_ref_entries = false;
|
|
3368
|
+
for (image_attr_entry *e = local_img_attrs; e; e = e->next) {
|
|
3369
|
+
if (e->ref_name) { has_ref_entries = true; break; }
|
|
3370
|
+
}
|
|
3371
|
+
if ((do_image_attrs && local_img_attrs) || has_ref_entries) {
|
|
3372
|
+
char *expanded_output = malloc(strlen(output) * 3 + 1);
|
|
3373
|
+
if (expanded_output) {
|
|
3374
|
+
const char *read2 = output;
|
|
3375
|
+
char *write2 = expanded_output;
|
|
3376
|
+
size_t remaining2 = strlen(output) * 3;
|
|
3377
|
+
bool made_expansions = false;
|
|
3378
|
+
|
|
3379
|
+
while (*read2) {
|
|
3380
|
+
/* Look for reference-style images: ![alt][ref] */
|
|
3381
|
+
if (*read2 == '!' && read2[1] == '[') {
|
|
3382
|
+
const char *img_start = read2;
|
|
3383
|
+
read2 += 2; /* Skip ![ */
|
|
3384
|
+
|
|
3385
|
+
/* Find closing ] for alt text */
|
|
3386
|
+
const char *alt_end = strchr(read2, ']');
|
|
3387
|
+
if (alt_end && alt_end[1] == '[') {
|
|
3388
|
+
/* Found ![alt][ */
|
|
3389
|
+
const char *ref_start = alt_end + 2; /* After ][ */
|
|
3390
|
+
const char *ref_end = strchr(ref_start, ']');
|
|
3391
|
+
if (ref_end) {
|
|
3392
|
+
/* Extract reference name (strip surrounding [ ]) */
|
|
3393
|
+
const char *name_start = (*ref_start == '[') ? ref_start + 1 : ref_start;
|
|
3394
|
+
if (name_start > ref_end) {
|
|
3395
|
+
name_start = ref_start;
|
|
3396
|
+
}
|
|
3397
|
+
size_t ref_name_len = (size_t)(ref_end - name_start);
|
|
3398
|
+
char *ref_name = malloc(ref_name_len + 1);
|
|
3399
|
+
if (ref_name) {
|
|
3400
|
+
memcpy(ref_name, name_start, ref_name_len);
|
|
3401
|
+
ref_name[ref_name_len] = '\0';
|
|
3402
|
+
/* Trim whitespace from reference name */
|
|
3403
|
+
char *p = ref_name;
|
|
3404
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
3405
|
+
if (p > ref_name) {
|
|
3406
|
+
memmove(ref_name, p, strlen(p) + 1);
|
|
3407
|
+
}
|
|
3408
|
+
p = ref_name + strlen(ref_name) - 1;
|
|
3409
|
+
while (p >= ref_name && isspace((unsigned char)*p)) {
|
|
3410
|
+
*p = '\0';
|
|
3411
|
+
p--;
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
/* Look up if this reference has attributes */
|
|
3415
|
+
image_attr_entry *def_entry = find_image_attr_by_ref(local_img_attrs, ref_name);
|
|
3416
|
+
if (def_entry && def_entry->url) {
|
|
3417
|
+
if (getenv("APEX_DEBUG_PIPELINE")) {
|
|
3418
|
+
fprintf(stderr, "[APEX_DEBUG] expand ref [%s] -> inline image\n", ref_name);
|
|
3419
|
+
}
|
|
3420
|
+
/* Expand to inline image:  */
|
|
3421
|
+
/* Extract alt text */
|
|
3422
|
+
size_t alt_len = alt_end - read2;
|
|
3423
|
+
|
|
3424
|
+
/* Convert attributes to markdown format */
|
|
3425
|
+
char *attr_str = attributes_to_markdown(def_entry->attrs);
|
|
3426
|
+
|
|
3427
|
+
/* Build expanded image:  */
|
|
3428
|
+
/* Need space for:  */
|
|
3429
|
+
size_t url_len = strlen(def_entry->url);
|
|
3430
|
+
size_t attr_len = attr_str ? strlen(attr_str) : 0;
|
|
3431
|
+
size_t needed = 4 + alt_len + url_len + (attr_len > 0 ? attr_len + 1 : 0) + 1;
|
|
3432
|
+
|
|
3433
|
+
/* Check if we need to expand buffer */
|
|
3434
|
+
if (remaining2 < needed) {
|
|
3435
|
+
size_t written = write2 - expanded_output;
|
|
3436
|
+
size_t new_cap = (written + needed + 1) * 2;
|
|
3437
|
+
char *new_expanded = realloc(expanded_output, new_cap);
|
|
3438
|
+
if (new_expanded) {
|
|
3439
|
+
expanded_output = new_expanded;
|
|
3440
|
+
write2 = expanded_output + written;
|
|
3441
|
+
remaining2 = new_cap - written;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
if (remaining2 >= needed) {
|
|
3446
|
+
made_expansions = true;
|
|
3447
|
+
|
|
3448
|
+
/* Write ;
|
|
3453
|
+
write2 += alt_len;
|
|
3454
|
+
remaining2 -= alt_len;
|
|
3455
|
+
*write2++ = ']';
|
|
3456
|
+
*write2++ = '(';
|
|
3457
|
+
remaining2 -= 2;
|
|
3458
|
+
|
|
3459
|
+
/* Write URL */
|
|
3460
|
+
memcpy(write2, def_entry->url, url_len);
|
|
3461
|
+
write2 += url_len;
|
|
3462
|
+
remaining2 -= url_len;
|
|
3463
|
+
|
|
3464
|
+
/* Write attributes if present (inside parentheses for inline format) */
|
|
3465
|
+
if (attr_str && *attr_str) {
|
|
3466
|
+
*write2++ = ' ';
|
|
3467
|
+
remaining2--;
|
|
3468
|
+
memcpy(write2, attr_str, attr_len);
|
|
3469
|
+
write2 += attr_len;
|
|
3470
|
+
remaining2 -= attr_len;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
*write2++ = ')';
|
|
3474
|
+
remaining2--;
|
|
3475
|
+
|
|
3476
|
+
read2 = ref_end + 1;
|
|
3477
|
+
free(ref_name);
|
|
3478
|
+
free(attr_str);
|
|
3479
|
+
continue;
|
|
3480
|
+
}
|
|
3481
|
+
free(attr_str);
|
|
3482
|
+
}
|
|
3483
|
+
free(ref_name);
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
/* Not a reference-style image with attributes, or expansion failed - copy as-is */
|
|
3488
|
+
read2 = img_start;
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
/* Copy character */
|
|
3492
|
+
if (remaining2 > 0) {
|
|
3493
|
+
*write2++ = *read2++;
|
|
3494
|
+
remaining2--;
|
|
3495
|
+
} else {
|
|
3496
|
+
read2++;
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
*write2 = '\0';
|
|
3501
|
+
free(output);
|
|
3502
|
+
output = expanded_output;
|
|
3503
|
+
|
|
3504
|
+
/* If we made expansions, we need to extract attributes from the expanded inline images */
|
|
3505
|
+
/* Process the expanded output to extract attributes from newly created inline images */
|
|
3506
|
+
if (made_expansions) {
|
|
3507
|
+
/* Create a temporary buffer to process expanded inline images */
|
|
3508
|
+
const char *proc_read = output;
|
|
3509
|
+
char *proc_output = malloc(strlen(output) * 2 + 1);
|
|
3510
|
+
if (proc_output) {
|
|
3511
|
+
char *proc_write = proc_output;
|
|
3512
|
+
size_t proc_remaining = strlen(output) * 2;
|
|
3513
|
+
|
|
3514
|
+
while (*proc_read) {
|
|
3515
|
+
/* Look for inline images that were just expanded */
|
|
3516
|
+
if (*proc_read == '!' && proc_read[1] == '[') {
|
|
3517
|
+
const char *img_start = proc_read;
|
|
3518
|
+
const char *check_pos = proc_read + 2; /* After ![ */
|
|
3519
|
+
|
|
3520
|
+
/* Find closing ] for alt text */
|
|
3521
|
+
const char *alt_end = strchr(check_pos, ']');
|
|
3522
|
+
if (alt_end && alt_end[1] == '(') {
|
|
3523
|
+
const char *url_start = alt_end + 2;
|
|
3524
|
+
const char *p = url_start;
|
|
3525
|
+
const char *url_end = NULL;
|
|
3526
|
+
const char *attr_start = NULL;
|
|
3527
|
+
const char *paren_end = NULL;
|
|
3528
|
+
|
|
3529
|
+
/* Find closing paren */
|
|
3530
|
+
while (*p && *p != ')' && *p != '\n') p++;
|
|
3531
|
+
if (*p == ')') {
|
|
3532
|
+
paren_end = p;
|
|
3533
|
+
p = url_start;
|
|
3534
|
+
|
|
3535
|
+
/* Look for attributes */
|
|
3536
|
+
while (p < paren_end) {
|
|
3537
|
+
if (*p == ' ' || *p == '\t') {
|
|
3538
|
+
const char *after_space = p;
|
|
3539
|
+
while (after_space < paren_end && (*after_space == ' ' || *after_space == '\t')) after_space++;
|
|
3540
|
+
if (after_space < paren_end && looks_like_attribute_start(after_space, paren_end)) {
|
|
3541
|
+
attr_start = after_space;
|
|
3542
|
+
url_end = p;
|
|
3543
|
+
break;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
p++;
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
if (!url_end) url_end = paren_end;
|
|
3550
|
+
|
|
3551
|
+
/* Only process images with attributes (these are the expanded reference-style images) */
|
|
3552
|
+
/* Images without attributes were already processed in the first pass */
|
|
3553
|
+
if (url_end > url_start && attr_start) {
|
|
3554
|
+
/* Extract URL and attributes */
|
|
3555
|
+
size_t url_len = url_end - url_start;
|
|
3556
|
+
char *url = malloc(url_len + 1);
|
|
3557
|
+
char *encoded_url = NULL;
|
|
3558
|
+
apex_attributes *attrs = NULL;
|
|
3559
|
+
|
|
3560
|
+
if (url) {
|
|
3561
|
+
memcpy(url, url_start, url_len);
|
|
3562
|
+
url[url_len] = '\0';
|
|
3563
|
+
|
|
3564
|
+
size_t attr_len = paren_end - attr_start;
|
|
3565
|
+
attrs = parse_image_attributes(attr_start, attr_len);
|
|
3566
|
+
|
|
3567
|
+
/* URL is already encoded from expansion, so use as-is */
|
|
3568
|
+
encoded_url = strdup(url);
|
|
3569
|
+
if (encoded_url && attrs) {
|
|
3570
|
+
/* Count existing entries to get index */
|
|
3571
|
+
int img_index = 0;
|
|
3572
|
+
for (image_attr_entry *e = local_img_attrs; e; e = e->next) {
|
|
3573
|
+
if (e->index >= 0) img_index++;
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
image_attr_entry *entry = create_image_attr_entry(&local_img_attrs, encoded_url, img_index);
|
|
3577
|
+
if (entry) {
|
|
3578
|
+
/* Copy attributes */
|
|
3579
|
+
for (int i = 0; i < attrs->attr_count; i++) {
|
|
3580
|
+
add_attribute(entry->attrs, attrs->keys[i], attrs->values[i]);
|
|
3581
|
+
}
|
|
3582
|
+
if (attrs->id) {
|
|
3583
|
+
entry->attrs->id = strdup(attrs->id);
|
|
3584
|
+
}
|
|
3585
|
+
for (int i = 0; i < attrs->class_count; i++) {
|
|
3586
|
+
add_class(entry->attrs, attrs->classes[i]);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
|
|
3591
|
+
/* Write the processed image:  - attributes removed */
|
|
3592
|
+
size_t prefix_len = url_start - img_start; /* Includes  {
|
|
3594
|
+
memcpy(proc_write, img_start, prefix_len);
|
|
3595
|
+
proc_write += prefix_len;
|
|
3596
|
+
proc_remaining -= prefix_len;
|
|
3597
|
+
} else {
|
|
3598
|
+
/* Buffer too small - skip this image */
|
|
3599
|
+
if (attrs) apex_free_attributes(attrs);
|
|
3600
|
+
free(encoded_url);
|
|
3601
|
+
free(url);
|
|
3602
|
+
proc_read = paren_end + 1;
|
|
3603
|
+
continue;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
/* Write encoded URL (already encoded, so write as-is) */
|
|
3607
|
+
size_t encoded_len = strlen(encoded_url ? encoded_url : url);
|
|
3608
|
+
if (encoded_len < proc_remaining) {
|
|
3609
|
+
memcpy(proc_write, encoded_url ? encoded_url : url, encoded_len);
|
|
3610
|
+
proc_write += encoded_len;
|
|
3611
|
+
proc_remaining -= encoded_len;
|
|
3612
|
+
} else {
|
|
3613
|
+
if (attrs) apex_free_attributes(attrs);
|
|
3614
|
+
free(encoded_url);
|
|
3615
|
+
free(url);
|
|
3616
|
+
proc_read = paren_end + 1;
|
|
3617
|
+
continue;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
/* Write closing paren */
|
|
3621
|
+
if (proc_remaining > 0) {
|
|
3622
|
+
*proc_write++ = ')';
|
|
3623
|
+
proc_remaining--;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
/* Cleanup */
|
|
3627
|
+
if (attrs) apex_free_attributes(attrs);
|
|
3628
|
+
free(encoded_url);
|
|
3629
|
+
free(url);
|
|
3630
|
+
|
|
3631
|
+
/* Advance past the processed image */
|
|
3632
|
+
proc_read = paren_end + 1;
|
|
3633
|
+
continue;
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
/* Not a valid inline image, or no attributes (already processed in first pass) - fall through to copy */
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
/* Copy character - this handles regular text and inline images without attributes */
|
|
3642
|
+
if (proc_remaining > 0) {
|
|
3643
|
+
*proc_write++ = *proc_read++;
|
|
3644
|
+
proc_remaining--;
|
|
3645
|
+
} else {
|
|
3646
|
+
proc_read++;
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
*proc_write = '\0';
|
|
3651
|
+
free(output);
|
|
3652
|
+
output = proc_output;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
/* Return the image attributes list */
|
|
3659
|
+
*img_attrs = local_img_attrs;
|
|
3660
|
+
|
|
3661
|
+
if (getenv("APEX_DEBUG_PIPELINE") && output) {
|
|
3662
|
+
size_t len = strlen(output);
|
|
3663
|
+
fprintf(stderr, "[APEX_DEBUG] preprocess_image_attributes out (len=%zu): %.300s%s\n",
|
|
3664
|
+
len, output, len > 300 ? "..." : "");
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
return output;
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
/**
|
|
3671
|
+
* Apply image attributes to image nodes in AST
|
|
3672
|
+
* Uses two matching strategies:
|
|
3673
|
+
* 1. First tries to match by index (position) for inline images
|
|
3674
|
+
* 2. Then tries to match by URL for reference-style images
|
|
3675
|
+
* This ensures inline images with same URL get different attributes,
|
|
3676
|
+
* while reference-style images share attributes from their definition.
|
|
3677
|
+
*/
|
|
3678
|
+
void apex_apply_image_attributes(cmark_node *document, image_attr_entry *img_attrs) {
|
|
3679
|
+
if (!document || !img_attrs) return;
|
|
3680
|
+
|
|
3681
|
+
cmark_iter *iter = cmark_iter_new(document);
|
|
3682
|
+
cmark_event_type event;
|
|
3683
|
+
|
|
3684
|
+
/* For each image node, we:
|
|
3685
|
+
* 1. Prefer a matching inline entry (index >= 0), using each at most once.
|
|
3686
|
+
* 2. If none, fall back to a reference-style entry (index == -1) matching by URL.
|
|
3687
|
+
*
|
|
3688
|
+
* This avoids relying on a separate image_index counter that can drift when
|
|
3689
|
+
* images are expanded/rewrapped, and cleanly distinguishes inline vs ref
|
|
3690
|
+
* attributes without letting one overwrite the other.
|
|
3691
|
+
*/
|
|
3692
|
+
while ((event = cmark_iter_next(iter)) != CMARK_EVENT_DONE) {
|
|
3693
|
+
cmark_node *node = cmark_iter_get_node(iter);
|
|
3694
|
+
if (event == CMARK_EVENT_ENTER && cmark_node_get_type(node) == CMARK_NODE_IMAGE) {
|
|
3695
|
+
const char *url = cmark_node_get_url(node);
|
|
3696
|
+
image_attr_entry *matching = NULL;
|
|
3697
|
+
|
|
3698
|
+
/* First, try to find an unused inline entry for this URL (index >= 0). */
|
|
3699
|
+
for (image_attr_entry *e = img_attrs; e; e = e->next) {
|
|
3700
|
+
if (e->index >= 0 && e->url && url && strcmp(e->url, url) == 0 && e->attrs) {
|
|
3701
|
+
matching = e;
|
|
3702
|
+
/* Consume this inline entry so it is only applied once. */
|
|
3703
|
+
e->index = -2; /* mark as used inline */
|
|
3704
|
+
break;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
/* If no inline entry found, try reference-style entries (index == -1) by URL. */
|
|
3709
|
+
if (!matching && url) {
|
|
3710
|
+
for (image_attr_entry *e = img_attrs; e; e = e->next) {
|
|
3711
|
+
if (e->index == -1 && e->url && strcmp(e->url, url) == 0 && e->attrs) {
|
|
3712
|
+
matching = e;
|
|
3713
|
+
break;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
if (matching && matching->attrs) {
|
|
3719
|
+
char *attr_str = attributes_to_html_for_image(url, matching->attrs);
|
|
3720
|
+
if (attr_str) {
|
|
3721
|
+
char *existing = (char *)cmark_node_get_user_data(node);
|
|
3722
|
+
if (existing) {
|
|
3723
|
+
char *combined = malloc(strlen(existing) + strlen(attr_str) + 2);
|
|
3724
|
+
if (combined) {
|
|
3725
|
+
strcpy(combined, existing);
|
|
3726
|
+
strcat(combined, " ");
|
|
3727
|
+
strcat(combined, attr_str);
|
|
3728
|
+
cmark_node_set_user_data(node, combined);
|
|
3729
|
+
free(attr_str);
|
|
3730
|
+
} else {
|
|
3731
|
+
cmark_node_set_user_data(node, attr_str);
|
|
3732
|
+
}
|
|
3733
|
+
} else {
|
|
3734
|
+
cmark_node_set_user_data(node, attr_str);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
cmark_iter_free(iter);
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
/**
|
|
3745
|
+
* Extract reference link definition IDs from text
|
|
3746
|
+
* Returns a hash set (simple array) of reference IDs
|
|
3747
|
+
* Caller must free the returned array
|
|
3748
|
+
*/
|
|
3749
|
+
static char **extract_reference_link_ids(const char *text, size_t *count) {
|
|
3750
|
+
if (!text || !count) return NULL;
|
|
3751
|
+
|
|
3752
|
+
*count = 0;
|
|
3753
|
+
size_t capacity = 16;
|
|
3754
|
+
char **ids = malloc(capacity * sizeof(char*));
|
|
3755
|
+
if (!ids) return NULL;
|
|
3756
|
+
|
|
3757
|
+
const char *p = text;
|
|
3758
|
+
while (*p) {
|
|
3759
|
+
const char *line_start = p;
|
|
3760
|
+
const char *line_end = strchr(p, '\n');
|
|
3761
|
+
if (!line_end) line_end = p + strlen(p);
|
|
3762
|
+
|
|
3763
|
+
/* Skip leading whitespace */
|
|
3764
|
+
const char *content_start = line_start;
|
|
3765
|
+
while (content_start < line_end && (*content_start == ' ' || *content_start == '\t')) {
|
|
3766
|
+
content_start++;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
/* Check if this is a reference link definition: [id]: URL */
|
|
3770
|
+
if (content_start < line_end && *content_start == '[') {
|
|
3771
|
+
const char *id_end = strchr(content_start + 1, ']');
|
|
3772
|
+
if (id_end && id_end < line_end && id_end[1] == ':') {
|
|
3773
|
+
/* Extract the ID from this definition */
|
|
3774
|
+
size_t def_id_len = id_end - (content_start + 1);
|
|
3775
|
+
if (def_id_len > 0) {
|
|
3776
|
+
char *def_id = malloc(def_id_len + 1);
|
|
3777
|
+
if (def_id) {
|
|
3778
|
+
memcpy(def_id, content_start + 1, def_id_len);
|
|
3779
|
+
def_id[def_id_len] = '\0';
|
|
3780
|
+
|
|
3781
|
+
/* Check if we already have this ID */
|
|
3782
|
+
bool found = false;
|
|
3783
|
+
for (size_t i = 0; i < *count; i++) {
|
|
3784
|
+
if (strcmp(ids[i], def_id) == 0) {
|
|
3785
|
+
found = true;
|
|
3786
|
+
free(def_id);
|
|
3787
|
+
break;
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
if (!found) {
|
|
3792
|
+
/* Add to array */
|
|
3793
|
+
if (*count >= capacity) {
|
|
3794
|
+
capacity *= 2;
|
|
3795
|
+
char **new_ids = realloc(ids, capacity * sizeof(char*));
|
|
3796
|
+
if (!new_ids) {
|
|
3797
|
+
free(def_id);
|
|
3798
|
+
break;
|
|
3799
|
+
}
|
|
3800
|
+
ids = new_ids;
|
|
3801
|
+
}
|
|
3802
|
+
ids[*count] = def_id;
|
|
3803
|
+
(*count)++;
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
p = (*line_end == '\n') ? line_end + 1 : line_end;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
return ids;
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
/**
|
|
3817
|
+
* Check if a reference ID matches the given text (case-insensitive, whitespace normalized)
|
|
3818
|
+
*/
|
|
3819
|
+
static bool reference_id_matches(const char *ref_id, const char *text, size_t text_len) {
|
|
3820
|
+
if (!ref_id || !text) return false;
|
|
3821
|
+
|
|
3822
|
+
const char *p = ref_id;
|
|
3823
|
+
const char *t = text;
|
|
3824
|
+
size_t remaining = text_len;
|
|
3825
|
+
|
|
3826
|
+
/* Skip leading whitespace in both */
|
|
3827
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
3828
|
+
while (remaining > 0 && isspace((unsigned char)*t)) {
|
|
3829
|
+
t++;
|
|
3830
|
+
remaining--;
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
/* Compare character by character (case-insensitive) */
|
|
3834
|
+
while (*p && remaining > 0) {
|
|
3835
|
+
if (tolower((unsigned char)*p) != tolower((unsigned char)*t)) {
|
|
3836
|
+
/* Check if difference is just whitespace */
|
|
3837
|
+
if (isspace((unsigned char)*p) && isspace((unsigned char)*t)) {
|
|
3838
|
+
/* Both are whitespace - skip and continue */
|
|
3839
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
3840
|
+
while (remaining > 0 && isspace((unsigned char)*t)) {
|
|
3841
|
+
t++;
|
|
3842
|
+
remaining--;
|
|
3843
|
+
}
|
|
3844
|
+
continue;
|
|
3845
|
+
}
|
|
3846
|
+
return false;
|
|
3847
|
+
}
|
|
3848
|
+
p++;
|
|
3849
|
+
t++;
|
|
3850
|
+
remaining--;
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
/* Skip trailing whitespace in both */
|
|
3854
|
+
while (*p && isspace((unsigned char)*p)) p++;
|
|
3855
|
+
while (remaining > 0 && isspace((unsigned char)*t)) {
|
|
3856
|
+
t++;
|
|
3857
|
+
remaining--;
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
/* Both should be at end */
|
|
3861
|
+
return (*p == '\0' && remaining == 0);
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
/**
|
|
3865
|
+
* Preprocess bracketed spans [text]{IAL}
|
|
3866
|
+
* Converts [text]{IAL} to <span markdown="span" ...>text</span> if [text] is not a reference link
|
|
3867
|
+
*/
|
|
3868
|
+
char *apex_preprocess_bracketed_spans(const char *text) {
|
|
3869
|
+
if (!text) return NULL;
|
|
3870
|
+
|
|
3871
|
+
/* First, extract all reference link definition IDs */
|
|
3872
|
+
size_t ref_count = 0;
|
|
3873
|
+
char **ref_ids = extract_reference_link_ids(text, &ref_count);
|
|
3874
|
+
|
|
3875
|
+
size_t text_len = strlen(text);
|
|
3876
|
+
size_t output_capacity = text_len * 2; /* Worst case: every char becomes part of HTML */
|
|
3877
|
+
char *output = malloc(output_capacity);
|
|
3878
|
+
if (!output) {
|
|
3879
|
+
/* Free ref_ids */
|
|
3880
|
+
if (ref_ids) {
|
|
3881
|
+
for (size_t i = 0; i < ref_count; i++) {
|
|
3882
|
+
free(ref_ids[i]);
|
|
3883
|
+
}
|
|
3884
|
+
free(ref_ids);
|
|
3885
|
+
}
|
|
3886
|
+
return NULL;
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
const char *read = text;
|
|
3890
|
+
char *write = output;
|
|
3891
|
+
size_t remaining = output_capacity;
|
|
3892
|
+
bool in_code_block = false;
|
|
3893
|
+
bool in_inline_code = false;
|
|
3894
|
+
int code_block_backticks = 0;
|
|
3895
|
+
|
|
3896
|
+
while (*read) {
|
|
3897
|
+
/* Skip code blocks and inline code */
|
|
3898
|
+
if (!in_code_block && !in_inline_code && *read == '`') {
|
|
3899
|
+
int backtick_count = 1;
|
|
3900
|
+
const char *p = read + 1;
|
|
3901
|
+
while (*p == '`') {
|
|
3902
|
+
backtick_count++;
|
|
3903
|
+
p++;
|
|
3904
|
+
}
|
|
3905
|
+
if (backtick_count >= 3) {
|
|
3906
|
+
/* Code block */
|
|
3907
|
+
in_code_block = !in_code_block;
|
|
3908
|
+
code_block_backticks = backtick_count;
|
|
3909
|
+
} else {
|
|
3910
|
+
/* Inline code */
|
|
3911
|
+
in_inline_code = !in_inline_code;
|
|
3912
|
+
}
|
|
3913
|
+
} else if (in_code_block && *read == '`') {
|
|
3914
|
+
int backtick_count = 1;
|
|
3915
|
+
const char *p = read + 1;
|
|
3916
|
+
while (*p == '`') {
|
|
3917
|
+
backtick_count++;
|
|
3918
|
+
p++;
|
|
3919
|
+
}
|
|
3920
|
+
if (backtick_count >= code_block_backticks) {
|
|
3921
|
+
in_code_block = false;
|
|
3922
|
+
code_block_backticks = 0;
|
|
3923
|
+
}
|
|
3924
|
+
} else if (in_inline_code && *read == '`') {
|
|
3925
|
+
in_inline_code = false;
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
/* Only process if not in code */
|
|
3929
|
+
if (!in_code_block && !in_inline_code && *read == '[') {
|
|
3930
|
+
const char *bracket_start = read;
|
|
3931
|
+
/* Find matching closing bracket by counting nested brackets */
|
|
3932
|
+
const char *bracket_end = NULL;
|
|
3933
|
+
int bracket_depth = 1;
|
|
3934
|
+
const char *p = bracket_start + 1;
|
|
3935
|
+
|
|
3936
|
+
while (*p && bracket_depth > 0) {
|
|
3937
|
+
if (*p == '[') {
|
|
3938
|
+
bracket_depth++;
|
|
3939
|
+
} else if (*p == ']') {
|
|
3940
|
+
bracket_depth--;
|
|
3941
|
+
if (bracket_depth == 0) {
|
|
3942
|
+
bracket_end = p;
|
|
3943
|
+
break;
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
p++;
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
if (bracket_end) {
|
|
3950
|
+
/* Check if this is followed by {IAL} */
|
|
3951
|
+
const char *after_bracket = bracket_end + 1;
|
|
3952
|
+
/* Skip whitespace */
|
|
3953
|
+
while (*after_bracket && (*after_bracket == ' ' || *after_bracket == '\t')) {
|
|
3954
|
+
after_bracket++;
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
if (*after_bracket == '{') {
|
|
3958
|
+
/* Found potential {IAL} - check if it's a valid IAL */
|
|
3959
|
+
const char *ial_start = after_bracket;
|
|
3960
|
+
const char *ial_end = strchr(ial_start + 1, '}');
|
|
3961
|
+
|
|
3962
|
+
if (ial_end) {
|
|
3963
|
+
/* Extract text inside brackets */
|
|
3964
|
+
size_t text_len = bracket_end - (bracket_start + 1);
|
|
3965
|
+
char *bracket_text = malloc(text_len + 1);
|
|
3966
|
+
if (bracket_text) {
|
|
3967
|
+
memcpy(bracket_text, bracket_start + 1, text_len);
|
|
3968
|
+
bracket_text[text_len] = '\0';
|
|
3969
|
+
|
|
3970
|
+
/* Check if this matches a reference link definition */
|
|
3971
|
+
bool is_reference_link = false;
|
|
3972
|
+
for (size_t i = 0; i < ref_count; i++) {
|
|
3973
|
+
if (reference_id_matches(ref_ids[i], bracket_text, text_len)) {
|
|
3974
|
+
is_reference_link = true;
|
|
3975
|
+
break;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
if (!is_reference_link) {
|
|
3980
|
+
/* This is a bracketed span - convert to <span> */
|
|
3981
|
+
/* Parse IAL attributes */
|
|
3982
|
+
size_t ial_len = ial_end - (ial_start + 1);
|
|
3983
|
+
apex_attributes *attrs = parse_ial_content(ial_start + 1, ial_len);
|
|
3984
|
+
|
|
3985
|
+
if (attrs) {
|
|
3986
|
+
/* Build span tag with attributes */
|
|
3987
|
+
char *attr_str = attributes_to_html(attrs);
|
|
3988
|
+
if (attr_str) {
|
|
3989
|
+
/* Calculate space needed */
|
|
3990
|
+
size_t span_open_len = 20 + strlen(attr_str) + strlen(bracket_text) + 10; /* <span markdown="span" ...>text</span> */
|
|
3991
|
+
if (remaining < span_open_len) {
|
|
3992
|
+
size_t written = write - output;
|
|
3993
|
+
output_capacity = (written + span_open_len + 1) * 2;
|
|
3994
|
+
char *new_output = realloc(output, output_capacity);
|
|
3995
|
+
if (!new_output) {
|
|
3996
|
+
free(bracket_text);
|
|
3997
|
+
free(attr_str);
|
|
3998
|
+
apex_free_attributes(attrs);
|
|
3999
|
+
goto cleanup;
|
|
4000
|
+
}
|
|
4001
|
+
output = new_output;
|
|
4002
|
+
write = output + written;
|
|
4003
|
+
remaining = output_capacity - written;
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
/* Write <span markdown="span" ...> */
|
|
4007
|
+
int written = snprintf(write, remaining, "<span markdown=\"span\"%s>", attr_str);
|
|
4008
|
+
if (written > 0 && (size_t)written < remaining) {
|
|
4009
|
+
write += written;
|
|
4010
|
+
remaining -= written;
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
/* Write the text content */
|
|
4014
|
+
size_t text_written = strlen(bracket_text);
|
|
4015
|
+
if (text_written < remaining) {
|
|
4016
|
+
memcpy(write, bracket_text, text_written);
|
|
4017
|
+
write += text_written;
|
|
4018
|
+
remaining -= text_written;
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
/* Write </span> */
|
|
4022
|
+
if (remaining >= 7) {
|
|
4023
|
+
memcpy(write, "</span>", 7);
|
|
4024
|
+
write += 7;
|
|
4025
|
+
remaining -= 7;
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
free(attr_str);
|
|
4029
|
+
read = ial_end + 1; /* Skip past the IAL */
|
|
4030
|
+
free(bracket_text);
|
|
4031
|
+
apex_free_attributes(attrs);
|
|
4032
|
+
continue;
|
|
4033
|
+
}
|
|
4034
|
+
apex_free_attributes(attrs);
|
|
4035
|
+
}
|
|
4036
|
+
free(bracket_text);
|
|
4037
|
+
} else {
|
|
4038
|
+
free(bracket_text);
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
/* Copy character as-is */
|
|
4047
|
+
if (remaining > 0) {
|
|
4048
|
+
*write++ = *read++;
|
|
4049
|
+
remaining--;
|
|
4050
|
+
} else {
|
|
4051
|
+
size_t written = write - output;
|
|
4052
|
+
output_capacity = (written + 1) * 2;
|
|
4053
|
+
char *new_output = realloc(output, output_capacity);
|
|
4054
|
+
if (!new_output) {
|
|
4055
|
+
goto cleanup;
|
|
4056
|
+
}
|
|
4057
|
+
output = new_output;
|
|
4058
|
+
write = output + written;
|
|
4059
|
+
remaining = output_capacity - written;
|
|
4060
|
+
*write++ = *read++;
|
|
4061
|
+
remaining--;
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
*write = '\0';
|
|
4066
|
+
|
|
4067
|
+
cleanup:
|
|
4068
|
+
/* Free ref_ids */
|
|
4069
|
+
if (ref_ids) {
|
|
4070
|
+
for (size_t i = 0; i < ref_count; i++) {
|
|
4071
|
+
free(ref_ids[i]);
|
|
4072
|
+
}
|
|
4073
|
+
free(ref_ids);
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
/* Check if we made any changes */
|
|
4077
|
+
if (strcmp(output, text) == 0) {
|
|
4078
|
+
free(output);
|
|
4079
|
+
return NULL; /* No changes */
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
return output;
|
|
4083
|
+
}
|
|
4084
|
+
|