├── .codecov.yml ├── .github └── workflows │ ├── ci-bench.yml │ ├── ci-build.yml │ └── ci-release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CONTRIBUTE.adoc ├── LICENSE ├── LIMITATIONS.adoc ├── Makefile ├── README.adoc ├── cmd └── libasciidoc │ ├── cmd_suite_test.go │ ├── main.go │ ├── root_cmd.go │ ├── root_cmd_test.go │ ├── test │ ├── admonition.adoc │ ├── doc_with_attributes.adoc │ └── test.adoc │ ├── version_cmd.go │ └── version_cmd_test.go ├── code-gen └── renderer-templates │ └── main.go ├── docs └── design.adoc ├── go.mod ├── go.sum ├── libasciidoc.go ├── libasciidoc_bench_test.go ├── libasciidoc_suite_test.go ├── libasciidoc_test.go ├── make ├── bench.mk ├── clean.mk ├── git.mk ├── go.mk ├── help.mk ├── lint.mk ├── profile.mk ├── test.mk └── verbose.mk ├── pkg ├── configuration │ ├── configuration.go │ └── macro_template.go ├── parser │ ├── attribute_substitution_test.go │ ├── attributes_test.go │ ├── bench_test.go │ ├── blank_line_test.go │ ├── check_list_test.go │ ├── comment_test.go │ ├── context.go │ ├── cross_reference_test.go │ ├── delimited_block_admonition_test.go │ ├── delimited_block_example_test.go │ ├── delimited_block_fenced_test.go │ ├── delimited_block_listing_test.go │ ├── delimited_block_literal_test.go │ ├── delimited_block_markdown_quote_test.go │ ├── delimited_block_open_test.go │ ├── delimited_block_passthrough_test.go │ ├── delimited_block_quote_test.go │ ├── delimited_block_sidebar_test.go │ ├── delimited_block_source_test.go │ ├── delimited_block_verse_test.go │ ├── document_fragment_processing_test.go │ ├── document_header_test.go │ ├── document_preprocessing.go │ ├── document_preprocessing_builder_test.go │ ├── document_preprocessing_conditionals_test.go │ ├── document_preprocessing_include_files_test.go │ ├── document_processing.go │ ├── document_processing_aggregate.go │ ├── document_processing_aggregate_test.go │ ├── document_processing_apply_substitutions.go │ ├── document_processing_apply_substitutions_test.go │ ├── document_processing_arrange_lists.go │ ├── document_processing_collect_footnotes.go │ ├── document_processing_collect_footnotes_test.go │ ├── document_processing_filter_elements.go │ ├── document_processing_filter_elements_test.go │ ├── document_processing_insert_preamble_test.go │ ├── document_processing_parse_fragments.go │ ├── document_processing_parse_fragments_test.go │ ├── document_processing_raw_source_test.go │ ├── document_processing_refine_fragments.go │ ├── document_substitutions.go │ ├── document_substitutions_test.go │ ├── document_test.go │ ├── element_attributes_test.go │ ├── escape_backslash_test.go │ ├── footnote_test.go │ ├── frontmatter_test.go │ ├── generate.go │ ├── icon_test.go │ ├── image_test.go │ ├── index_terms_test.go │ ├── inline_button_test.go │ ├── inline_elements_test.go │ ├── inline_menu_test.go │ ├── labeled_list_test.go │ ├── line_break_test.go │ ├── link_test.go │ ├── mixed_lists_test.go │ ├── ordered_list_test.go │ ├── paragragh_debug_test.go │ ├── paragragh_stats_test.go │ ├── paragraph_test.go │ ├── parser.go │ ├── parser.peg │ ├── parser_ext.go │ ├── parser_ext_test.go │ ├── parser_suite_test.go │ ├── passthrough_test.go │ ├── q_a_list_test.go │ ├── quoted_string_test.go │ ├── quoted_text_test.go │ ├── section_test.go │ ├── special_character_test.go │ ├── stats.go │ ├── symbol_test.go │ ├── table_of_contents_test.go │ ├── table_test.go │ ├── thematic_break_test.go │ ├── unordered_list_test.go │ └── user_macro_test.go ├── renderer │ ├── renderer.go │ ├── renderer_suite_test.go │ └── sgml │ │ ├── attribution.go │ │ ├── blank_line.go │ │ ├── context.go │ │ ├── cross_reference.go │ │ ├── delimited_block.go │ │ ├── delimited_block_admonition.go │ │ ├── delimited_block_discard_lines.go │ │ ├── delimited_block_example.go │ │ ├── delimited_block_fenced.go │ │ ├── delimited_block_listing.go │ │ ├── delimited_block_markdown_quote.go │ │ ├── delimited_block_open.go │ │ ├── delimited_block_passthrough.go │ │ ├── delimited_block_quote.go │ │ ├── delimited_block_sidebar.go │ │ ├── delimited_block_source.go │ │ ├── delimited_block_verse.go │ │ ├── document_details.go │ │ ├── element_id.go │ │ ├── element_role.go │ │ ├── element_style.go │ │ ├── elements.go │ │ ├── footnote_reference.go │ │ ├── html5 │ │ ├── article.go │ │ ├── blank_line.go │ │ ├── blank_line_test.go │ │ ├── callout_list.go │ │ ├── check_list_test.go │ │ ├── comment_test.go │ │ ├── cross_reference.go │ │ ├── cross_reference_test.go │ │ ├── delimited_block_admonition_test.go │ │ ├── delimited_block_admonition_tmpl.go │ │ ├── delimited_block_example_test.go │ │ ├── delimited_block_example_tmpl.go │ │ ├── delimited_block_fenced_test.go │ │ ├── delimited_block_fenced_tmpl.go │ │ ├── delimited_block_listing_test.go │ │ ├── delimited_block_listing_tmpl.go │ │ ├── delimited_block_literal_test.go │ │ ├── delimited_block_literal_tmpl.go │ │ ├── delimited_block_markdown_quote_test.go │ │ ├── delimited_block_markdown_quote_tmpl.go │ │ ├── delimited_block_open_test.go │ │ ├── delimited_block_open_tmpl.go │ │ ├── delimited_block_passthrough_test.go │ │ ├── delimited_block_passthrough_tmpl.go │ │ ├── delimited_block_quote_test.go │ │ ├── delimited_block_quote_tmpl.go │ │ ├── delimited_block_sidebar_test.go │ │ ├── delimited_block_sidebar_tmpl.go │ │ ├── delimited_block_source_test.go │ │ ├── delimited_block_source_tmpl.go │ │ ├── delimited_block_verse_test.go │ │ ├── delimited_block_verse_tmpl.go │ │ ├── distinct_lists_test.go │ │ ├── document_details.go │ │ ├── document_details_test.go │ │ ├── file_inclusion_test.go │ │ ├── footnote_reference.go │ │ ├── footnote_reference_test.go │ │ ├── front_matter_test.go │ │ ├── html5.go │ │ ├── html5_suite_test.go │ │ ├── html5_test.go │ │ ├── icon.go │ │ ├── icon_test.go │ │ ├── image.go │ │ ├── image_test.go │ │ ├── index_terms_test.go │ │ ├── inline_button.go │ │ ├── inline_button_test.go │ │ ├── inline_menu.go │ │ ├── inline_menu_test.go │ │ ├── labeled_list.go │ │ ├── labeled_list_test.go │ │ ├── link.go │ │ ├── link_test.go │ │ ├── manpage.go │ │ ├── ordered_list.go │ │ ├── ordered_list_test.go │ │ ├── paragraph.go │ │ ├── paragraph_test.go │ │ ├── passthrough_test.go │ │ ├── preamble.go │ │ ├── quoted_string_test.go │ │ ├── quoted_text.go │ │ ├── quoted_text_test.go │ │ ├── section.go │ │ ├── section_test.go │ │ ├── special_character_test.go │ │ ├── string_test.go │ │ ├── symbol_test.go │ │ ├── table.go │ │ ├── table_of_contents.go │ │ ├── table_of_contents_test.go │ │ ├── table_test.go │ │ ├── templates.go │ │ ├── templates_test.go │ │ ├── thematic_break_test.go │ │ ├── unordered_list.go │ │ ├── unordered_list_test.go │ │ └── user_macro_test.go │ │ ├── html_escape.go │ │ ├── icon.go │ │ ├── image.go │ │ ├── index_term.go │ │ ├── inline_button.go │ │ ├── inline_elements.go │ │ ├── inline_menu.go │ │ ├── link.go │ │ ├── list.go │ │ ├── literal_blocks.go │ │ ├── paragraph.go │ │ ├── passthrough.go │ │ ├── preamble.go │ │ ├── predefined_attribute.go │ │ ├── predefined_attribute_test.go │ │ ├── quoted_text.go │ │ ├── render_plain_text.go │ │ ├── section.go │ │ ├── sgml.go │ │ ├── sgml_renderer.go │ │ ├── sgml_suite_test.go │ │ ├── special_character.go │ │ ├── string.go │ │ ├── symbol.go │ │ ├── table.go │ │ ├── table_of_contents.go │ │ ├── templates.go │ │ ├── user_macro.go │ │ └── xhtml5 │ │ ├── article.go │ │ ├── blank_line.go │ │ ├── blank_line_test.go │ │ ├── comment_test.go │ │ ├── cross_reference_test.go │ │ ├── delimited_block_admonition_test.go │ │ ├── delimited_block_example_test.go │ │ ├── delimited_block_fenced_test.go │ │ ├── delimited_block_listing_test.go │ │ ├── delimited_block_literal_test.go │ │ ├── delimited_block_markdown_quote_test.go │ │ ├── delimited_block_passthrough_test.go │ │ ├── delimited_block_quote_test.go │ │ ├── delimited_block_quote_tmpl.go │ │ ├── delimited_block_sidebar_test.go │ │ ├── delimited_block_source_test.go │ │ ├── delimited_block_verse_test.go │ │ ├── delimited_block_verse_tmpl.go │ │ ├── distinct_lists_test.go │ │ ├── document_details.go │ │ ├── document_details_test.go │ │ ├── file_inclusion_test.go │ │ ├── footnote_reference.go │ │ ├── footnote_reference_test.go │ │ ├── icon.go │ │ ├── icon_test.go │ │ ├── image.go │ │ ├── image_test.go │ │ ├── index_terms_test.go │ │ ├── labeled_list.go │ │ ├── labeled_list_test.go │ │ ├── link_test.go │ │ ├── ordered_list_test.go │ │ ├── paragraph.go │ │ ├── paragraph_test.go │ │ ├── passthrough_test.go │ │ ├── quoted_string_test.go │ │ ├── quoted_text_test.go │ │ ├── section_test.go │ │ ├── string_test.go │ │ ├── suite_test.go │ │ ├── table.go │ │ ├── table_of_contents_test.go │ │ ├── table_test.go │ │ ├── thematic_break_test.go │ │ ├── unordered_list_test.go │ │ ├── user_macro_test.go │ │ ├── xhtml5.go │ │ └── xhtml5_test.go ├── types │ ├── attributes.go │ ├── document_xrefs.go │ ├── footnotes.go │ ├── non_alphanumeric_replacement_test.go │ ├── non_alphanumerics_replacement.go │ ├── predefined_attributes.go │ ├── section_numbering_test.go │ ├── types.go │ ├── types_suite_test.go │ ├── types_test.go │ ├── types_utils.go │ └── types_utils_test.go └── validator │ ├── validator.go │ ├── validator_suite_test.go │ └── validator_test.go ├── test ├── bench │ ├── mocking.adoc │ └── vertx-examples.adoc ├── compat │ ├── article.adoc │ ├── article.html │ ├── basic.adoc │ ├── basic.html │ ├── demo-full.tmpl.html │ ├── demo-shorter.adoc │ ├── demo-shorter.html │ ├── demo.adoc │ ├── demo.html │ ├── encoding.adoc │ ├── encoding.html │ ├── exline-mono.adoc │ ├── exline-mono.html │ ├── include.adoc │ ├── includes │ │ ├── chapter-a.adoc │ │ ├── child-include.adoc │ │ └── grandchild-include.adoc │ ├── lists.adoc │ ├── lists.html │ ├── master.adoc │ ├── master.html │ ├── parent-include.adoc │ ├── parent-include.html │ ├── sample.adoc │ ├── sample.html │ ├── sscript.adoc │ └── sscript.html ├── compat_test.go ├── doc.go ├── images │ └── favicon-glasses-16x16.png ├── includes │ ├── attributes.adoc │ ├── chapter-a.adoc │ ├── child-include-preamble.adoc │ ├── child-include.adoc │ ├── grandchild-include.adoc │ ├── hello_world.go.txt │ ├── include.foo │ ├── parent-include-absolute-offset.adoc │ ├── parent-include-relative-offset.adoc │ ├── parent-include.adoc │ ├── sample.html │ ├── table_common.txt │ ├── table_parent.adoc │ ├── table_specific.txt │ ├── tag-include-unclosed.adoc │ └── tag-include.adoc └── test_suite_test.go ├── testsupport ├── console_matcher.go ├── console_matcher_test.go ├── document_fragment_matcher.go ├── document_fragment_matcher_test.go ├── document_fragments_matcher.go ├── document_fragments_matcher_test.go ├── document_matcher.go ├── document_matcher_test.go ├── html5_matcher.go ├── html5_matcher_test.go ├── inline_elements_matcher.go ├── inline_elements_matcher_test.go ├── log_init.go ├── metadata_matcher.go ├── metadata_matcher_test.go ├── parse_document.go ├── parse_document_fragments.go ├── parse_document_fragments_test.go ├── parse_document_test.go ├── preparse_document.go ├── render_html5.go ├── render_html5_test.go ├── table_of_contents_matcher.go ├── table_of_contents_matcher_test.go └── testsupport_suite_test.go └── tools.go /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | token: 61ac59d6-e16b-4057-9b30-c541bde05a3f 3 | notify: 4 | require_ci_to_pass: yes 5 | 6 | coverage: 7 | status: 8 | project: 9 | default: 10 | # basic 11 | target: 80 12 | threshold: 2 13 | patch: yes 14 | changes: no 15 | precision: 2 16 | round: down 17 | 18 | parsers: 19 | gcov: 20 | branch_detection: 21 | conditional: yes 22 | loop: yes 23 | method: no 24 | macro: no 25 | 26 | comment: 27 | layout: "header, diff" 28 | behavior: default 29 | require_changes: no 30 | 31 | ignore: 32 | # - "path/to/folder" # ignore folders and all its contents 33 | # - "test_*.rb" # wildcards accepted 34 | # - "**/*.py" # glob accepted 35 | # - "[a-z]+/test_.*" # regexp accepted 36 | - "pkg/parser/parser.go" -------------------------------------------------------------------------------- /.github/workflows/ci-bench.yml: -------------------------------------------------------------------------------- 1 | name: ci-bench 2 | on: 3 | pull_request_target: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | bench: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | go-version: [1.18.x] 13 | os: [ubuntu-latest] 14 | name: Benchstat with Go ${{ matrix.go-version }} 15 | 16 | steps: 17 | - name: Install Go 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: ${{ matrix.go-version }} 21 | 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | 27 | - name: Cache dependencies 28 | uses: actions/cache@v3 29 | with: 30 | path: ~/go/pkg/mod 31 | key: ${{ runner.os }}-go-${{ hashFiles ('**/go.sum') }} 32 | restore-keys: | 33 | ${{ runner.os }}-go- 34 | 35 | - name: Verify parser 36 | run: | 37 | make verify-parser 38 | 39 | - name: Bench and Diff 40 | id: bench-diff 41 | run: | 42 | go install golang.org/x/perf/cmd/benchstat@latest 43 | make bench-diff 44 | DIFF_MASTER=$(make print-bench-diff-master) 45 | # because set-output doesn't support multiline content out-of-the-box 46 | DIFF_MASTER="${DIFF_MASTER//'%'/'%25'}" 47 | DIFF_MASTER="${DIFF_MASTER//$'\n'/'%0A'}" 48 | DIFF_MASTER="${DIFF_MASTER//$'\r'/'%0D'}" 49 | echo "::set-output name=diff-master::$DIFF_MASTER" 50 | 51 | DIFF_LATEST_RELEASE=$(make print-bench-diff-latest-release) 52 | # because set-output doesn't support multiline content out-of-the-box 53 | DIFF_LATEST_RELEASE="${DIFF_LATEST_RELEASE//'%'/'%25'}" 54 | DIFF_LATEST_RELEASE="${DIFF_LATEST_RELEASE//$'\n'/'%0A'}" 55 | DIFF_LATEST_RELEASE="${DIFF_LATEST_RELEASE//$'\r'/'%0D'}" 56 | echo "::set-output name=diff-latest-release::$DIFF_LATEST_RELEASE" 57 | 58 | - name: Comment Benchmark Diffs 59 | uses: marocchino/sticky-pull-request-comment@v2 60 | with: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | header: bench 63 | message: | 64 | Comparing with `master` branch: 65 | 66 | ``` 67 | ${{ steps.bench-diff.outputs.diff-master }} 68 | ``` 69 | 70 | Comparing with latest release: 71 | 72 | ``` 73 | ${{ steps.bench-diff.outputs.diff-latest-release }} 74 | ``` 75 | 76 | -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: ci-build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags-ignore: 7 | - '*.*' 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | go-version: [1.18.x,1.19.x] 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | name: Test ${{ matrix.os }} with Go ${{ matrix.go-version }} 20 | 21 | steps: 22 | - name: Install Go 23 | uses: actions/setup-go@v3 24 | with: 25 | go-version: ${{ matrix.go-version }} 26 | 27 | - name: Checkout code 28 | uses: actions/checkout@v3 29 | 30 | - name: Cache dependencies 31 | uses: actions/cache@v3 32 | with: 33 | path: ~/go/pkg/mod 34 | key: ${{ runner.os }}-go-${{ hashFiles ('**/go.sum') }} 35 | restore-keys: | 36 | ${{ runner.os }}-go- 37 | 38 | - name: Verify parser 39 | if: runner.os == 'Linux' 40 | run: | 41 | make verify-parser 42 | 43 | - name: Build 44 | run: | 45 | make build 46 | 47 | - name: Test 48 | run: | 49 | make test-with-coverage 50 | 51 | - name: Codecov 52 | if: runner.os == 'Linux' 53 | uses: codecov/codecov-action@v3 54 | with: 55 | # Path to coverage file to upload 56 | file: coverprofile.out 57 | 58 | golangci: 59 | name: Lint with Go ${{ matrix.go-version }} 60 | runs-on: ubuntu-latest 61 | strategy: 62 | matrix: 63 | go-version: [1.18.x] 64 | steps: 65 | - name: Checkout code 66 | uses: actions/checkout@v3 67 | 68 | - name: Lint 69 | uses: golangci/golangci-lint-action@v3 70 | with: 71 | version: latest 72 | args: -c .golangci.yml 73 | 74 | -------------------------------------------------------------------------------- /.github/workflows/ci-release.yml: -------------------------------------------------------------------------------- 1 | name: ci-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - 21 | name: Fetch all tags 22 | run: git fetch --force --tags 23 | - 24 | name: Set up Go 25 | uses: actions/setup-go@v3 26 | with: 27 | go-version: 1.18 28 | - 29 | name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v2 31 | with: 32 | distribution: goreleaser 33 | version: latest 34 | args: release --rm-dist 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Intellij ### 2 | .idea/ 3 | *.iws 4 | *.iml 5 | /out/ 6 | .idea_modules/ 7 | atlassian-ide-plugin.xml 8 | 9 | ### Eclipse ### 10 | .metadata 11 | tmp/ 12 | *.tmp 13 | *.bak 14 | *.swp 15 | *~.nib 16 | local.properties 17 | .settings/ 18 | .loadpath 19 | .recommenders 20 | 21 | # Eclipse Core 22 | .project 23 | 24 | # External tool builders 25 | .externalToolBuilders/ 26 | 27 | # Locally stored "Eclipse launch configurations" 28 | *.launch 29 | 30 | # Code Recommenders 31 | .recommenders/ 32 | 33 | 34 | ### VisualStudioCode ### 35 | .vscode 36 | 37 | ### Go ### 38 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 39 | *.o 40 | *.a 41 | *.so 42 | *.coverprofile 43 | 44 | # Folders 45 | _obj 46 | _test 47 | 48 | # binaries 49 | bin/ 50 | 51 | # dep package manager 52 | vendor 53 | 54 | # exclude `tmp` and `dist` directories 55 | tmp 56 | dist 57 | 58 | # exclude code coverage merged results 59 | coverage.txt 60 | .DS_Store 61 | *.html 62 | *.test 63 | !test/**/*.html 64 | !pkg/**/*.html 65 | *libasciidoc.html 66 | profile*.* 67 | !make/*.mk 68 | coverprofile.out 69 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | skip-dirs: 3 | - test/includes 4 | skip-files: 5 | - pkg/parser/parser.go # generated 6 | timeout: 10m 7 | 8 | linters: 9 | enable-all: false 10 | enable: 11 | - megacheck 12 | - govet 13 | - gocyclo 14 | - unused 15 | - gofmt 16 | - revive 17 | - misspell 18 | - exportloopref 19 | - nolintlint 20 | disable-all: false 21 | disable: 22 | - maligned 23 | - prealloc 24 | - scopelint 25 | - golint 26 | - asasalint 27 | presets: 28 | - bugs 29 | - unused 30 | fast: false 31 | 32 | linters-settings: 33 | revive: 34 | # Enable all available rules. 35 | # Default: false 36 | # enable-all-rules: true 37 | rules: 38 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#dot-imports 39 | - name: dot-imports 40 | disabled: true 41 | exhaustive: 42 | # check switch statements in generated files also 43 | check-generated: false 44 | # indicates that switch statements are to be considered exhaustive if a 45 | # 'default' case is present, even if all enum members aren't listed in the 46 | # switch 47 | default-signifies-exhaustive: true 48 | nolintlint: 49 | # Disable to ensure that all nolint directives actually have an effect. 50 | # Default: false 51 | allow-unused: false 52 | # Disable to ensure that nolint directives don't have a leading space. 53 | # Default: true 54 | allow-leading-space: false 55 | # Exclude following linters from requiring an explanation. 56 | # Default: [] 57 | allow-no-explanation: [] 58 | # Enable to require an explanation of nonzero length after each nolint directive. 59 | # Default: false 60 | require-explanation: false 61 | # Enable to require nolint directives to mention the specific linter being suppressed. 62 | # Default: false 63 | require-specific: true -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # you may remove this if you don't use vgo 6 | # - go mod download 7 | # you may remove this if you don't need go generate 8 | # - go generate ./... 9 | builds: 10 | - 11 | # Path to main.go file or main package. 12 | # Default is `.`. 13 | main: ./cmd/libasciidoc 14 | # Custom ldflags templates. 15 | # Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`. 16 | ldflags: 17 | - "-s -w -X github.com/bytesparadise/libasciidoc.BuildTag={{.Version}} -X github.com/bytesparadise/libasciidoc.BuildCommit={{.Commit}} -X github.com/bytesparadise/libasciidoc.BuildTime={{.Date}}" 18 | env: 19 | - CGO_ENABLED=0 20 | 21 | archives: 22 | - id: default 23 | replacements: 24 | darwin: Darwin 25 | linux: Linux 26 | windows: Windows 27 | 386: i386 28 | amd64: x86_64 29 | 30 | checksum: 31 | name_template: 'checksums.txt' 32 | 33 | snapshot: 34 | name_template: "{{ .Tag }}-next" 35 | 36 | changelog: 37 | use: github 38 | sort: asc 39 | groups: 40 | - title: Features 41 | regexp: "^.*feat[(\\w)(/(\\w))?]*:+.*$" 42 | order: 0 43 | - title: Refactoring 44 | regexp: "^.*refactor[(\\w)(/(\\w))?]*:+.*$" 45 | order: 1 46 | - title: 'Bug fixes' 47 | regexp: "^.*fix[(\\w)(/(\\w))?]*:+.*$" 48 | order: 2 49 | - title: Others 50 | order: 999 51 | filters: 52 | exclude: 53 | - '^docs:' 54 | - '^test:' 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # It's necessary to set this because some environments don't link sh -> bash. 2 | SHELL := /bin/bash 3 | 4 | include ./make/*.mk 5 | .DEFAULT_GOAL := help -------------------------------------------------------------------------------- /cmd/libasciidoc/cmd_suite_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | 9 | _ "github.com/bytesparadise/libasciidoc/testsupport" 10 | ) 11 | 12 | func TestCmd(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Cmd Suite") 15 | } 16 | -------------------------------------------------------------------------------- /cmd/libasciidoc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func main() { 13 | rootCmd := NewRootCmd() 14 | versionCmd := NewVersionCmd() 15 | rootCmd.AddCommand(versionCmd) 16 | rootCmd.SetHelpCommand(helpCommand) 17 | if err := rootCmd.Execute(); err != nil { 18 | fmt.Println(err) 19 | os.Exit(1) 20 | } 21 | } 22 | 23 | var helpCommand = &cobra.Command{ 24 | Use: "help [command]", 25 | Short: "Help about the command", 26 | RunE: func(c *cobra.Command, args []string) error { 27 | cmd, args, e := c.Root().Find(args) 28 | if cmd == nil || e != nil || len(args) > 0 { 29 | return errors.Errorf("unknown help topic: %v", strings.Join(args, " ")) 30 | } 31 | helpFunc := cmd.HelpFunc() 32 | helpFunc(cmd, args) 33 | return nil 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /cmd/libasciidoc/test/admonition.adoc: -------------------------------------------------------------------------------- 1 | NOTE: this is a note 2 | 3 | [NOTE] 4 | a para note 5 | 6 | [NOTE] 7 | ---- 8 | multiple 9 | 10 | paras 11 | 12 | ---- 13 | -------------------------------------------------------------------------------- /cmd/libasciidoc/test/doc_with_attributes.adoc: -------------------------------------------------------------------------------- 1 | :foo1: FOO1 2 | 3 | :!foo1: 4 | 5 | {foo1} and {foo2} -------------------------------------------------------------------------------- /cmd/libasciidoc/test/test.adoc: -------------------------------------------------------------------------------- 1 | [NOTE] 2 | ---- 3 | multiple 4 | 5 | paragraphs 6 | 7 | ---- -------------------------------------------------------------------------------- /cmd/libasciidoc/version_cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // NewVersionCmd returns the root command 11 | func NewVersionCmd() *cobra.Command { 12 | return &cobra.Command{ 13 | Use: "version", 14 | Short: "Print the version and build info", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | if libasciidoc.BuildTag != "" { 17 | fmt.Fprintf(cmd.OutOrStdout(), "version: %s\n", libasciidoc.BuildTag) 18 | } else { 19 | fmt.Fprintf(cmd.OutOrStdout(), "commit: %s\n", libasciidoc.BuildCommit) 20 | } 21 | fmt.Fprintf(cmd.OutOrStdout(), "build time: %s\n", libasciidoc.BuildTime) 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/libasciidoc/version_cmd_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "bytes" 5 | 6 | main "github.com/bytesparadise/libasciidoc/cmd/libasciidoc" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("version cmd", func() { 13 | 14 | It("ok", func() { 15 | // given 16 | versionCmd := main.NewVersionCmd() 17 | buf := new(bytes.Buffer) 18 | versionCmd.SetOutput(buf) 19 | versionCmd.SetArgs([]string{}) 20 | // when 21 | err := versionCmd.Execute() 22 | // then 23 | Expect(err).ToNot(HaveOccurred()) 24 | Expect(buf.String()).ToNot(BeEmpty()) 25 | }) 26 | 27 | }) 28 | -------------------------------------------------------------------------------- /docs/design.adoc: -------------------------------------------------------------------------------- 1 | = Libasciidoc Design 2 | 3 | This document brifley explains how the library works, from parsing a document to rendering in HTML. 4 | 5 | == Types 6 | 7 | A document contains blocks, which can have attributes, nested blocks and elements. 8 | 9 | Blocks are of the following types: 10 | 11 | - Section 12 | - Paragraph 13 | - Delimited block 14 | - Image 15 | - Table 16 | - List 17 | - File inclusion 18 | - Comment 19 | 20 | Tables, lists and delimited blocks can contain other blocks. 21 | 22 | Blocks can also contain elements, such as: 23 | 24 | - quoted text 25 | - links 26 | - (inline) images 27 | - passthrough text 28 | - foot notes 29 | - user-defined macros 30 | 31 | Attributes are set within squate brackets (`[]`), both on top of the document blocks and as a suffix of elements. 32 | 33 | 34 | == Parsing the document 35 | 36 | First, the internal parser (generated after the link:../pkg/parser/parser.peg[grammar] returns a "draft document" in which the sections are not embedded in a hierarchical manner, other blocks are also not attached to their parent section, blanklines are present, document attributes declarations, substitutions and reset macros are also present, and file inclusions have been processed. 37 | 38 | This so-called "draft document" is then processed to return a "final document" in which the sections are organized in a hierarchical manner with their child blocks (paragraphs, delimited blocks, etc.) attached to them. Also, all document attribute substitutions have been processed (sometimes resulting in new elements such as links) and removed. Blank line elements have been stripped off, too (except in delimited blocks). 39 | 40 | == HTMl5 Rendering 41 | 42 | The HTML5 renderer takes the final document and applies templates for each element. Having the sections in a hierarchical manner makes life easier in term of surrounding `
` tags. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bytesparadise/libasciidoc 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/alecthomas/chroma/v2 v2.3.0 7 | github.com/davecgh/go-spew v1.1.1 8 | github.com/felixge/fgtrace v0.1.0 9 | github.com/google/go-cmp v0.5.9 10 | github.com/mna/pigeon v1.1.0 11 | github.com/onsi/ginkgo/v2 v2.7.0 12 | github.com/onsi/gomega v1.24.2 13 | github.com/pkg/errors v0.9.1 14 | github.com/pkg/profile v1.6.0 15 | github.com/sirupsen/logrus v1.7.0 16 | github.com/spf13/cobra v1.1.1 17 | github.com/spf13/pflag v1.0.5 18 | github.com/stretchr/testify v1.8.0 19 | gopkg.in/yaml.v2 v2.4.0 20 | ) 21 | 22 | require ( 23 | github.com/DataDog/gostackparse v0.5.0 // indirect 24 | github.com/dlclark/regexp2 v1.4.0 // indirect 25 | github.com/go-logr/logr v1.2.3 // indirect 26 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect 27 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect 28 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | golang.org/x/mod v0.8.0 // indirect 31 | golang.org/x/net v0.17.0 // indirect 32 | golang.org/x/sys v0.13.0 // indirect 33 | golang.org/x/text v0.13.0 // indirect 34 | golang.org/x/tools v0.6.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | 38 | // include support for disabling unexported fields 39 | // TODO: still needed? 40 | replace github.com/davecgh/go-spew => github.com/flw-cn/go-spew v1.1.2-0.20200624141737-10fccbfd0b23 41 | -------------------------------------------------------------------------------- /libasciidoc_bench_test.go: -------------------------------------------------------------------------------- 1 | package libasciidoc_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/bytesparadise/libasciidoc" 8 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 9 | 10 | // pkgprofile "github.com/pkg/profile" 11 | "github.com/felixge/fgtrace" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func BenchmarkRealDocumentProcessing(b *testing.B) { 17 | // defer pkgprofile.Start(pkgprofile.MemProfile).Stop() 18 | defer fgtrace.Config{Dst: fgtrace.File("./tmp/fgtrace.json")}.Trace().Stop() //nolint:errcheck 19 | log.SetLevel(log.ErrorLevel) 20 | b.Run("demo.adoc", processDocument("./test/compat/demo.adoc")) 21 | b.Run("vertx-examples.adoc", processDocument("./test/bench/vertx-examples.adoc")) 22 | b.Run("mocking.adoc", processDocument("./test/bench/mocking.adoc")) 23 | } 24 | 25 | func processDocument(filename string) func(b *testing.B) { 26 | return func(b *testing.B) { 27 | for i := 0; i < b.N; i++ { 28 | out := &strings.Builder{} 29 | _, err := libasciidoc.ConvertFile(out, 30 | configuration.NewConfiguration( 31 | configuration.WithFilename(filename), 32 | configuration.WithCSS([]string{"path/to/style.css"}), 33 | configuration.WithHeaderFooter(true))) 34 | require.NoError(b, err) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libasciidoc_suite_test.go: -------------------------------------------------------------------------------- 1 | package libasciidoc_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestLibasciidoc(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Libasciidoc Suite") 13 | } 14 | -------------------------------------------------------------------------------- /make/bench.mk: -------------------------------------------------------------------------------- 1 | REPORTS_DIR=./tmp/bench/reports 2 | BENCH_COUNT ?= 10 3 | 4 | # Detecting GOPATH and removing trailing "/" if any 5 | GOPATH = $(realpath $(shell go env GOPATH)) 6 | 7 | .PHONY: bench 8 | ## run the top-level benchmarks 9 | bench: clean generate-optimized 10 | @mkdir -p $(REPORTS_DIR) 11 | @go test -tags bench -bench=. -benchmem -count=$(BENCH_COUNT) -run=XXX \ 12 | github.com/bytesparadise/libasciidoc \ 13 | | tee $(REPORTS_DIR)/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).bench 14 | @echo "generated $(REPORTS_DIR)/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).bench" 15 | 16 | .PHONY: bench-diff 17 | ## run the top-level benchmarks and compares with results of 'master' and 'v0.7.0' 18 | bench-diff: clean generate-optimized check-git-status 19 | @git config advice.detachedHead false 20 | @mkdir -p $(REPORTS_DIR) 21 | @go test -tags bench -bench=. -benchmem -count=$(BENCH_COUNT) -run=XXX \ 22 | github.com/bytesparadise/libasciidoc \ 23 | | tee $(REPORTS_DIR)/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).bench 24 | @echo "generated $(REPORTS_DIR)/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).bench" 25 | @git checkout master 26 | @go test -tags bench -bench=. -benchmem -count=$(BENCH_COUNT) -run=XXX \ 27 | github.com/bytesparadise/libasciidoc \ 28 | | tee $(REPORTS_DIR)/master.bench 29 | @echo "generated $(REPORTS_DIR)/master.bench" 30 | @git checkout v0.7.0 31 | @go test -tags bench -bench=. -benchmem -count=$(BENCH_COUNT) -run=XXX \ 32 | github.com/bytesparadise/libasciidoc \ 33 | | tee $(REPORTS_DIR)/v0.7.0.bench 34 | @echo "generated $(REPORTS_DIR)/v0.7.0.bench" 35 | @git checkout $(GITHUB_SHA) 36 | @echo "HEAD is now at $(shell git log -1 --format='%h') (expecting $(GITHUB_SHA))" 37 | @$(GOPATH)/bin/benchstat $(REPORTS_DIR)/master.bench $(REPORTS_DIR)/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).bench >> $(REPORTS_DIR)/diffs-master.txt 38 | @$(GOPATH)/bin/benchstat $(REPORTS_DIR)/v0.7.0.bench $(REPORTS_DIR)/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).bench >> $(REPORTS_DIR)/diffs-latest-release.txt 39 | 40 | .PHONY: print-bench-diff-master 41 | print-bench-diff-master: 42 | @cat $(REPORTS_DIR)/diffs-master.txt 43 | 44 | .PHONY: print-bench-diff-latest-release 45 | print-bench-diff-latest-release: 46 | @cat $(REPORTS_DIR)/diffs-latest-release.txt 47 | 48 | check-git-status: 49 | ifneq ("$(shell git status --porcelain)","") 50 | @echo "Repository contains uncommitted changes:" 51 | @git status --porcelain 52 | @exit 1 53 | else 54 | @echo "Repository has no uncommitted changes" 55 | endif 56 | 57 | -------------------------------------------------------------------------------- /make/clean.mk: -------------------------------------------------------------------------------- 1 | clean: 2 | @-rm pkg/parser/parser.go -------------------------------------------------------------------------------- /make/git.mk: -------------------------------------------------------------------------------- 1 | GIT_COMMIT_ID_SHORT := $(shell git rev-parse --short HEAD) 2 | ifneq ($(shell git status --porcelain),) 3 | GIT_COMMIT_ID_SHORT := $(GIT_COMMIT_ID_SHORT)-dirty 4 | endif 5 | 6 | # using the env var defined by GitHub Actions 7 | # (see https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables) 8 | GITHUB_SHA ?= $(shell git rev-parse --abbrev-ref HEAD) 9 | 10 | BUILD_TIME = `date -u '+%Y-%m-%dT%H:%M:%SZ'` -------------------------------------------------------------------------------- /make/help.mk: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | # Based on https://gist.github.com/rcmachado/af3db315e31383502660 3 | ## Display this help text 4 | help:/ 5 | $(info Available targets) 6 | $(info -----------------) 7 | @awk '/^[a-zA-Z\-%\_0-9]+:/ { \ 8 | helpMessage = match(lastLine, /^## (.*)/); \ 9 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 10 | if (helpMessage) { \ 11 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 12 | gsub(/##/, "\n ", helpMessage); \ 13 | printf "%-35s - %s\n", helpCommand, helpMessage; \ 14 | lastLine = "" \ 15 | } \ 16 | } \ 17 | { hasComment = match(lastLine, /^## (.*)/); \ 18 | if(hasComment) { \ 19 | lastLine=lastLine$$0; \ 20 | } \ 21 | else { \ 22 | lastLine = $$0 \ 23 | } \ 24 | }' $(MAKEFILE_LIST) -------------------------------------------------------------------------------- /make/lint.mk: -------------------------------------------------------------------------------- 1 | .PHONY: install-golangci-lint 2 | ## Install development tools. 3 | install-golangci-lint: 4 | @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin 5 | 6 | .PHONY: lint 7 | ## run golangci-lint against project 8 | lint: install-golangci-lint 9 | @$(shell go env GOPATH)/bin/golangci-lint run -v -c .golangci.yml ./... 10 | -------------------------------------------------------------------------------- /make/profile.mk: -------------------------------------------------------------------------------- 1 | .PHONY: profile 2 | ## run the profilers on the parser 3 | profile: clean generate-optimized 4 | @mkdir -p ./tmp/bench/reports 5 | @go test \ 6 | -tags=bench \ 7 | -cpuprofile=tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).cpu.prof \ 8 | -memprofile tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).mem.prof \ 9 | -bench=. \ 10 | -benchtime=10x \ 11 | github.com/bytesparadise/libasciidoc \ 12 | -run=XXX 13 | @echo "generate CPU reports..." 14 | @go tool pprof -text -output=tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).cpu.txt \ 15 | tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).cpu.prof 16 | ifndef CI 17 | @go tool pprof -svg -output=tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).cpu.svg \ 18 | tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).cpu.prof 19 | endif 20 | @echo "generate memory reports" 21 | @go tool pprof -text -output=tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).mem.txt \ 22 | tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).mem.prof 23 | ifndef CI 24 | @go tool pprof -svg -output=tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).mem.svg \ 25 | tmp/bench/reports/$(GITHUB_SHA)-$(GIT_COMMIT_ID_SHORT).mem.prof 26 | endif 27 | 28 | -------------------------------------------------------------------------------- /make/test.mk: -------------------------------------------------------------------------------- 1 | .PHONY: install-ginkgo 2 | ## Install development tools. 3 | install-ginkgo: 4 | @go install -v github.com/onsi/ginkgo/v2/ginkgo 5 | @ginkgo version 6 | 7 | .PHONY: test 8 | ## run all tests excluding fixtures and vendored packages 9 | test: clean generate-optimized install-ginkgo 10 | @ginkgo -r --randomize-all --randomize-suites --trace --race --compilers=0 11 | 12 | COVERPKGS := $(shell go list ./... | grep -v vendor | paste -sd "," -) 13 | 14 | .PHONY: test-with-coverage 15 | ## run all tests excluding fixtures and vendored packages 16 | test-with-coverage: generate-optimized install-ginkgo 17 | @echo $(COVERPKGS) 18 | @ginkgo -r --randomize-all --randomize-suites --trace --race --compilers=0 --cover -coverpkg $(COVERPKGS) 19 | 20 | .PHONY: test-fixtures 21 | ## run all fixtures tests 22 | test-fixtures: generate-optimized 23 | @ginkgo -r --randomize-all --randomize-suites --trace --race --compilers=0 -tags=fixtures --focus=fixtures 24 | -------------------------------------------------------------------------------- /make/verbose.mk: -------------------------------------------------------------------------------- 1 | # When you run make VERBOSE=1 (the default), executed commands will be printed 2 | # before executed. If you run make VERBOSE=2 verbose flags are turned on and 3 | # quiet flags are turned off for various commands. Use V_FLAG in places where 4 | # you can toggle on/off verbosity using -v. Use Q_FLAG in places where you can 5 | # toggle on/off quiet mode using -q. Use S_FLAG where you want to toggle on/off 6 | # silence mode using -s... 7 | VERBOSE ?= 1 8 | Q = @ 9 | Q_FLAG = -q 10 | QUIET_FLAG = --quiet 11 | V_FLAG = 12 | S_FLAG = -s 13 | X_FLAG = 14 | ifeq ($(VERBOSE),1) 15 | Q = 16 | endif 17 | ifeq ($(VERBOSE),2) 18 | Q = 19 | Q_FLAG = 20 | QUIET_FLAG = 21 | S_FLAG = 22 | V_FLAG = -v 23 | X_FLAG = -x 24 | endif -------------------------------------------------------------------------------- /pkg/configuration/macro_template.go: -------------------------------------------------------------------------------- 1 | package configuration 2 | 3 | import "io" 4 | 5 | // MacroTemplate an interface of template for user macro. 6 | type MacroTemplate interface { 7 | Execute(wr io.Writer, data interface{}) error 8 | } 9 | -------------------------------------------------------------------------------- /pkg/parser/document_preprocessing_builder_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 5 | "github.com/bytesparadise/libasciidoc/pkg/types" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("preprocessing condition stack", func() { 12 | 13 | It("should eval to false when pushing single disabled entry", func() { 14 | // given 15 | c := newConditions() 16 | ctx := NewParseContext(configuration.NewConfiguration()) 17 | // when 18 | eval := c.push(ctx, &types.IfdefCondition{ 19 | Name: "cookie", 20 | }) 21 | // then 22 | Expect(eval).To(BeFalse()) 23 | }) 24 | 25 | It("should eval to true when pushing single enabled entry", func() { 26 | // given 27 | c := newConditions() 28 | ctx := NewParseContext(configuration.NewConfiguration(configuration.WithAttribute("cookie", "yummy"))) 29 | // when 30 | eval := c.push(ctx, &types.IfdefCondition{ 31 | Name: "cookie", 32 | }) 33 | // then 34 | Expect(eval).To(BeTrue()) 35 | }) 36 | 37 | It("should update when pushing multiple entries", func() { 38 | // given 39 | c := newConditions() 40 | ctx := NewParseContext(configuration.NewConfiguration( 41 | configuration.WithAttribute("cookie", "yummy"), 42 | configuration.WithAttribute("chocolate", "dark"), 43 | configuration.WithAttribute("pasta", ""), 44 | )) 45 | // when 46 | eval := c.push(ctx, &types.IfdefCondition{ 47 | Name: "cookie", 48 | }) 49 | // then 50 | Expect(eval).To(BeTrue()) 51 | 52 | // when 53 | eval = c.push(ctx, &types.IfdefCondition{ 54 | Name: "cookie", 55 | }) 56 | // then 57 | Expect(eval).To(BeTrue()) 58 | 59 | // when 60 | eval = c.push(ctx, &types.IfdefCondition{ 61 | Name: "unknown", 62 | }) 63 | // then switch to false when condition is evaled to `false` 64 | Expect(eval).To(BeFalse()) 65 | 66 | // when 67 | eval = c.push(ctx, &types.IfdefCondition{ 68 | Name: "cookie", 69 | }) 70 | // then remains to `false` because of `unknown` condition 71 | Expect(eval).To(BeFalse()) 72 | 73 | // when 74 | eval = c.pop() 75 | // then remains to `false` because of `unknown` condition 76 | Expect(eval).To(BeFalse()) 77 | 78 | // when 79 | eval = c.pop() 80 | // then back to `true` 81 | Expect(eval).To(BeTrue()) 82 | 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /pkg/parser/document_processing.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 7 | "github.com/bytesparadise/libasciidoc/pkg/types" 8 | 9 | "github.com/davecgh/go-spew/spew" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | const bufferSize = 10 14 | 15 | // ParseDocument parses the content of the reader identitied by the filename and applies all the substitutions and arrangements 16 | func ParseDocument(r io.Reader, config *configuration.Configuration, opts ...Option) (*types.Document, error) { 17 | done := make(chan interface{}) 18 | defer close(done) 19 | 20 | footnotes := types.NewFootnotes() 21 | doc, err := Aggregate(NewParseContext(config, opts...), 22 | // SplitHeader(done, 23 | FilterOut(done, 24 | ArrangeLists(done, 25 | CollectFootnotes(footnotes, done, 26 | ApplySubstitutions(NewParseContext(config, opts...), done, // needs to be before 'ArrangeLists' 27 | RefineFragments(NewParseContext(config, opts...), r, done, 28 | ParseDocumentFragments(NewParseContext(config, opts...), r, done), 29 | ), 30 | ), 31 | ), 32 | ), 33 | ), 34 | // ), 35 | ) 36 | if err != nil { 37 | return nil, err 38 | } 39 | if len(footnotes.Notes) > 0 { 40 | doc.Footnotes = footnotes.Notes 41 | } 42 | if log.IsLevelEnabled(log.DebugLevel) { 43 | log.Debugf("parsed document:\n%s", spew.Sdump(doc)) 44 | } 45 | return doc, nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/parser/document_processing_collect_footnotes.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | func CollectFootnotes(n *types.Footnotes, done <-chan interface{}, fragmentStream <-chan types.DocumentFragment) chan types.DocumentFragment { 9 | processedFragmentStream := make(chan types.DocumentFragment, bufferSize) 10 | go func() { 11 | defer close(processedFragmentStream) 12 | for f := range fragmentStream { 13 | select { 14 | case <-done: 15 | log.WithField("pipeline_stage", "collect_footnotes").Debug("received 'done' signal") 16 | return 17 | case processedFragmentStream <- collectFootnotes(n, f): 18 | } 19 | } 20 | log.WithField("pipeline_stage", "collect_footnotes").Debug("done") 21 | }() 22 | return processedFragmentStream 23 | } 24 | 25 | func collectFootnotes(n *types.Footnotes, f types.DocumentFragment) types.DocumentFragment { 26 | if f.Error != nil { 27 | log.Debugf("skipping footnotes") 28 | return f 29 | } 30 | for _, e := range f.Elements { 31 | if log.IsLevelEnabled(log.DebugLevel) { 32 | log.Debugf("collecting footnotes in element of type '%T'", e) 33 | } 34 | if e, ok := e.(types.WithFootnotes); ok { 35 | e.SubstituteFootnotes(n) 36 | } 37 | } 38 | return f 39 | } 40 | -------------------------------------------------------------------------------- /pkg/parser/document_processing_raw_source_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/parser" 7 | "github.com/bytesparadise/libasciidoc/pkg/types" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = DescribeTable("'FileLocation' pattern", 14 | func(filename string, expected interface{}) { 15 | reader := strings.NewReader(filename) 16 | actual, err := parser.ParseReader(filename, reader, parser.Entrypoint("FileLocation")) 17 | Expect(err).ToNot(HaveOccurred()) 18 | // GinkgoT().Log("actual result: %s", spew.Sdump(actual)) 19 | // GinkgoT().Log("expected result: %s", spew.Sdump(expected)) 20 | Expect(actual).To(Equal(expected)) 21 | }, 22 | Entry("'chapter-a.adoc'", "chapter-a.adoc", &types.Location{ 23 | Path: "chapter-a.adoc", 24 | }), 25 | Entry("'chapter_a.adoc'", "chapter_a.adoc", &types.Location{ 26 | Path: "chapter_a.adoc", 27 | }), 28 | Entry("'../../test/includes/chapter_a.adoc'", "../../test/includes/chapter_a.adoc", &types.Location{ 29 | Path: "../../test/includes/chapter_a.adoc", 30 | }), 31 | Entry("'{includedir}/chapter-{foo}.adoc'", "{includedir}/chapter-{foo}.adoc", &types.Location{ 32 | Path: "{includedir}/chapter-{foo}.adoc", // attribute substitutions are treared as part of the string element 33 | }), 34 | Entry("'{scheme}://{path}'", "{scheme}://{path}", &types.Location{ 35 | Path: "{scheme}://{path}", 36 | }), 37 | ) 38 | 39 | var _ = DescribeTable("check asciidoc file", 40 | func(path string, expectation bool) { 41 | Expect(parser.IsAsciidoc(path)).To(Equal(expectation)) 42 | }, 43 | Entry("foo.adoc", "foo.adoc", true), 44 | Entry("foo.asc", "foo.asc", true), 45 | Entry("foo.ad", "foo.ad", true), 46 | Entry("foo.asciidoc", "foo.asciidoc", true), 47 | Entry("foo.txt", "foo.txt", true), 48 | Entry("foo.csv", "foo.csv", false), 49 | Entry("foo.go", "foo.go", false), 50 | ) 51 | -------------------------------------------------------------------------------- /pkg/parser/frontmatter_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | . "github.com/bytesparadise/libasciidoc/testsupport" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("front-matters", func() { 12 | 13 | Context("in final documents", func() { 14 | 15 | Context("yaml front-matter", func() { 16 | 17 | It("with simple attributes", func() { 18 | source := `--- 19 | title: a title 20 | author: Xavier 21 | --- 22 | 23 | first paragraph` 24 | expected := &types.Document{ 25 | Elements: []interface{}{ 26 | &types.FrontMatter{ 27 | Attributes: types.Attributes{ 28 | "title": "a title", // TODO: convert `title` attribute from front-matter into `doctitle` here ? 29 | "author": "Xavier", 30 | }, 31 | }, 32 | &types.Paragraph{ 33 | Elements: []interface{}{ 34 | &types.StringElement{Content: "first paragraph"}, 35 | }, 36 | }, 37 | }, 38 | } 39 | Expect(ParseDocument(source)).To(MatchDocument(expected)) 40 | }) 41 | 42 | It("empty front-matter", func() { 43 | source := `--- 44 | --- 45 | 46 | first paragraph` 47 | expected := &types.Document{ 48 | Elements: []interface{}{ 49 | &types.FrontMatter{}, 50 | &types.Paragraph{ 51 | Elements: []interface{}{ 52 | &types.StringElement{Content: "first paragraph"}, 53 | }, 54 | }, 55 | }, 56 | } 57 | Expect(ParseDocument(source)).To(MatchDocument(expected)) 58 | }) 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /pkg/parser/generate.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | //go:generate pigeon -optimize-parser -optimize-grammar -alternate-entrypoints DocumentRawLine,DocumentFragment,NormalGroup,AttributeStructuredValue,DelimitedBlockElements,AttributeDeclarationValue,FileLocation,IncludedFileLine,MarkdownQuoteAttribution,BlockAttributes,InlineAttributes,TableColumnsAttribute,LineRanges,TagRanges,DocumentAuthorFullName -o parser.go parser.peg 4 | -------------------------------------------------------------------------------- /pkg/parser/inline_button_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | . "github.com/bytesparadise/libasciidoc/testsupport" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("inline buttons", func() { 12 | 13 | Context("in final documents", func() { 14 | 15 | It("when experimental is enabled", func() { 16 | source := `:experimental: 17 | 18 | Click on btn:[OK].` 19 | expected := &types.Document{ 20 | Elements: []interface{}{ 21 | &types.AttributeDeclaration{ 22 | Name: "experimental", 23 | }, 24 | &types.Paragraph{ 25 | Elements: []interface{}{ 26 | &types.StringElement{ 27 | Content: "Click on ", 28 | }, 29 | &types.InlineButton{ 30 | Attributes: types.Attributes{ 31 | types.AttrButtonLabel: "OK", 32 | }, 33 | }, 34 | &types.StringElement{ 35 | Content: ".", 36 | }, 37 | }, 38 | }, 39 | }, 40 | } 41 | Expect(ParseDocument(source)).To(MatchDocument(expected)) 42 | }) 43 | 44 | It("when experimental is not enabled", func() { 45 | source := `Click on btn:[OK].` 46 | expected := &types.Document{ 47 | Elements: []interface{}{ 48 | &types.Paragraph{ 49 | Elements: []interface{}{ 50 | &types.StringElement{ 51 | Content: "Click on btn:[OK].", 52 | }, 53 | }, 54 | }, 55 | }, 56 | } 57 | Expect(ParseDocument(source)).To(MatchDocument(expected)) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /pkg/parser/line_break_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | . "github.com/bytesparadise/libasciidoc/testsupport" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("line breaks", func() { 12 | 13 | Context("in final documents", func() { 14 | 15 | It("simple case", func() { 16 | source := `since 2021 +` 17 | expected := &types.Document{ 18 | Elements: []interface{}{ 19 | &types.Paragraph{ 20 | Elements: []interface{}{ 21 | &types.StringElement{ 22 | Content: "since 2021", 23 | }, 24 | &types.LineBreak{}, 25 | }, 26 | }, 27 | }, 28 | } 29 | Expect(ParseDocument(source)).To(MatchDocument(expected)) 30 | }) 31 | 32 | It("after punctuation", func() { 33 | source := `:author: Xavier 34 | Copyright (C) 2021 {author}. +` 35 | expected := &types.Document{ 36 | Elements: []interface{}{ 37 | &types.AttributeDeclaration{ 38 | Name: types.AttrAuthor, 39 | Value: "Xavier", 40 | }, 41 | &types.Paragraph{ 42 | Elements: []interface{}{ 43 | &types.StringElement{ 44 | Content: "Copyright ", 45 | }, 46 | &types.Symbol{ 47 | Name: "(C)", 48 | }, 49 | &types.StringElement{ 50 | Content: " 2021 Xavier.", 51 | }, 52 | &types.LineBreak{}, 53 | }, 54 | }, 55 | }, 56 | } 57 | Expect(ParseDocument(source)).To(MatchDocument(expected)) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /pkg/parser/paragragh_debug_test.go: -------------------------------------------------------------------------------- 1 | // +build stats 2 | 3 | package parser_test 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | 9 | "github.com/bytesparadise/libasciidoc/pkg/parser" 10 | "github.com/bytesparadise/libasciidoc/pkg/types" 11 | . "github.com/bytesparadise/libasciidoc/testsupport" 12 | 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("paragraphs", func() { 18 | 19 | It("multiline paragraph with rich content", func() { 20 | source := `:fds-version: 6.7.4 21 | 22 | PyroSim is a graphical user interface for the https://github.com/firemodels/fds/releases/tag/FDS{fds-version}/[Fire Dynamics Simulator (FDS) version {fds-version}]. 23 | FDS is closely integrated into PyroSim. 24 | FDS models can predict smoke, temperature, carbon monoxide, and other substances during fires. 25 | The results of these simulations are used to ensure the safety of buildings before construction, evaluate safety options of existing buildings, reconstruct fires for post-accident investigation, and assist in firefighter training. 26 | ` 27 | expected := &types.Document{ 28 | Elements: []interface{}{ 29 | &types.AttributeDeclaration{ 30 | Name: "fds-version", 31 | Value: string("6.7.4"), 32 | }, 33 | &types.Paragraph{ 34 | Elements: []interface{}{ 35 | &types.StringElement{ 36 | Content: "PyroSim is a graphical user interface for the ", 37 | }, 38 | // https://github.com/firemodels/fds/releases/tag/FDS{fds-version}/[Fire Dynamics Simulator (FDS) version {fds-version}] 39 | &types.InlineLink{ 40 | Attributes: types.Attributes{ 41 | types.AttrInlineLinkText: "Fire Dynamics Simulator (FDS) version 6.7.4", 42 | }, 43 | Location: &types.Location{ 44 | Scheme: "https://", 45 | Path: "github.com/firemodels/fds/releases/tag/FDS6.7.4/", 46 | }, 47 | }, 48 | &types.StringElement{ 49 | Content: ".\nFDS is closely integrated into PyroSim.\nFDS models can predict smoke, temperature, carbon monoxide, and other substances during fires.\nThe results of these simulations are used to ensure the safety of buildings before construction, evaluate safety options of existing buildings, reconstruct fires for post-accident investigation, and assist in firefighter training.", 50 | }, 51 | }, 52 | }, 53 | }, 54 | } 55 | stats := parser.Stats{} 56 | Expect(ParseDocument(source, parser.Debug(true), parser.Statistics(&stats, "no match"))).To(MatchDocument(expected)) 57 | fmt.Printf("ExprCnt: %d\n", stats.ExprCnt) 58 | result, _ := json.MarshalIndent(stats.ChoiceAltCnt, " ", " ") 59 | fmt.Printf("ChoiceAltCnt: \n%s\n", result) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /pkg/parser/paragragh_stats_test.go: -------------------------------------------------------------------------------- 1 | // +build stats 2 | 3 | package parser_test 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | 9 | "github.com/bytesparadise/libasciidoc/pkg/parser" 10 | "github.com/bytesparadise/libasciidoc/pkg/types" 11 | . "github.com/bytesparadise/libasciidoc/testsupport" 12 | 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("paragraphs", func() { 18 | 19 | It("multiline paragraph with rich content", func() { 20 | source := `:fds-version: 6.7.4 21 | 22 | PyroSim is a graphical user interface for the https://github.com/firemodels/fds/releases/tag/FDS{fds-version}/[Fire Dynamics Simulator (FDS) version {fds-version}]. 23 | FDS is closely integrated into PyroSim. 24 | FDS models can predict smoke, temperature, carbon monoxide, and other substances during fires. 25 | The results of these simulations are used to ensure the safety of buildings before construction, evaluate safety options of existing buildings, reconstruct fires for post-accident investigation, and assist in firefighter training. 26 | ` 27 | expected := &types.Document{ 28 | Elements: []interface{}{ 29 | &types.AttributeDeclaration{ 30 | Name: "fds-version", 31 | Value: string("6.7.4"), 32 | }, 33 | &types.Paragraph{ 34 | Elements: []interface{}{ 35 | &types.StringElement{ 36 | Content: "PyroSim is a graphical user interface for the ", 37 | }, 38 | // https://github.com/firemodels/fds/releases/tag/FDS{fds-version}/[Fire Dynamics Simulator (FDS) version {fds-version}] 39 | &types.InlineLink{ 40 | Attributes: types.Attributes{ 41 | types.AttrInlineLinkText: "Fire Dynamics Simulator (FDS) version 6.7.4", 42 | }, 43 | Location: &types.Location{ 44 | Scheme: "https://", 45 | Path: "github.com/firemodels/fds/releases/tag/FDS6.7.4/", 46 | }, 47 | }, 48 | &types.StringElement{ 49 | Content: ".\nFDS is closely integrated into PyroSim.\nFDS models can predict smoke, temperature, carbon monoxide, and other substances during fires.\nThe results of these simulations are used to ensure the safety of buildings before construction, evaluate safety options of existing buildings, reconstruct fires for post-accident investigation, and assist in firefighter training.", 50 | }, 51 | }, 52 | }, 53 | }, 54 | } 55 | stats := parser.Stats{} 56 | Expect(ParseDocument(source, parser.Debug(true), parser.Statistics(&stats, "no match"))).To(MatchDocument(expected)) 57 | fmt.Printf("ExprCnt: %d\n", stats.ExprCnt) 58 | result, _ := json.MarshalIndent(stats.ChoiceAltCnt, " ", " ") 59 | fmt.Printf("ChoiceAltCnt: \n%s\n", result) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /pkg/parser/parser_suite_test.go: -------------------------------------------------------------------------------- 1 | package parser_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | 9 | _ "github.com/bytesparadise/libasciidoc/testsupport" 10 | ) 11 | 12 | func TestParser(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Parser Suite") 15 | } 16 | -------------------------------------------------------------------------------- /pkg/parser/stats.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | func PrettyPrintStats(stats *Stats) string { 10 | rules := make([]string, 0, len(stats.ChoiceAltCnt)) 11 | for r := range stats.ChoiceAltCnt { 12 | rules = append(rules, r) 13 | } 14 | sort.Strings(rules) 15 | buf := &strings.Builder{} 16 | for _, r := range rules { 17 | buf.WriteString(fmt.Sprintf("%s ", r)) 18 | for choice, count := range stats.ChoiceAltCnt[r] { 19 | buf.WriteString(fmt.Sprintf("| %s->%dx ", choice, count)) 20 | } 21 | buf.WriteString("\n") 22 | } 23 | buf.WriteString(fmt.Sprintf("---------------\nTotal: %d\n\n", stats.ExprCnt)) 24 | return buf.String() 25 | } 26 | -------------------------------------------------------------------------------- /pkg/renderer/renderer.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 8 | "github.com/bytesparadise/libasciidoc/pkg/renderer/sgml/html5" 9 | "github.com/bytesparadise/libasciidoc/pkg/renderer/sgml/xhtml5" 10 | "github.com/bytesparadise/libasciidoc/pkg/types" 11 | ) 12 | 13 | func Render(doc *types.Document, config *configuration.Configuration, output io.Writer) (types.Metadata, error) { 14 | switch config.BackEnd { 15 | case "html", "html5": 16 | return html5.Render(doc, config, output) 17 | case "xhtml", "xhtml5": 18 | return xhtml5.Render(doc, config, output) 19 | default: 20 | return types.Metadata{}, fmt.Errorf("backend '%s' not supported", config.BackEnd) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pkg/renderer/renderer_suite_test.go: -------------------------------------------------------------------------------- 1 | package renderer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | 9 | _ "github.com/bytesparadise/libasciidoc/testsupport" 10 | ) 11 | 12 | func TestRenderer(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Renderer Suite") 15 | } 16 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/attribution.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import "github.com/bytesparadise/libasciidoc/pkg/types" 4 | 5 | // Attribution a document block attribution 6 | type Attribution struct { // TODO: unexport this type? 7 | First string 8 | Second string 9 | } 10 | 11 | func newAttribution(b types.WithAttributes) Attribution { 12 | result := Attribution{} 13 | if author, found := b.GetAttributes().GetAsString(types.AttrQuoteAuthor); found { 14 | result.First = author 15 | if title, found := b.GetAttributes().GetAsString(types.AttrQuoteTitle); found { 16 | result.Second = title 17 | } 18 | } else if title, found := b.GetAttributes().GetAsString(types.AttrQuoteTitle); found { 19 | result.First = title 20 | } 21 | return result 22 | } 23 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/blank_line.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func (r *sgmlRenderer) renderLineBreak() (string, error) { 8 | buf := &strings.Builder{} 9 | tmpl, err := r.lineBreak() 10 | if err != nil { 11 | return "", err 12 | } 13 | if err := tmpl.Execute(buf, nil); err != nil { 14 | return "", err 15 | } 16 | return buf.String(), nil 17 | } 18 | 19 | func (r *sgmlRenderer) renderThematicBreak() (string, error) { 20 | buf := &strings.Builder{} 21 | tmpl, err := r.thematicBreak() 22 | if err != nil { 23 | return "", err 24 | } 25 | if err := tmpl.Execute(buf, nil); err != nil { 26 | return "", err 27 | } 28 | return buf.String(), nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | ) 8 | 9 | func (r *sgmlRenderer) renderDelimitedBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 10 | switch b.Kind { 11 | case types.Example: 12 | if b.Attributes.Has(types.AttrStyle) { 13 | return r.renderAdmonitionBlock(ctx, b) 14 | } 15 | return r.renderExampleBlock(ctx, b) 16 | case types.Fenced: 17 | return r.renderFencedBlock(ctx, b) 18 | case types.Literal: 19 | return r.renderLiteralBlock(ctx, b) 20 | case types.Listing: 21 | return r.renderListingBlock(ctx, b) 22 | case types.MarkdownQuote: 23 | return r.renderMarkdownQuoteBlock(ctx, b) 24 | case types.Passthrough: 25 | return r.renderPassthroughBlock(ctx, b) 26 | case types.Quote: 27 | return r.renderQuoteBlock(ctx, b) 28 | case types.Verse: 29 | return r.renderVerseBlock(ctx, b) 30 | case types.Sidebar: 31 | return r.renderSidebarBlock(ctx, b) 32 | case types.Open: 33 | return r.renderOpenBlock(ctx, b) 34 | default: 35 | return "", fmt.Errorf("unsupported kind of delimited block: '%s'", b.Kind) 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block_discard_lines.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | ) 6 | 7 | func discardBlankLines(lines []interface{}) []interface{} { 8 | // discard blank elements at the end 9 | // log.Debugf("discarding blank lines on %d elements...", len(lines)) 10 | filteredLines := make([]interface{}, len(lines)) 11 | copy(filteredLines, lines) 12 | // leading empty lines 13 | for { 14 | if len(filteredLines) == 0 { 15 | break 16 | } 17 | if _, ok := filteredLines[0].(types.BlankLine); ok { 18 | // remove last element of the slice since it's a blank line 19 | filteredLines = filteredLines[:len(filteredLines)-1] 20 | } else { 21 | break 22 | } 23 | } 24 | // trailing empty lines 25 | for { 26 | if len(filteredLines) == 0 { 27 | break 28 | } 29 | if _, ok := filteredLines[len(filteredLines)-1].(types.BlankLine); ok { 30 | // remove last element of the slice since it's a blank line 31 | filteredLines = filteredLines[:len(filteredLines)-1] 32 | } else { 33 | break 34 | } 35 | } 36 | return filteredLines 37 | } 38 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block_fenced.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func (r *sgmlRenderer) renderFencedBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 11 | previousWithinDelimitedBlock := ctx.withinDelimitedBlock 12 | defer func() { 13 | ctx.withinDelimitedBlock = previousWithinDelimitedBlock 14 | }() 15 | ctx.withinDelimitedBlock = true 16 | content, err := r.renderElements(ctx, b.Elements) 17 | if err != nil { 18 | return "", errors.Wrap(err, "unable to render fenced block content") 19 | } 20 | roles, err := r.renderElementRoles(ctx, b.Attributes) 21 | if err != nil { 22 | return "", errors.Wrap(err, "unable to render fenced block roles") 23 | } 24 | title, err := r.renderElementTitle(ctx, b.Attributes) 25 | if err != nil { 26 | return "", errors.Wrap(err, "unable to render fenced block roles") 27 | } 28 | return r.execute(r.fencedBlock, struct { 29 | Context *context 30 | ID string 31 | Title string 32 | Roles string 33 | Content string 34 | }{ 35 | Context: ctx, 36 | ID: r.renderElementID(b.Attributes), 37 | Title: title, 38 | Roles: roles, 39 | Content: strings.Trim(content, "\n"), 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block_listing.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func (r *sgmlRenderer) renderListingBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 11 | if k, found := b.Attributes[types.AttrStyle]; found && k == types.Source { 12 | return r.renderSourceBlock(ctx, b) 13 | } 14 | previousWithinDelimitedBlock := ctx.withinDelimitedBlock 15 | defer func() { 16 | ctx.withinDelimitedBlock = previousWithinDelimitedBlock 17 | }() 18 | ctx.withinDelimitedBlock = true 19 | content, err := r.renderElements(ctx, b.Elements) 20 | if err != nil { 21 | return "", errors.Wrap(err, "unable to render listing block content") 22 | } 23 | roles, err := r.renderElementRoles(ctx, b.Attributes) 24 | if err != nil { 25 | return "", errors.Wrap(err, "unable to render listing block roles") 26 | } 27 | title, err := r.renderElementTitle(ctx, b.Attributes) 28 | if err != nil { 29 | return "", errors.Wrap(err, "unable to render listing block title") 30 | } 31 | return r.execute(r.listingBlock, struct { 32 | Context *context 33 | ID string 34 | Title string 35 | Roles string 36 | Content string 37 | }{ 38 | Context: ctx, 39 | ID: r.renderElementID(b.Attributes), 40 | Title: title, 41 | Roles: roles, 42 | Content: strings.Trim(content, "\n"), 43 | }) 44 | } 45 | 46 | func (r *sgmlRenderer) renderListingParagraph(ctx *context, p *types.Paragraph) (string, error) { 47 | content, err := r.renderElements(ctx, p.Elements) 48 | if err != nil { 49 | return "", errors.Wrap(err, "unable to render listing block content") 50 | } 51 | roles, err := r.renderElementRoles(ctx, p.Attributes) 52 | if err != nil { 53 | return "", errors.Wrap(err, "unable to render listing block roles") 54 | } 55 | title, err := r.renderElementTitle(ctx, p.Attributes) 56 | if err != nil { 57 | return "", errors.Wrap(err, "unable to render listing paragraph roles") 58 | } 59 | return r.execute(r.listingBlock, struct { 60 | Context *context 61 | ID string 62 | Title string 63 | Roles string 64 | Content string 65 | }{ 66 | Context: ctx, 67 | ID: r.renderElementID(p.Attributes), 68 | Title: title, 69 | Roles: roles, 70 | Content: content, 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block_markdown_quote.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func (r *sgmlRenderer) renderMarkdownQuoteBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 11 | content, err := r.renderElements(ctx, b.Elements) 12 | if err != nil { 13 | return "", errors.Wrap(err, "unable to render markdown quote block content") 14 | } 15 | roles, err := r.renderElementRoles(ctx, b.Attributes) 16 | if err != nil { 17 | return "", errors.Wrap(err, "unable to render markdown quote block roles") 18 | } 19 | attribution := newAttribution(b) 20 | title, err := r.renderElementTitle(ctx, b.Attributes) 21 | if err != nil { 22 | return "", errors.Wrap(err, "unable to render markdown quote block title") 23 | } 24 | return r.execute(r.markdownQuoteBlock, struct { 25 | Context *context 26 | ID string 27 | Title string 28 | Roles string 29 | Attribution Attribution 30 | Content string 31 | }{ 32 | Context: ctx, 33 | ID: r.renderElementID(b.Attributes), 34 | Title: title, 35 | Roles: roles, 36 | Attribution: attribution, 37 | Content: strings.Trim(content, "\n"), 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block_open.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func (r *sgmlRenderer) renderOpenBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 9 | blocks := discardBlankLines(b.Elements) 10 | content, err := r.renderElements(ctx, blocks) 11 | if err != nil { 12 | return "", errors.Wrap(err, "unable to render open block content") 13 | } 14 | roles, err := r.renderElementRoles(ctx, b.Attributes) 15 | if err != nil { 16 | return "", errors.Wrap(err, "unable to render open block roles") 17 | } 18 | title, err := r.renderElementTitle(ctx, b.Attributes) 19 | if err != nil { 20 | return "", errors.Wrap(err, "unable to render open block title") 21 | } 22 | return r.execute(r.openBlock, struct { 23 | Context *context 24 | ID string 25 | Title string 26 | Roles string 27 | Content string 28 | }{ 29 | Context: ctx, 30 | ID: r.renderElementID(b.Attributes), 31 | Title: title, 32 | Roles: roles, 33 | Content: content, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block_passthrough.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func (r *sgmlRenderer) renderPassthroughBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 11 | previousWithinDelimitedBlock := ctx.withinDelimitedBlock 12 | defer func() { 13 | ctx.withinDelimitedBlock = previousWithinDelimitedBlock 14 | }() 15 | ctx.withinDelimitedBlock = true 16 | content, err := r.renderElements(ctx, b.Elements) 17 | if err != nil { 18 | return "", errors.Wrap(err, "unable to render passthrough block content") 19 | } 20 | roles, err := r.renderElementRoles(ctx, b.Attributes) 21 | if err != nil { 22 | return "", errors.Wrap(err, "unable to render passthrough block roles") 23 | } 24 | return r.execute(r.passthroughBlock, struct { 25 | Context *context 26 | ID string 27 | Roles string 28 | Content string 29 | }{ 30 | Context: ctx, 31 | ID: r.renderElementID(b.Attributes), 32 | Roles: roles, 33 | Content: strings.Trim(content, "\n"), 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block_quote.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | "github.com/pkg/errors" 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func (r *sgmlRenderer) renderQuoteBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 10 | content, err := r.renderElements(ctx, b.Elements) 11 | if err != nil { 12 | return "", errors.Wrap(err, "unable to render quote block content") 13 | } 14 | roles, err := r.renderElementRoles(ctx, b.Attributes) 15 | if err != nil { 16 | return "", errors.Wrap(err, "unable to render quote block roles") 17 | } 18 | attribution := newAttribution(b) 19 | title, err := r.renderElementTitle(ctx, b.Attributes) 20 | if err != nil { 21 | return "", errors.Wrap(err, "unable to render quote block title") 22 | } 23 | return r.execute(r.quoteBlock, struct { 24 | Context *context 25 | ID string 26 | Title string 27 | Roles string 28 | Attribution Attribution 29 | Content string 30 | }{ 31 | Context: ctx, 32 | ID: r.renderElementID(b.Attributes), 33 | Title: title, 34 | Roles: roles, 35 | Attribution: attribution, 36 | Content: content, 37 | }) 38 | } 39 | 40 | func (r *sgmlRenderer) renderQuoteParagraph(ctx *context, p *types.Paragraph) (string, error) { 41 | log.Debug("rendering quote paragraph...") 42 | content, err := r.renderParagraphElements(ctx, p) 43 | if err != nil { 44 | return "", errors.Wrap(err, "unable to render quote paragraph lines") 45 | } 46 | attribution := newAttribution(p) 47 | title, err := r.renderElementTitle(ctx, p.Attributes) 48 | if err != nil { 49 | return "", errors.Wrap(err, "unable to render callout list roles") 50 | } 51 | return r.execute(r.quoteParagraph, struct { 52 | Context *context 53 | ID string 54 | Title string 55 | Attribution Attribution 56 | Content string 57 | }{ 58 | Context: ctx, 59 | ID: r.renderElementID(p.Attributes), 60 | Title: title, 61 | Attribution: attribution, 62 | Content: string(content), 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block_sidebar.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func (r *sgmlRenderer) renderSidebarBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 9 | blocks := discardBlankLines(b.Elements) 10 | content, err := r.renderElements(ctx, blocks) 11 | if err != nil { 12 | return "", errors.Wrap(err, "unable to render sidebar block content") 13 | } 14 | roles, err := r.renderElementRoles(ctx, b.Attributes) 15 | if err != nil { 16 | return "", errors.Wrap(err, "unable to render sidebar block roles") 17 | } 18 | title, err := r.renderElementTitle(ctx, b.Attributes) 19 | if err != nil { 20 | return "", errors.Wrap(err, "unable to render sidebar block title") 21 | } 22 | return r.execute(r.sidebarBlock, struct { 23 | Context *context 24 | ID string 25 | Title string 26 | Roles string 27 | Content string 28 | }{ 29 | Context: ctx, 30 | ID: r.renderElementID(b.Attributes), 31 | Title: title, 32 | Roles: roles, 33 | Content: content, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/delimited_block_verse.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/pkg/errors" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func (r *sgmlRenderer) renderVerseBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 12 | roles, err := r.renderElementRoles(ctx, b.Attributes) 13 | if err != nil { 14 | return "", errors.Wrap(err, "unable to render verser block roles") 15 | } 16 | previousWithinDelimitedBlock := ctx.withinDelimitedBlock 17 | defer func() { 18 | ctx.withinDelimitedBlock = previousWithinDelimitedBlock 19 | }() 20 | ctx.withinDelimitedBlock = true 21 | content, err := r.renderElements(ctx, b.Elements) 22 | if err != nil { 23 | return "", errors.Wrap(err, "unable to render verse block content") 24 | } 25 | attribution := newAttribution(b) 26 | title, err := r.renderElementTitle(ctx, b.Attributes) 27 | if err != nil { 28 | return "", errors.Wrap(err, "unable to render verse block title") 29 | } 30 | return r.execute(r.verseBlock, struct { 31 | Context *context 32 | ID string 33 | Title string 34 | Roles string 35 | Attribution Attribution 36 | Content string 37 | }{ 38 | Context: ctx, 39 | ID: r.renderElementID(b.Attributes), 40 | Title: title, 41 | Roles: roles, 42 | Attribution: attribution, 43 | Content: strings.Trim(string(content), "\n"), 44 | }) 45 | } 46 | 47 | func (r *sgmlRenderer) renderVerseParagraph(ctx *context, p *types.Paragraph) (string, error) { 48 | log.Debug("rendering verse paragraph...") 49 | content, err := RenderParagraphElements(p) 50 | if err != nil { 51 | return "", errors.Wrap(err, "unable to render verse paragraph lines") 52 | } 53 | attribution := newAttribution(p) 54 | title, err := r.renderElementTitle(ctx, p.Attributes) 55 | if err != nil { 56 | return "", errors.Wrap(err, "unable to render callout list roles") 57 | } 58 | return r.execute(r.verseParagraph, struct { 59 | Context *context 60 | ID string 61 | Title string 62 | Attribution Attribution 63 | Content string 64 | }{ 65 | Context: ctx, 66 | ID: r.renderElementID(p.Attributes), 67 | Title: title, 68 | Attribution: attribution, 69 | Content: string(content), 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/element_id.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | texttemplate "text/template" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | ) 8 | 9 | func (r *sgmlRenderer) renderElementID(attrs types.Attributes) string { 10 | if id, ok := attrs[types.AttrID].(string); ok { 11 | return string(texttemplate.HTMLEscapeString(id)) 12 | } 13 | return "" 14 | } 15 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/element_role.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/bytesparadise/libasciidoc/pkg/types" 8 | ) 9 | 10 | func (r *sgmlRenderer) renderElementRoles(ctx *context, attrs types.Attributes) (string, error) { 11 | if roles, ok := attrs[types.AttrRoles].(types.Roles); ok { 12 | result := make([]string, len(roles)) 13 | for i, e := range roles { 14 | s, err := r.renderElementRole(ctx, e) 15 | if err != nil { 16 | return "", err 17 | } 18 | result[i] = s 19 | } 20 | return strings.Join(result, " "), nil 21 | } 22 | return "", nil 23 | } 24 | 25 | // Image roles add float and alignment attributes -- we turn these into roles. 26 | func (r *sgmlRenderer) renderImageRoles(ctx *context, attrs types.Attributes) (string, error) { 27 | var result []string 28 | if val, found := attrs.GetAsString(types.AttrFloat); found { 29 | result = append(result, val) 30 | } 31 | if val, found := attrs.GetAsString(types.AttrImageAlign); found { 32 | result = append(result, "text-"+val) 33 | } 34 | if roles, ok := attrs[types.AttrRoles].(types.Roles); ok { 35 | for _, e := range roles { 36 | s, err := r.renderElementRole(ctx, e) 37 | if err != nil { 38 | return "", err 39 | } 40 | result = append(result, s) 41 | } 42 | } 43 | // log.Debugf("rendered image roles: '%s'", result) 44 | return strings.Join(result, " "), nil 45 | } 46 | 47 | func (r *sgmlRenderer) renderElementRole(ctx *context, role interface{}) (string, error) { 48 | result := strings.Builder{} 49 | switch role := role.(type) { 50 | case string: 51 | result.WriteString(role) 52 | case []interface{}: 53 | // when the role is made of strings and special characters 54 | for _, e := range role { 55 | s, err := r.renderElement(ctx, e) 56 | if err != nil { 57 | return "", err 58 | } 59 | result.WriteString(s) 60 | } 61 | default: 62 | return "", fmt.Errorf("unexpected type of element while rendering element role: '%T'", role) 63 | } 64 | // log.Debugf("rendered role: '%s'", result.String()) 65 | return result.String(), nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/element_style.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | texttemplate "text/template" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | ) 8 | 9 | func (r *sgmlRenderer) renderElementStyle(attrs types.Attributes) string { 10 | if id, ok := attrs[types.AttrStyle].(string); ok { 11 | return string(texttemplate.HTMLEscapeString(id)) 12 | } 13 | return "" 14 | } 15 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/article.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | articleTmpl = ` 5 | 6 | 7 | 8 | 9 | 10 | {{ if .Generator }} 11 | {{ end }}{{ if .Description }} 12 | {{ end }}{{ if .Authors }} 13 | {{ end }}{{ range $css := .CSS }} 14 | {{ end }}{{ .Title }} 15 | 16 | 17 | {{ if .IncludeHTMLBodyHeader }}{{ .Header }}{{ end }}
18 | {{ .Content }}
19 | {{ if .IncludeHTMLBodyFooter }} 25 | {{ end }} 26 | 27 | ` 28 | 29 | articleHeaderTmpl = ` 32 | ` 33 | ) 34 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/blank_line.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | lineBreakTmpl = "
" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/blank_line_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("blank lines", func() { 11 | 12 | It("blank line between 2 paragraphs", func() { 13 | source := `first paragraph 14 | 15 | second paragraph` 16 | expected := `
17 |

first paragraph

18 |
19 |
20 |

second paragraph

21 |
22 | ` 23 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 24 | }) 25 | 26 | It("blank line with spaces and tabs between 2 paragraphs", func() { 27 | source := `first paragraph 28 | 29 | second paragraph` 30 | expected := `
31 |

first paragraph

32 |
33 |
34 |

second paragraph

35 |
36 | ` 37 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 38 | }) 39 | 40 | It("blank lines (tabs) at end of document", func() { 41 | source := `first paragraph 42 | 43 | 44 | ` 45 | expected := `
46 |

first paragraph

47 |
48 | ` 49 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 50 | }) 51 | 52 | It("blank lines (spaces) at end of document", func() { 53 | source := `first paragraph 54 | 55 | 56 | ` 57 | expected := `
58 |

first paragraph

59 |
60 | ` 61 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/callout_list.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | // initializes the sgml 4 | const ( 5 | calloutListTmpl = `\n" + 8 | "{{ if .Title }}
{{ .Title }}
\n{{ end }}" + 9 | "
    \n" + 10 | "{{ .Content }}" + 11 | "
\n
\n" 12 | 13 | // NB: The items are numbered sequentially. 14 | calloutListElementTmpl = "
  • \n{{ .Content }}
  • \n" 15 | 16 | // This should probably have been a , but for compatibility we use 17 | calloutRefTmpl = "({{ .Ref }})" 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/comment_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("comments", func() { 11 | 12 | Context("single line comments", func() { 13 | 14 | It("single line comment alone", func() { 15 | source := `// A single-line comment.` 16 | expected := "" 17 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 18 | }) 19 | 20 | It("single line comment at end of line", func() { 21 | source := `foo // A single-line comment.` 22 | expected := `
    23 |

    foo // A single-line comment.

    24 |
    25 | ` 26 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 27 | }) 28 | 29 | It("single line comment within a paragraph", func() { 30 | source := `a first line 31 | // A single-line comment. 32 | another line` 33 | expected := `
    34 |

    a first line 35 | another line

    36 |
    37 | ` 38 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 39 | }) 40 | }) 41 | 42 | Context("comment blocks", func() { 43 | 44 | It("comment block alone", func() { 45 | source := `//// 46 | a *comment* block 47 | with multiple lines 48 | ////` 49 | expected := "" 50 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 51 | }) 52 | 53 | It("comment block with paragraphs around", func() { 54 | source := `a first paragraph 55 | 56 | //// 57 | a *comment* block 58 | with multiple lines 59 | //// 60 | 61 | a second paragraph` 62 | expected := `
    63 |

    a first paragraph

    64 |
    65 |
    66 |

    a second paragraph

    67 |
    68 | ` 69 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 70 | }) 71 | }) 72 | 73 | }) 74 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/cross_reference.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | internalCrossReferenceTmpl = `{{ .Label }}` 5 | externalCrossReferenceTmpl = `{{ .Label }}` 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_admonition_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | admonitionBlockTmpl = `
    \n" + 6 | "\n" + 7 | "\n" + 8 | "\n" + 9 | "\n\n
    \n{{ .Icon }}\n\n" + 10 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 11 | "{{ .Content }}" + 12 | "
    \n
    \n" 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_example_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | exampleBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Caption }}{{ .Title }}
    \n{{ end }}" + 7 | "
    \n" + 8 | "{{ .Content }}" + 9 | "
    \n" + 10 | "
    \n" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_fenced_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("delimited blocks", func() { 11 | 12 | Context("fenced blocks", func() { 13 | 14 | It("fenced block with surrounding empty lines", func() { 15 | source := "```\n\nsome source code \n\nhere \n\n\n\n```" 16 | expected := `
    17 |
    18 |
    some source code
    19 | 
    20 | here
    21 |
    22 |
    23 | ` 24 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 25 | }) 26 | 27 | It("fenced block with empty lines", func() { 28 | source := "```\n\n\n\n```" 29 | expected := `
    30 |
    31 |
    32 |
    33 |
    34 | ` 35 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 36 | }) 37 | 38 | It("fenced block with id and title and empty end lines", func() { 39 | source := "[#id-for-fences]\n.fenced block title\n```\nsome source code\n\nhere\n\n\n\n```" 40 | expected := `
    41 |
    fenced block title
    42 |
    43 |
    some source code
    44 | 
    45 | here
    46 |
    47 |
    48 | ` 49 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 50 | }) 51 | 52 | It("fenced block with external link inside amd empty end line", func() { 53 | source := "```" + "\n" + 54 | "a https://website.com" + "\n" + 55 | "and more text on the" + "\n" + 56 | "next lines" + "\n\n" + 57 | "```" 58 | expected := `
    59 |
    60 |
    a https://website.com
    61 | and more text on the
    62 | next lines
    63 |
    64 |
    65 | ` 66 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_fenced_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | fencedBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    \n" + 8 | "
    {{ .Content }}
    \n" + 9 | "
    \n" + 10 | "
    \n" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_listing_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | listingBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    \n" + 8 | "
    {{ .Content }}
    \n" + 9 | "
    \n" + 10 | "
    \n" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_literal_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | literalBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    \n" + 8 | "
    {{ .Content }}
    \n" + 9 | "
    \n" + 10 | "
    \n" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_markdown_quote_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("markdown-style quote blocks", func() { 11 | 12 | Context("as delimited blocks", func() { 13 | 14 | It("with single marker without author", func() { 15 | source := `> some text 16 | on *multiple lines*` 17 | 18 | expected := `
    19 |
    20 |
    21 |

    some text 22 | on multiple lines

    23 |
    24 |
    25 |
    26 | ` 27 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 28 | }) 29 | 30 | It("with marker on each line without author", func() { 31 | source := `> some text 32 | > on *multiple lines*` 33 | 34 | expected := `
    35 |
    36 |
    37 |

    some text 38 | on multiple lines

    39 |
    40 |
    41 |
    42 | ` 43 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 44 | }) 45 | 46 | It("with marker on each line with author", func() { 47 | source := `> some text 48 | > on *multiple lines* 49 | > -- John Doe` 50 | expected := `
    51 |
    52 |
    53 |

    some text 54 | on multiple lines

    55 |
    56 |
    57 |
    58 | — John Doe 59 |
    60 |
    61 | ` 62 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 63 | }) 64 | 65 | It("with marker on each line with author and title", func() { 66 | source := `.title 67 | > some text 68 | > on *multiple lines* 69 | > -- John Doe` 70 | expected := `
    71 |
    title
    72 |
    73 |
    74 |

    some text 75 | on multiple lines

    76 |
    77 |
    78 |
    79 | — John Doe 80 |
    81 |
    82 | ` 83 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 84 | }) 85 | 86 | It("with author only", func() { 87 | source := `> -- John Doe` 88 | expected := `
    89 |
    90 |
    91 |
    92 | — John Doe 93 |
    94 |
    95 | ` 96 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 97 | }) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_markdown_quote_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | markdownQuoteBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    \n" + 8 | "{{ if .Content }}
    \n" + 9 | "

    {{ .Content }}

    \n" + 10 | "
    \n{{ end }}" + 11 | "
    \n" + 12 | "{{ if .Attribution.First }}
    \n" + 13 | "— {{ .Attribution.First }}" + 14 | "{{ if .Attribution.Second }}
    \n{{ .Attribution.Second }}{{ end }}\n" + 15 | "
    \n{{ end }}" + 16 | "
    \n" 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_open_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("open blocks", func() { 11 | 12 | Context("without masquerade", func() { 13 | 14 | It("with basic content and attributes", func() { 15 | source := `[#block-id] 16 | .Block Title 17 | -- 18 | basic content 19 | --` 20 | expected := `
    21 |
    Block Title
    22 |
    23 |
    24 |

    basic content

    25 |
    26 |
    27 |
    28 | ` 29 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 30 | }) 31 | 32 | It("with table", func() { 33 | source := `[#block-id] 34 | .Block Title 35 | -- 36 | [cols="2*^"] 37 | |=== 38 | a| 39 | [#id] 40 | .A title 41 | image::image.png[] 42 | a| 43 | [#another-id] 44 | .Another title 45 | image::another-image.png[] 46 | |=== 47 | --` 48 | expected := `
    49 |
    Block Title
    50 |
    51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 64 | 70 | 71 | 72 |
    59 |
    60 | image 61 |
    62 |
    Figure 1. A title
    63 |
    65 |
    66 | another image 67 |
    68 |
    Figure 2. Another title
    69 |
    73 |
    74 |
    75 | ` 76 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 77 | }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_open_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | openBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    \n" + 8 | "{{ .Content }}" + 9 | "
    \n" + 10 | "
    \n" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_passthrough_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("passthrough blocks", func() { 11 | 12 | Context("as delimited blocks", func() { 13 | 14 | It("with title", func() { 15 | source := `.a title 16 | ++++ 17 | _foo_ 18 | 19 | *bar* 20 | ++++` 21 | expected := `_foo_ 22 | 23 | *bar* 24 | ` 25 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 26 | }) 27 | 28 | It("with empty lines around", func() { 29 | source := `++++ 30 | 31 | _foo_ 32 | 33 | *bar* 34 | 35 | ++++` 36 | expected := `_foo_ 37 | 38 | *bar* 39 | ` 40 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 41 | }) 42 | 43 | It("with special characters", func() { 44 | source := `++++ 45 | 46 | 47 | 48 | ++++` 49 | expected := ` 50 | 51 | 52 | ` 53 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 54 | }) 55 | }) 56 | 57 | Context("as paragraph blocks", func() { 58 | 59 | It("2-line paragraph followed by another paragraph", func() { 60 | source := `[pass] 61 | 63 | 64 | another paragraph` 65 | expected := ` 67 |
    68 |

    another paragraph

    69 |
    70 | ` 71 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_passthrough_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | 5 | // the name here is weird because "pass" as a prefix triggers a false security warning 6 | passthroughBlock = "{{ .Content }}\n" //nolint:gosec // avoids a Gosec false positive because the const name starts with 'pass' 7 | ) 8 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_quote_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | quoteBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    \n" + 8 | "{{ .Content }}" + 9 | "
    \n" + 10 | "{{ if .Attribution.First }}
    \n" + 11 | "— {{ .Attribution.First }}" + 12 | "{{ if .Attribution.Second }}
    \n{{ .Attribution.Second }}{{ end }}\n" + 13 | "
    \n{{ end }}" + 14 | "
    \n" 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_sidebar_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("sidebar blocks", func() { 11 | 12 | Context("as delimited blocks", func() { 13 | 14 | It("sidebar block with paragraph", func() { 15 | source := `**** 16 | some *verse* content 17 | 18 | ****` 19 | expected := `
    20 |
    21 |
    22 |

    some verse content

    23 |
    24 |
    25 |
    26 | ` 27 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 28 | }) 29 | 30 | It("sidebar block with id, title, paragraph and sourcecode block", func() { 31 | source := `[#id-for-sidebar] 32 | .title for sidebar 33 | **** 34 | some *verse* content 35 | 36 | ---- 37 | foo 38 | bar 39 | ---- 40 | ****` 41 | expected := `
    42 |
    43 |
    title for sidebar
    44 |
    45 |

    some verse content

    46 |
    47 |
    48 |
    49 |
    foo
    50 | bar
    51 |
    52 |
    53 |
    54 |
    55 | ` 56 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_sidebar_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | sidebarBlockTmpl = "
    \n" + 6 | "
    \n" + 7 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 8 | "{{ .Content }}" + 9 | "
    \n" + 10 | "
    \n" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_source_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | sourceBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    \n" + 8 | "
    ` +
    13 | 		`` +
    15 | 		"{{ .Content }}
    \n" + 16 | "
    \n" + 17 | "
    \n" 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/delimited_block_verse_tmpl.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | verseBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    {{ .Content }}
    \n" + 8 | "{{ if .Attribution.First }}
    \n— {{ .Attribution.First }}" + 9 | "{{ if .Attribution.Second }}
    \n{{ .Attribution.Second }}{{ end }}\n" + 10 | "
    \n{{ end }}" + 11 | "
    \n" 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/document_details.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | documentDetailsTmpl = `
    {{ if .Authors }} 5 | {{ .Authors }}{{ end }}{{ if .RevNumber }} 6 | {{ if .RevLabel }}{{ .RevLabel }} {{ end }}{{ .RevNumber }}{{ if .RevDate }},{{ end }}{{ end }}{{ if .RevDate }} 7 | {{ .RevDate }}{{ end }}{{ if .RevRemark }} 8 |
    {{ .RevRemark }}{{ end }} 9 |
    10 | ` 11 | 12 | documentAuthorDetailsTmpl = `{{ if .Name }}{{ .Name }}
    {{ end }}{{ if .Email }} 13 |
    {{ end }}` 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/footnote_reference.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | footnoteTmpl = `[{{ .ID }}]` 5 | footnoteRefTmpl = `[{{ .ID }}]` 6 | invalidFootnoteTmpl = `[{{ .Ref }}]` 7 | footnotesTmpl = "
    \n
    \n{{ .Content }}
    \n" 8 | 9 | // arguably this should instead be an ordered list. 10 | footnoteElementTmpl = "
    \n" + 11 | "{{ .ID }}. {{ .Content }}\n" + 12 | "
    \n" 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/front_matter_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("documents with front-matter", func() { 11 | 12 | It("should render with front-matter", func() { 13 | source := `--- 14 | description: User Manual 15 | --- 16 | 17 | {description} 18 | ` 19 | expected := `
    20 |

    User Manual

    21 |
    22 | ` 23 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 24 | }) 25 | 26 | }) 27 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/html5.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 7 | "github.com/bytesparadise/libasciidoc/pkg/renderer/sgml" 8 | "github.com/bytesparadise/libasciidoc/pkg/types" 9 | ) 10 | 11 | // Render renders the document to the output, using the SGML renderer configured with the HTML5 templates 12 | func Render(doc *types.Document, config *configuration.Configuration, output io.Writer) (types.Metadata, error) { 13 | return sgml.Render(doc, config, output, templates) 14 | } 15 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/html5_suite_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | 9 | _ "github.com/bytesparadise/libasciidoc/testsupport" 10 | ) 11 | 12 | func TestHtml5(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Html5 Suite") 15 | } 16 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/icon.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | // Inline icons are presented as the icon as an image, or font icon, or the text "[class]" where class 5 | // is the Alt text, and defaults to the icon type. Unlike asciidoctor, we do place class settings on 6 | // the image to match rotate, flip, and size, allowing images to be manipulated with css style just 7 | // like the icons can. We also allow for an ID to be placed on the icon, as another enhancement. 8 | inlineIconTmpl = `` + 9 | `{{ if .Link }}{{ end }}` + 10 | `{{ .Icon }}` + 11 | `{{ if .Link }}{{ end }}` + 12 | `` 13 | 14 | iconImageTmpl = `{{ .Alt }}{{ end }}` 24 | 25 | iconFontTmpl = `` 31 | 32 | iconTextTmpl = `{{ if .Admonition }}
    {{ .Alt }}
    {{ else }}[{{ .Alt }}]{{ end }}` 33 | ) 34 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/image.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | blockImageTmpl = ` 5 |
    6 | {{ if ne .Href "" }}{{ end }}{{ .Alt }}{{ if ne .Href "" }}{{ end }} 7 |
    {{ if .Title }} 8 |
    {{ .Caption }}{{ .Title }}
    9 | {{ else }} 10 | {{ end }} 11 | ` 12 | inlineImageTmpl = `{{ if ne .Href "" }}{{ end }}{{ .Alt }}{{ if ne .Href "" }}{{ end }}` 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/inline_button.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | // initializes the sgml 4 | const ( 5 | inlineButtonTmpl = `{{ . }}` 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/inline_button_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("buttons", func() { 11 | 12 | Context("in final documents", func() { 13 | 14 | It("when experimental is enabled", func() { 15 | source := `:experimental: 16 | 17 | Click on btn:[OK].` 18 | expected := `
    19 |

    Click on OK.

    20 |
    21 | ` 22 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 23 | }) 24 | 25 | It("when experimental is not enabled", func() { 26 | source := `Click on btn:[OK].` 27 | expected := `
    28 |

    Click on btn:[OK].

    29 |
    30 | ` 31 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/inline_menu.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | // initializes the sgml 4 | const ( 5 | inlineMenuTmpl = 6 | // eg: `File` 7 | `{{ if len .Path | eq 1 }}{{ index .Path 0 }}` + 8 | // eg: `File  Zoom  Reset` 9 | `{{ else }}` + 10 | `` + 11 | `{{ with $path := .Path }}` + 12 | `{{ range $index, $element := $path }}` + 13 | `{{ if eq $index 0 }}{{ $element }}` + 14 | `{{ else if lastInStrings $path $index }}  {{ $element }}` + 15 | `{{ else }}  {{ $element }}` + 16 | `{{ end }}` + 17 | `{{ end }}` + 18 | `` + 19 | `{{ end }}` + 20 | `{{ end }}` 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/inline_menu_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("inline menus", func() { 11 | 12 | Context("in final documents", func() { 13 | 14 | It("with main path", func() { 15 | source := `:experimental: 16 | 17 | Select menu:File[].` 18 | expected := `
    19 |

    Select File.

    20 |
    21 | ` 22 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 23 | }) 24 | 25 | It("with single sub path", func() { 26 | source := `:experimental: 27 | 28 | Select menu:File[Save].` 29 | expected := `
    30 |

    Select File  Save.

    31 |
    32 | ` 33 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 34 | }) 35 | 36 | It("with multiple sub paths", func() { 37 | source := `:experimental: 38 | 39 | Select menu:File[Zoom > Reset].` 40 | expected := `
    41 |

    Select File  Zoom  Reset.

    42 |
    43 | ` 44 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 45 | }) 46 | 47 | It("when experimental is not enabled", func() { 48 | source := `Select menu:File[Zoom > Reset].` 49 | expected := `
    50 |

    Select menu:File[Zoom > Reset].

    51 |
    52 | ` 53 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/labeled_list.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | labeledListTmpl = `\n" + 7 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 8 | "
    \n{{ .Content }}
    \n\n" 9 | 10 | labeledListElementTmpl = "
    {{ .Term }}
    \n" + 11 | "{{ if .Content }}
    \n{{ .Content }}
    \n{{ end }}" 12 | 13 | labeledListHorizontalTmpl = `\n" + 16 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 17 | "\n{{ .Content }}
    \n\n" 18 | 19 | // Continuation items (multiple terms sharing a single definition) make this a bit more complex. 20 | labeledListHorizontalElementTmpl = "{{ if not .Continuation }}\n" + 21 | "\n{{ else }}
    \n{{ end }}" + 22 | "{{ .Term }}\n" + 23 | "{{ if .Content }}\n\n{{ .Content }}\n\n{{ end }}" 24 | 25 | qAndAListTmpl = "\n" + 28 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 29 | "
      \n{{ .Content }}
    \n\n" 30 | 31 | qAndAListElementTmpl = "
  • \n

    {{ .Term }}

    \n{{ .Content }}
  • \n" 32 | ) 33 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/link.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | linkTmpl = `{{ .Text }}` 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/manpage.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | manpageHeaderTmpl = `{{ if.IncludeH1 }} 10 | {{ end }}` 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/ordered_list.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | orderedListTmpl = `\n" + 8 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 9 | `\n{{ .Content }}\n\n" 15 | 16 | orderedListElementTmpl = "
  • \n{{ .Content }}
  • \n" 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/paragraph.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | paragraphTmpl = "\n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "

    {{ .Content }}

    \n" + 8 | "\n" 9 | 10 | admonitionParagraphTmpl = `{{ if .Content }}` + 11 | "
    \n" + 13 | "\n\n\n" + 16 | "\n\n
    \n" + 14 | "{{ if .Icon }}{{ .Icon }}{{ end }}\n" + 15 | "\n" + 17 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 18 | "{{ .Content }}" + 19 | "\n
    \n
    \n{{ end }}" 20 | 21 | embeddedParagraphTmpl = "{{ .CheckStyle }}{{ .Content }}

    \n" 22 | 23 | verseParagraphTmpl = "
    \n" + 24 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 25 | "
    {{ .Content }}
    \n" + 26 | "{{ if .Attribution.First }}
    \n— {{ .Attribution.First }}" + 27 | "{{ if .Attribution.Second }}
    \n{{ .Attribution.Second }}\n{{ else }}\n{{ end }}" + 28 | "
    \n{{ end }}
    \n" 29 | 30 | quoteParagraphTmpl = "
    \n" + 31 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 32 | "
    \n" + 33 | "{{ .Content }}\n" + 34 | "
    \n" + 35 | "{{ if .Attribution.First }}
    \n— {{ .Attribution.First }}" + 36 | "{{ if .Attribution.Second }}
    \n{{ .Attribution.Second }}\n{{ else }}\n{{ end }}" + 37 | "
    \n{{ end }}
    \n" 38 | 39 | manpageNameParagraphTmpl = "

    {{ .Content }}

    \n" 40 | 41 | thematicBreakTmpl = "
    \n" 42 | ) 43 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/preamble.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | preambleTmpl = `{{ if .Wrapper }}
    5 |
    6 | {{ end }}{{ .Content }}{{ if .Wrapper }}
    7 | {{ if .ToC }}{{ .ToC }}{{ end }}
    8 | {{ end }}` 9 | ) 10 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/quoted_text.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | boldTextTmpl = `{{ .Content }}` 5 | italicTextTmpl = `{{ .Content }}` 6 | monospaceTextTmpl = `{{ .Content }}` 7 | subscriptTextTmpl = `{{ .Content }}` 8 | superscriptTextTmpl = `{{ .Content }}` 9 | markedTextTmpl = `{{ if .Roles }}{{ .Content }}{{ if .Roles }}
    {{ else }}{{ end }}` 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/section.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | // initializes the sgml 4 | const ( 5 | sectionContentTmpl = `
    6 | {{ .Header }}{{ if eq .Level 1 }}
    7 | {{ end }}{{ .Content }}{{ if eq .Level 1 }}
    8 | {{ end }}
    9 | ` 10 | sectionTitleTmpl = `{{ if .Number }}{{ .Number }}. {{ end }}{{ .Content }} 11 | ` 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/special_character_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("special characters", func() { 11 | 12 | It("should parse in paragraph", func() { 13 | source := "* ' &" 14 | expected := `
    15 |

    <b>*</b> &apos; &amp;

    16 |
    17 | ` 18 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 19 | }) 20 | 21 | It("should parse in delimited block", func() { 22 | source := "```" + "\n" + 23 | "* ' &" + "\n" + 24 | "```" 25 | expected := `
    26 |
    27 |
    <b>*</b> &apos; &amp;
    28 |
    29 |
    30 | ` 31 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 32 | }) 33 | 34 | It("should parse in paragraph and delimited block", func() { 35 | source := "* ' &" + "\n\n" + 36 | "```" + "\n" + 37 | "* ' &" + "\n" + 38 | "```" 39 | expected := `
    40 |

    <b>*</b> &apos; &amp;

    41 |
    42 |
    43 |
    44 |
    <b>*</b> &apos; &amp;
    45 |
    46 |
    47 | ` 48 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/table.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | tableTmpl = "\n" + 12 | "{{ if .Title }}\n{{ end }}" + 13 | "{{ if .Body }}" + 14 | "\n" + 15 | "{{ range $i, $w := .Columns }}\n{{ end}}" + 18 | "\n" + 19 | "{{ .Header }}" + 20 | "{{ .Body }}" + 21 | "{{ .Footer }}" + 22 | "{{ end }}" + 23 | "
    {{ .Caption }}{{ .Title }}
    \n" 24 | 25 | tableBodyTmpl = "{{ if .Content }}\n{{ .Content }}\n{{ end }}" 26 | 27 | tableHeaderTmpl = "{{ if .Content }}\n\n{{ .Content }}\n\n{{ end }}" 28 | 29 | tableHeaderCellTmpl = "{{ .Content }}\n" 30 | 31 | tableFooterTmpl = "{{ if .Content }}\n\n{{ .Content }}\n\n{{ end }}" 32 | 33 | tableFooterCellTmpl = "

    {{ .Content }}

    \n" 34 | 35 | tableRowTmpl = "\n{{ .Content }}\n" 36 | 37 | tableCellTmpl = "{{ .Content }}\n" 38 | 39 | tableCellBlockTmpl = "
    {{ trimLineFeedSuffix .Content }}
    " 40 | ) 41 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/table_of_contents.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | tocRootTmpl = "
    \n" + 5 | "
    {{ .Title }}
    \n" + 6 | "{{ .Sections }}" + 7 | "
    \n" 8 | 9 | tocSectionTmpl = "\n" 10 | 11 | tocEntryTmpl = "
  • {{ if .Number }}{{ .Number }}. {{ end }}{{ .Title }}" + 12 | "{{ if .Content }}\n{{ .Content }}{{ end }}
  • \n" 13 | ) 14 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/templates_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/renderer/sgml/html5" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("fields", func() { 12 | 13 | It("template fields are not empty", func() { 14 | tmp := html5.Templates() // sgml.Templates 15 | typ := reflect.TypeOf(tmp) 16 | val := reflect.ValueOf(tmp) 17 | 18 | for i := 0; i < typ.NumField(); i++ { 19 | fn := typ.Field(i).Name 20 | fv := val.FieldByName(fn) 21 | 22 | s, ok := fv.Interface().(string) 23 | Expect(ok).To(BeTrue()) 24 | Expect(s).NotTo(BeEmpty()) 25 | } 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/thematic_break_test.go: -------------------------------------------------------------------------------- 1 | package html5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("thenatic breaks", func() { 11 | 12 | It("simple", func() { 13 | source := `before 14 | 15 | ''' 16 | 17 | after` 18 | expected := `
    19 |

    before

    20 |
    21 |
    22 |
    23 |

    after

    24 |
    25 | ` 26 | Expect(RenderHTML(source)).To(MatchHTML(expected)) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html5/unordered_list.go: -------------------------------------------------------------------------------- 1 | package html5 2 | 3 | const ( 4 | unorderedListTmpl = `\n" + 9 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 10 | "\n" + 11 | "{{ .Content }}\n\n" 12 | 13 | unorderedListElementTmpl = "
  • \n{{ .Content }}
  • \n" 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/html_escape.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "html" 5 | "strings" 6 | ) 7 | 8 | // escapeString is like html5.Escape func except but bypasses 9 | // a few replacements. It is a bit more conservative. 10 | func escapeString(s string) string { 11 | return htmlEscaper.Replace(s) 12 | } 13 | 14 | func unescapeString(s string) string { 15 | return html.UnescapeString(s) 16 | } 17 | 18 | var htmlEscaper = strings.NewReplacer( 19 | `<`, "<", // keep as-is (we do not want `&lt;`) 20 | `>`, ">", // keep `&lgt;` as-is (we do not want `&gt;`) 21 | `&`, "&", // keep `&s` as-is (we do not want `&amp;`) // TODO: still needed? 22 | `&#`, "&#", // assume this is for an character entity and this keep as-is 23 | // standard escape combinations 24 | `&`, "&", 25 | `<`, "<", 26 | `>`, ">", 27 | `'`, "'", // "'" is shorter than "'" and apos was not in HTML until HTML5. 28 | `"`, """, // """ is shorter than """. 29 | ) 30 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/index_term.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | ) 6 | 7 | func (r *sgmlRenderer) renderIndexTerm(ctx *context, t *types.IndexTerm) (string, error) { 8 | return r.renderInlineElements(ctx, t.Term) 9 | } 10 | 11 | func (r *sgmlRenderer) renderConcealedIndexTerm(_ *types.ConcealedIndexTerm) (string, error) { 12 | return "", nil // do not render in SGML 13 | } 14 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/inline_button.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func (r *sgmlRenderer) renderInlineButton(b *types.InlineButton) (string, error) { 11 | result := &strings.Builder{} 12 | tmpl, err := r.inlineButton() 13 | if err != nil { 14 | return "", errors.Wrap(err, "unable to load inline button template") 15 | } 16 | if err = tmpl.Execute(result, b.Attributes[types.AttrButtonLabel]); err != nil { 17 | return "", errors.Wrap(err, "unable to render inline button") 18 | } 19 | return result.String(), nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/inline_elements.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | ) 8 | 9 | func (r *sgmlRenderer) renderInlineElements(ctx *context, elements []interface{}) (string, error) { 10 | if len(elements) == 0 { 11 | return "", nil 12 | } 13 | // log.Debugf("rendering line with %d element(s)...", len(elements)) 14 | buf := &strings.Builder{} 15 | for i, element := range elements { 16 | renderedElement, err := r.renderElement(ctx, element) 17 | if err != nil { 18 | return "", err 19 | } 20 | if i == len(elements)-1 { 21 | if _, ok := element.(*types.StringElement); ok { // TODO: only for StringElement? or for any kind of element? 22 | // trim trailing spaces before returning the line 23 | buf.WriteString(strings.TrimRight(string(renderedElement), " ")) 24 | } else { 25 | buf.WriteString(renderedElement) 26 | } 27 | } else { 28 | buf.WriteString(renderedElement) 29 | } 30 | } 31 | // if log.IsLevelEnabled(log.DebugLevel) { 32 | // log.Debugf("rendered inline elements: '%s'", buf.String()) 33 | // } 34 | return buf.String(), nil 35 | } 36 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/inline_menu.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | ) 6 | 7 | func (r *sgmlRenderer) renderInlineMenu(m *types.InlineMenu) (string, error) { 8 | return r.execute(r.inlineMenu, struct { 9 | Path []string 10 | }{ 11 | Path: m.Path, 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/link.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func (r *sgmlRenderer) renderLink(ctx *context, l *types.InlineLink) (string, error) { 11 | text := "" 12 | class := "" 13 | id := l.Attributes.GetAsStringWithDefault(types.AttrID, "") 14 | roles, err := r.renderElementRoles(ctx, l.Attributes) 15 | if err != nil { 16 | return "", errors.Wrap(err, "unable to render link") 17 | } 18 | // TODO; support `mailto:` positional attributes 19 | if t, exists := l.Attributes[types.AttrInlineLinkText]; exists { 20 | switch t := t.(type) { 21 | case string: 22 | text = htmlEscaper.Replace(t) 23 | case []interface{}: 24 | var err error 25 | if text, err = r.renderInlineElements(ctx, t); err != nil { 26 | return "", errors.Wrap(err, "unable to render link") 27 | } 28 | } 29 | class = roles // can be empty (and it's fine) 30 | } else { 31 | text = htmlEscaper.Replace(l.Location.ToDisplayString()) 32 | if l.Location != nil && l.Location.Scheme != "mailto:" { 33 | class = "bare" 34 | } 35 | if len(roles) > 0 { 36 | class = strings.Join([]string{class, roles}, " ") // support case where class == "" (for email addresses) 37 | } 38 | } 39 | target := l.Attributes.GetAsStringWithDefault(types.AttrInlineLinkTarget, "") 40 | noopener := target == "_blank" || l.Attributes.HasOption("noopener") 41 | return r.execute(r.link, struct { 42 | ID string 43 | URL string 44 | Text string 45 | Class string 46 | Target string 47 | NoOpener bool 48 | }{ 49 | ID: id, 50 | URL: htmlEscaper.Replace(l.Location.ToString()), 51 | Text: text, 52 | Class: class, 53 | Target: target, 54 | NoOpener: noopener, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/literal_blocks.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/pkg/errors" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func (r *sgmlRenderer) renderLiteralBlock(ctx *context, b *types.DelimitedBlock) (string, error) { 12 | log.Debugf("rendering literal block") 13 | content, err := r.renderElements(ctx, b.Elements) 14 | if err != nil { 15 | return "", errors.Wrap(err, "unable to render literal block content") 16 | } 17 | roles, err := r.renderElementRoles(ctx, b.Attributes) 18 | if err != nil { 19 | return "", errors.Wrap(err, "unable to render literal block roles") 20 | } 21 | title, err := r.renderElementTitle(ctx, b.Attributes) 22 | if err != nil { 23 | return "", errors.Wrap(err, "unable to render literal block title") 24 | } 25 | 26 | return r.execute(r.literalBlock, struct { 27 | Context *context 28 | ID string 29 | Title string 30 | Roles string 31 | Content string 32 | }{ 33 | Context: ctx, 34 | ID: r.renderElementID(b.Attributes), 35 | Title: title, 36 | Roles: roles, 37 | Content: strings.Trim(content, "\n"), 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/passthrough.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | htmltemplate "html/template" 5 | "strings" 6 | 7 | "github.com/bytesparadise/libasciidoc/pkg/types" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func (r *sgmlRenderer) renderPassthroughParagraph(ctx *context, p *types.Paragraph) (string, error) { 12 | content, err := r.renderPassthroughContent(ctx, p.Elements) 13 | if err != nil { 14 | return "", err 15 | } 16 | return content + "\n", nil 17 | } 18 | 19 | func (r *sgmlRenderer) renderInlinePassthrough(ctx *context, p *types.InlinePassthrough) (string, error) { 20 | renderedContent, err := r.renderPassthroughContent(ctx, p.Elements) 21 | if err != nil { 22 | return "", errors.Wrap(err, "unable to render passthrough") 23 | } 24 | switch p.Kind { 25 | case types.SinglePlusPassthrough: 26 | // rendered passthrough content is in an HTML-escaped form 27 | buf := &strings.Builder{} 28 | htmltemplate.HTMLEscape(buf, []byte(renderedContent)) 29 | return buf.String(), nil 30 | default: 31 | return renderedContent, nil 32 | } 33 | } 34 | 35 | // renderPassthroughMacro renders the passthrough content in its raw from 36 | func (r *sgmlRenderer) renderPassthroughContent(ctx *context, elements []interface{}) (string, error) { 37 | result := &strings.Builder{} 38 | for _, element := range elements { 39 | switch element := element.(type) { 40 | case *types.StringElement: 41 | // "string" elements must be rendered as-is, ie, without any HTML escaping. 42 | _, err := result.WriteString(element.Content) 43 | if err != nil { 44 | return "", err 45 | } 46 | default: 47 | renderedElement, err := r.renderElement(ctx, element) 48 | if err != nil { 49 | return "", err 50 | } 51 | _, err = result.WriteString(renderedElement) 52 | if err != nil { 53 | return "", err 54 | } 55 | 56 | } 57 | } 58 | return strings.Trim(result.String(), "\n"), nil // remove leading and trailing empty lines 59 | } 60 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/preamble.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func (r *sgmlRenderer) renderPreamble(ctx *context, p *types.Preamble) (string, error) { 9 | // log.Debugf("rendering preamble...") 10 | // the
    wrapper is only necessary 11 | // if the document has a section 0 12 | 13 | content, err := r.renderElements(ctx, p.Elements) 14 | if err != nil { 15 | return "", errors.Wrap(err, "error rendering preamble elements") 16 | } 17 | toc, err := r.renderTableOfContents(ctx, p.TableOfContents) 18 | if err != nil { 19 | return "", errors.Wrap(err, "error rendering preamble elements") 20 | } 21 | return r.execute(r.preamble, struct { 22 | Context *context 23 | Wrapper bool 24 | Content string 25 | ToC string 26 | }{ 27 | Context: ctx, 28 | Wrapper: ctx.hasHeader, 29 | Content: string(content), 30 | ToC: string(toc), 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/predefined_attribute.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | ) 6 | 7 | func (r *sgmlRenderer) renderPredefinedAttribute(a *types.PredefinedAttribute) (string, error) { 8 | return predefinedAttribute(a.Name), nil 9 | } 10 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/predefined_attribute_test.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | ) 7 | 8 | var _ = DescribeTable("predefined attributes", 9 | func(code, rendered string) { 10 | Expect(predefinedAttribute(code)).To(Equal(rendered)) 11 | }, 12 | Entry("sp", "sp", " "), 13 | Entry("blank", "blank", ""), 14 | Entry("empty", "empty", ""), 15 | Entry("nbsp", "nbsp", " "), 16 | Entry("zwsp", "zwsp", "​"), 17 | Entry("wj", "wj", "⁠"), 18 | Entry("apos", "apos", "'"), 19 | Entry("quot", "quot", """), 20 | Entry("lsquo", "lsquo", "‘"), 21 | Entry("rsquo", "rsquo", "’"), 22 | Entry("ldquo", "ldquo", "“"), 23 | Entry("rdquo", "rdquo", "”"), 24 | Entry("deg", "deg", "°"), 25 | Entry("plus", "plus", "+"), 26 | Entry("brvbar", "brvbar", "¦"), 27 | Entry("vbar", "vbar", "|"), 28 | Entry("amp", "amp", "&"), 29 | Entry("lt", "lt", "<"), 30 | Entry("gt", "gt", ">"), 31 | Entry("startsb", "startsb", "["), 32 | Entry("endsb", "endsb", "]"), 33 | Entry("caret", "caret", "^"), 34 | Entry("asterisk", "asterisk", "*"), 35 | Entry("tilde", "tilde", "~"), 36 | Entry("backslash", "backslash", `\`), 37 | Entry("backtick", "backtick", "`"), 38 | Entry("two-colons", "two-colons", "::"), 39 | Entry("two-semicolons", "two-semicolons", ";"), 40 | Entry("cpp", "cpp", "C++"), 41 | ) 42 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/quoted_text.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | texttemplate "text/template" 6 | 7 | "github.com/bytesparadise/libasciidoc/pkg/types" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // TODO: The bold, italic, and monospace items should be refactored to support semantic tags instead. 12 | 13 | func (r *sgmlRenderer) renderQuotedText(ctx *context, t *types.QuotedText) (string, error) { 14 | elementsBuffer := &strings.Builder{} 15 | for _, element := range t.Elements { 16 | b, err := r.renderElement(ctx, element) 17 | if err != nil { 18 | return "", errors.Wrap(err, "unable to render text quote") 19 | } 20 | _, err = elementsBuffer.WriteString(b) 21 | if err != nil { 22 | return "", errors.Wrapf(err, "unable to render text quote") 23 | } 24 | } 25 | roles, err := r.renderElementRoles(ctx, t.Attributes) 26 | if err != nil { 27 | return "", errors.Wrap(err, "unable to render quoted text roles") 28 | } 29 | var tmpl *texttemplate.Template 30 | switch t.Kind { 31 | case types.SingleQuoteBold, types.DoubleQuoteBold: 32 | tmpl, err = r.boldText() 33 | case types.SingleQuoteItalic, types.DoubleQuoteItalic: 34 | tmpl, err = r.italicText() 35 | case types.SingleQuoteMarked, types.DoubleQuoteMarked: 36 | tmpl, err = r.markedText() 37 | case types.SingleQuoteMonospace, types.DoubleQuoteMonospace: 38 | tmpl, err = r.monospaceText() 39 | case types.SingleQuoteSubscript: 40 | tmpl, err = r.subscriptText() 41 | case types.SingleQuoteSuperscript: 42 | tmpl, err = r.superscriptText() 43 | default: 44 | return "", errors.Errorf("unsupported quoted text kind: '%v'", t.Kind) 45 | } 46 | if err != nil { 47 | return "", errors.Wrap(err, "unable to load quoted text template") 48 | } 49 | result := &strings.Builder{} 50 | if err := tmpl.Execute(result, struct { 51 | ID string 52 | Roles string 53 | Attributes types.Attributes 54 | Content string 55 | }{ 56 | Attributes: t.Attributes, 57 | ID: r.renderElementID(t.Attributes), 58 | Roles: roles, 59 | Content: string(elementsBuffer.String()), 60 | }); err != nil { 61 | return "", errors.Wrap(err, "unable to render quoted text") 62 | } 63 | return result.String(), nil 64 | } 65 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/section.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | 8 | "github.com/pkg/errors" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func (r *sgmlRenderer) renderSection(ctx *context, s *types.Section) (string, error) { 13 | // log.Debugf("rendering section level %d", s.Level) 14 | title, err := r.renderSectionTitle(ctx, s) 15 | if err != nil { 16 | return "", errors.Wrap(err, "error while rendering section title") 17 | } 18 | 19 | content, err := r.renderElements(ctx, s.Elements) 20 | if err != nil { 21 | return "", errors.Wrap(err, "error while rendering section content") 22 | } 23 | roles, err := r.renderElementRoles(ctx, s.Attributes) 24 | if err != nil { 25 | return "", errors.Wrap(err, "unable to render section roles") 26 | } 27 | return r.execute(r.sectionContent, struct { 28 | Context *context 29 | Header string 30 | Content string 31 | Elements []interface{} 32 | ID string 33 | Roles string 34 | Level int 35 | }{ 36 | Context: ctx, 37 | Header: title, 38 | Level: s.Level, 39 | Elements: s.Elements, 40 | ID: r.renderElementID(s.Attributes), 41 | Roles: roles, 42 | Content: string(content), 43 | }) 44 | } 45 | 46 | func (r *sgmlRenderer) renderSectionTitle(ctx *context, s *types.Section) (string, error) { 47 | renderedContent, err := r.renderInlineElements(ctx, s.Title) 48 | if err != nil { 49 | return "", errors.Wrap(err, "error while rendering section title") 50 | } 51 | renderedContentStr := strings.TrimSpace(renderedContent) 52 | var number string 53 | if ctx.sectionNumbering != nil { 54 | id := s.GetID() 55 | log.Debugf("number for section '%s': '%s'", id, number) 56 | number = ctx.sectionNumbering[id] 57 | } 58 | return r.execute(r.sectionTitle, struct { 59 | Level int 60 | LevelPlusOne int 61 | ID string 62 | Number string 63 | Content string 64 | }{ 65 | Level: s.Level, 66 | LevelPlusOne: s.Level + 1, // Level 1 is

    . 67 | ID: r.renderElementID(s.Attributes), 68 | Number: number, 69 | Content: renderedContentStr, 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/sgml_suite_test.go: -------------------------------------------------------------------------------- 1 | package sgml_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestSgml(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Sgml Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/special_character.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | ) 8 | 9 | func (r *sgmlRenderer) renderSpecialCharacter(s *types.SpecialCharacter) (string, error) { 10 | switch s.Name { 11 | case `&`: 12 | return "&", nil 13 | case `<`: 14 | return "<", nil 15 | case `>`: 16 | return ">", nil 17 | default: 18 | return "", fmt.Errorf("unknown special character: '%s'", s.Name) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/string.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | 8 | "github.com/bytesparadise/libasciidoc/pkg/types" 9 | ) 10 | 11 | func (r *sgmlRenderer) renderStringElement(ctx *context, str *types.StringElement) (string, error) { 12 | // NB: For all SGML flavors we are aware of, the numeric entities from 13 | // Unicode are supported. We generally avoid named entities. 14 | result := str.Content 15 | if !ctx.UseUnicode() { 16 | // convert to entities 17 | result = asciiEntify(result) 18 | } 19 | return result, nil 20 | } 21 | 22 | func asciiEntify(source string) string { 23 | out := &strings.Builder{} 24 | out.Grow(len(source)) 25 | for _, r := range source { 26 | // This will certain characters that should be escaped alone. Run them through 27 | // escape first if that is a concern. 28 | if r < 128 && (unicode.IsPrint(r) || unicode.IsSpace(r)) { 29 | out.WriteRune(r) 30 | continue 31 | } 32 | // take care that the entity is unsigned (should always be) 33 | fmt.Fprintf(out, "&#%d;", uint32(r)) // TODO: avoid `fmt.Fprintf`, use `fmt.Fprint` instead? 34 | } 35 | return out.String() 36 | } 37 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/symbol.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | ) 8 | 9 | var symbols = map[string]string{ 10 | "(C)": "©", 11 | "(R)": "®", 12 | "(TM)": "™", 13 | "...": "…​", // include the 'zero width' character (`​`) to prevent increased letter spacing in justification 14 | "'": "’", 15 | "'`": "‘", 16 | "`'": "’", 17 | "\"`": "“", 18 | "`\"": "”", 19 | "->": "→", 20 | "<-": "←", 21 | "=>": "⇒", 22 | "<=": "⇐", 23 | "--": "—​", // include the 'zero width' character (`​`) to prevent increased letter spacing in justification 24 | " -- ": " — ", // surrounded by thin spaces 25 | } 26 | 27 | func (r *sgmlRenderer) renderSymbol(s *types.Symbol) (string, error) { 28 | if str, found := symbols[s.Name]; found { 29 | // if s.Prefix != "" { 30 | // return s.Prefix + str, nil 31 | // } 32 | return str, nil 33 | } 34 | return "", fmt.Errorf("symbol '%s' is not defined", s.Name) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/user_macro.go: -------------------------------------------------------------------------------- 1 | package sgml 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | ) 8 | 9 | func (r *sgmlRenderer) renderUserMacro(ctx *context, m *types.UserMacro) (string, error) { 10 | buf := &strings.Builder{} 11 | macro, ok := ctx.config.Macros[m.Name] 12 | if !ok { 13 | if m.Kind == types.BlockMacro { 14 | // fallback to paragraph 15 | p, _ := types.NewParagraph( 16 | &types.StringElement{Content: m.RawText}, 17 | ) 18 | return r.renderParagraph(ctx, p) 19 | } 20 | // fallback to render raw text 21 | return m.RawText, nil 22 | } 23 | if err := macro.Execute(buf, m); err != nil { 24 | return "", err 25 | } 26 | return buf.String(), nil 27 | 28 | } 29 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/article.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | articleTmpl = "\n" + 5 | "\n" + 6 | "\n" + 7 | "\n" + 8 | "\n" + 9 | "\n" + 10 | "{{ if .Generator }}\n{{ end }}" + 11 | "{{ if .Authors }}\n{{ end }}" + 12 | "{{ range $css := .CSS }}\n{{ end }}" + 13 | "{{ .Title }}\n" + 14 | "\n" + 15 | "\n" + 18 | "{{ if .IncludeHTMLBodyHeader }}{{ .Header }}{{ end }}" + 19 | "
    \n" + 20 | "{{ .Content }}" + 21 | "
    \n" + 22 | "{{ if .IncludeHTMLBodyFooter }}
    \n" + 23 | "
    \n" + 24 | "{{ if .RevNumber }}Version {{ .RevNumber }}
    \n{{ end }}" + 25 | "Last updated {{ .LastUpdated }}\n" + 26 | "
    \n" + 27 | "
    \n{{ end }}" + 28 | "\n" + 29 | "\n" 30 | ) 31 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/blank_line.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | lineBreakTmpl = "
    " 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/blank_line_test.go: -------------------------------------------------------------------------------- 1 | package xhtml5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | . "github.com/onsi/ginkgo/v2" 6 | . "github.com/onsi/gomega" 7 | ) 8 | 9 | var _ = Describe("blank lines", func() { 10 | 11 | It("blank line between 2 paragraphs", func() { 12 | source := `first paragraph 13 | 14 | second paragraph` 15 | expected := `
    16 |

    first paragraph

    17 |
    18 |
    19 |

    second paragraph

    20 |
    21 | ` 22 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 23 | }) 24 | 25 | It("blank line with spaces and tabs between 2 paragraphs", func() { 26 | source := `first paragraph 27 | 28 | second paragraph` 29 | expected := `
    30 |

    first paragraph

    31 |
    32 |
    33 |

    second paragraph

    34 |
    35 | ` 36 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 37 | }) 38 | 39 | It("blank lines (tabs) at end of document", func() { 40 | source := `first paragraph 41 | 42 | 43 | ` 44 | expected := `
    45 |

    first paragraph

    46 |
    47 | ` 48 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 49 | }) 50 | 51 | It("blank lines (spaces) at end of document", func() { 52 | source := `first paragraph 53 | 54 | 55 | ` 56 | expected := `
    57 |

    first paragraph

    58 |
    59 | ` 60 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 61 | }) 62 | 63 | It("hard break", func() { 64 | source := `first line + 65 | second line` 66 | expected := `
    67 |

    first line
    68 | second line

    69 |
    70 | ` 71 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 72 | }) 73 | 74 | It("thematic break", func() { 75 | source := "- - -" 76 | expected := "
    \n" 77 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/comment_test.go: -------------------------------------------------------------------------------- 1 | package xhtml5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("comments", func() { 11 | 12 | Context("single line comments", func() { 13 | 14 | It("single line comment alone", func() { 15 | source := `// A single-line comment.` 16 | expected := "" 17 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 18 | }) 19 | 20 | It("single line comment at end of line", func() { 21 | source := `foo // A single-line comment.` 22 | expected := `
    23 |

    foo // A single-line comment.

    24 |
    25 | ` 26 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 27 | }) 28 | 29 | It("single line comment within a paragraph", func() { 30 | source := `a first line 31 | // A single-line comment. 32 | another line` 33 | expected := `
    34 |

    a first line 35 | another line

    36 |
    37 | ` 38 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 39 | }) 40 | }) 41 | 42 | Context("comment blocks", func() { 43 | 44 | It("comment block alone", func() { 45 | source := `//// 46 | a *comment* block 47 | with multiple lines 48 | ////` 49 | expected := "" 50 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 51 | }) 52 | 53 | It("comment block with paragraphs around", func() { 54 | source := `a first paragraph 55 | 56 | //// 57 | a *comment* block 58 | with multiple lines 59 | //// 60 | 61 | a second paragraph` 62 | expected := `
    63 |

    a first paragraph

    64 |
    65 |
    66 |

    a second paragraph

    67 |
    68 | ` 69 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 70 | }) 71 | }) 72 | 73 | }) 74 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/delimited_block_fenced_test.go: -------------------------------------------------------------------------------- 1 | package xhtml5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("fenced blocks", func() { 11 | 12 | Context("as delimited blocks", func() { 13 | 14 | It("fenced block with surrounding empty lines", func() { 15 | source := "```\n\nsome source code \n\nhere \n\n\n\n```" 16 | expected := `
    17 |
    18 |
    some source code
    19 | 
    20 | here
    21 |
    22 |
    23 | ` 24 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 25 | }) 26 | 27 | It("fenced block with empty lines", func() { 28 | source := "```\n\n\n\n```" 29 | expected := `
    30 |
    31 |
    32 |
    33 |
    34 | ` 35 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 36 | }) 37 | 38 | It("fenced block with id and title", func() { 39 | source := "[#id-for-fences]\n.fenced block title\n```\nsome source code\n\nhere\n\n\n\n```" 40 | expected := `
    41 |
    fenced block title
    42 |
    43 |
    some source code
    44 | 
    45 | here
    46 |
    47 |
    48 | ` 49 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 50 | }) 51 | 52 | It("fenced block with external link inside", func() { 53 | source := "```" + "\n" + 54 | "a https://website.com" + "\n" + 55 | "and more text on the" + "\n" + 56 | "next lines" + "\n\n" + 57 | "```" 58 | expected := `
    59 |
    60 |
    a https://website.com
    61 | and more text on the
    62 | next lines
    63 |
    64 |
    65 | ` 66 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/delimited_block_markdown_quote_test.go: -------------------------------------------------------------------------------- 1 | package xhtml5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("markdown-style quote blocks", func() { 11 | 12 | Context("as delimited blocks", func() { 13 | 14 | It("with single marker without author", func() { 15 | source := `> some text 16 | on *multiple lines*` 17 | 18 | expected := `
    19 |
    20 |
    21 |

    some text 22 | on multiple lines

    23 |
    24 |
    25 |
    26 | ` 27 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 28 | }) 29 | 30 | It("with marker on each line without author", func() { 31 | source := `> some text 32 | > on *multiple lines*` 33 | 34 | expected := `
    35 |
    36 |
    37 |

    some text 38 | on multiple lines

    39 |
    40 |
    41 |
    42 | ` 43 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 44 | }) 45 | 46 | It("with marker on each line with author", func() { 47 | source := `> some text 48 | > on *multiple lines* 49 | > -- John Doe` 50 | expected := `
    51 |
    52 |
    53 |

    some text 54 | on multiple lines

    55 |
    56 |
    57 |
    58 | — John Doe 59 |
    60 |
    61 | ` 62 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 63 | }) 64 | 65 | It("with marker on each line with author and title", func() { 66 | source := `.title 67 | > some text 68 | > on *multiple lines* 69 | > -- John Doe` 70 | expected := `
    71 |
    title
    72 |
    73 |
    74 |

    some text 75 | on multiple lines

    76 |
    77 |
    78 |
    79 | — John Doe 80 |
    81 |
    82 | ` 83 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 84 | }) 85 | 86 | It("with author only", func() { 87 | source := `> -- John Doe` 88 | expected := `
    89 |
    90 |
    91 |
    92 | — John Doe 93 |
    94 |
    95 | ` 96 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 97 | }) 98 | }) 99 | }) 100 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/delimited_block_passthrough_test.go: -------------------------------------------------------------------------------- 1 | package xhtml5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("passthrough blocks", func() { 11 | 12 | Context("as delimited blocks", func() { 13 | 14 | It("with title", func() { 15 | source := `.a title 16 | ++++ 17 | _foo_ 18 | 19 | *bar* 20 | ++++` 21 | expected := `_foo_ 22 | 23 | *bar* 24 | ` 25 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 26 | }) 27 | 28 | It("with special characters", func() { 29 | source := `++++ 30 | 31 | 32 | 33 | ++++` 34 | expected := ` 35 | 36 | 37 | ` 38 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 39 | }) 40 | 41 | }) 42 | 43 | Context("open block", func() { 44 | 45 | It("2-line paragraph followed by another paragraph", func() { 46 | source := `[pass] 47 | _foo_ 48 | *bar* 49 | 50 | another paragraph` 51 | expected := `_foo_ 52 | *bar* 53 |
    54 |

    another paragraph

    55 |
    56 | ` 57 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 58 | }) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/delimited_block_quote_tmpl.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | quoteBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    \n" + 8 | "{{ .Content }}" + 9 | "
    \n" + 10 | "{{ if .Attribution.First }}
    \n" + 11 | "— {{ .Attribution.First }}" + 12 | "{{ if .Attribution.Second }}
    \n{{ .Attribution.Second }}{{ end }}\n" + 13 | "
    \n{{ end }}" + 14 | "
    \n" 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/delimited_block_sidebar_test.go: -------------------------------------------------------------------------------- 1 | package xhtml5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("sidebar blocks", func() { 11 | 12 | Context("as delimited blocks", func() { 13 | 14 | It("sidebar block with paragraph", func() { 15 | source := `**** 16 | some *verse* content 17 | 18 | ****` 19 | expected := `
    20 |
    21 |
    22 |

    some verse content

    23 |
    24 |
    25 |
    26 | ` 27 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 28 | }) 29 | 30 | It("sidebar block with id, title, paragraph and sourcecode block", func() { 31 | source := `[#id-for-sidebar] 32 | .title for sidebar 33 | **** 34 | some *verse* content 35 | 36 | ---- 37 | foo 38 | bar 39 | ---- 40 | ****` 41 | expected := `
    42 |
    43 |
    title for sidebar
    44 |
    45 |

    some verse content

    46 |
    47 |
    48 |
    49 |
    foo
    50 | bar
    51 |
    52 |
    53 |
    54 |
    55 | ` 56 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/delimited_block_verse_tmpl.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | verseBlockTmpl = `
    \n" + 6 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 7 | "
    {{ .Content }}
    \n" + 8 | "{{ if .Attribution.First }}
    \n— {{ .Attribution.First }}" + 9 | "{{ if .Attribution.Second }}
    \n{{ .Attribution.Second }}{{ end }}\n" + 10 | "
    \n{{ end }}" + 11 | "
    \n" 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/document_details.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | documentDetailsTmpl = `
    {{ if .Authors }} 5 | {{ .Authors }}{{ end }}{{ if .RevNumber }} 6 | {{ if .RevLabel }}{{ .RevLabel }} {{ end }}{{ .RevNumber }}{{ if .RevDate }},{{ end }}{{ end }}{{ if .RevDate }} 7 | {{ .RevDate }}{{ end }}{{ if .RevRemark }} 8 |
    {{ .RevRemark }}{{ end }} 9 |
    10 | ` 11 | 12 | documentAuthorDetailsTmpl = `{{ if .Name }}{{ .Name }}
    {{ end }}{{ if .Email }} 13 |
    {{ end }}` 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/footnote_reference.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | footnotesTmpl = "
    \n
    \n{{ .Content }}
    \n" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/icon.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | // Inline icons are presented as the icon as an image, or font icon, or the text "[class]" where class 5 | // is the Alt text, and defaults to the icon type. Unlike asciidoctor, we do place class settings on 6 | // the image to match rotate, flip, and size, allowing images to be manipulated with css style just 7 | // like the icons can. We also allow for an ID to be placed on the icon, as another enhancement. 8 | // 9 | // Only the img tag needs to be made XHTML safe. 10 | 11 | iconImageTmpl = `{{ .Alt }}{{ end }}` 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/image.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | blockImageTmpl = "\n" + 7 | "
    \n" + 8 | `{{ if .Href }}{{ end }}` + 9 | `{{ .Alt }}{{ if .Href }}{{ end }}\n" + 13 | "
    \n" + 14 | "{{ if .Title }}
    {{ .Caption }}{{ .Title }}
    \n{{ end }}" + 15 | "

    \n" 16 | 17 | inlineImageTmpl = `` + 18 | `{{ if .Href }}{{ end }}` + 19 | `{{ .Alt }}{{ if .Href }}{{ end }}` 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/labeled_list.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | labeledListHorizontalItemTmpl = "{{ if not .Continuation }}\n" + 5 | "\n{{ else }}
    \n{{ end }}" + 6 | "{{ .Term }}\n" + 7 | "{{ if .Content }}\n\n{{ .Content }}\n\n{{ end }}" 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/paragraph.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | verseParagraphTmpl = "
    \n" + 5 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 6 | "
    {{ .Content }}
    \n" + 7 | "{{ if .Attribution.First }}
    \n— {{ .Attribution.First }}" + 8 | "{{ if .Attribution.Second }}
    \n{{ .Attribution.Second }}\n{{ else }}\n{{ end }}" + 9 | "
    \n{{ end }}
    \n" 10 | 11 | quoteParagraphTmpl = "
    \n" + 12 | "{{ if .Title }}
    {{ .Title }}
    \n{{ end }}" + 13 | "
    \n" + 14 | "{{ .Content }}\n" + 15 | "
    \n" + 16 | "{{ if .Attribution.First }}
    \n— {{ .Attribution.First }}" + 17 | "{{ if .Attribution.Second }}
    \n{{ .Attribution.Second }}\n{{ else }}\n{{ end }}" + 18 | "
    \n{{ end }}
    \n" 19 | 20 | thematicBreakTmpl = "
    \n" 21 | ) 22 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/string_test.go: -------------------------------------------------------------------------------- 1 | package xhtml5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("strings", func() { 11 | 12 | It("text with ellipsis", func() { 13 | source := `some text...` 14 | expected := `
    15 |

    some text…​

    16 |
    17 | ` 18 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 19 | }) 20 | 21 | It("text with copyright", func() { 22 | source := `Copyright (C)` 23 | expected := `
    24 |

    Copyright ©

    25 |
    26 | ` 27 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 28 | }) 29 | 30 | It("text with trademark", func() { 31 | source := `TheRightThing(TM)` 32 | expected := `
    33 |

    TheRightThing™

    34 |
    35 | ` 36 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 37 | }) 38 | 39 | It("text with registered", func() { 40 | source := `TheRightThing(R)` 41 | expected := `
    42 |

    TheRightThing®

    43 |
    44 | ` 45 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 46 | }) 47 | 48 | It("title with registered", func() { 49 | // We will often want to use these symbols in headers. 50 | source := `== Registered(R)` 51 | expected := `
    52 |

    Registered®

    53 |
    54 |
    55 |
    56 | ` 57 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/suite_test.go: -------------------------------------------------------------------------------- 1 | package xhtml5_test 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | 7 | "github.com/bytesparadise/libasciidoc" 8 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 9 | "github.com/bytesparadise/libasciidoc/pkg/types" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | log "github.com/sirupsen/logrus" 13 | 14 | "testing" 15 | 16 | _ "github.com/bytesparadise/libasciidoc/testsupport" 17 | ) 18 | 19 | func RenderXHTML(actual string, settings ...configuration.Setting) (string, error) { 20 | output, _, err := RenderXHTMLWithMetadata(actual, settings...) 21 | return output, err 22 | } 23 | 24 | func RenderXHTMLWithMetadata(actual string, settings ...configuration.Setting) (string, types.Metadata, error) { 25 | allSettings := append([]configuration.Setting{configuration.WithFilename("test.adoc"), configuration.WithBackEnd("xhtml5")}, settings...) 26 | config := configuration.NewConfiguration(allSettings...) 27 | 28 | contentReader := strings.NewReader(actual) 29 | resultWriter := bytes.NewBuffer(nil) 30 | metadata, err := libasciidoc.Convert(contentReader, resultWriter, config) 31 | if err != nil { 32 | return "", types.Metadata{}, err 33 | } 34 | if log.IsLevelEnabled(log.DebugLevel) { 35 | log.Debug(resultWriter.String()) 36 | } 37 | return resultWriter.String(), metadata, nil 38 | } 39 | 40 | func TestXHtml5(t *testing.T) { 41 | RegisterFailHandler(Fail) 42 | RunSpecs(t, "XHtml5 Suite") 43 | } 44 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/table.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | const ( 4 | tableTmpl = "\n" + 12 | "{{ if .Title }}\n{{ end }}" + 13 | "{{ if .Body }}" + 14 | "\n" + 15 | "{{ range $i, $w := .Columns }}\n{{ end}}" + 18 | "\n" + 19 | "{{ .Header }}" + 20 | "{{ .Body }}" + 21 | "{{ .Footer }}" + 22 | "{{ end }}" + 23 | "
    {{ .Caption }}{{ .Title }}
    \n" 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/thematic_break_test.go: -------------------------------------------------------------------------------- 1 | package xhtml5_test 2 | 3 | import ( 4 | . "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("thematic breaks", func() { 11 | 12 | It("simple", func() { 13 | source := `before 14 | 15 | ''' 16 | 17 | after` 18 | expected := `
    19 |

    before

    20 |
    21 |
    22 |
    23 |

    after

    24 |
    25 | ` 26 | Expect(RenderXHTML(source)).To(MatchHTML(expected)) 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /pkg/renderer/sgml/xhtml5/xhtml5.go: -------------------------------------------------------------------------------- 1 | package xhtml5 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 7 | "github.com/bytesparadise/libasciidoc/pkg/renderer/sgml" 8 | "github.com/bytesparadise/libasciidoc/pkg/renderer/sgml/html5" 9 | "github.com/bytesparadise/libasciidoc/pkg/types" 10 | ) 11 | 12 | // Render renders the document to the output, using the SGML renderer configured with the XHTML5 templates 13 | func Render(doc *types.Document, config *configuration.Configuration, output io.Writer) (types.Metadata, error) { 14 | templates := html5.Templates() 15 | // XHTML5 overrides of HTML5. 16 | templates.Article = articleTmpl 17 | templates.BlockImage = blockImageTmpl 18 | templates.LineBreak = lineBreakTmpl 19 | templates.DocumentAuthorDetails = documentAuthorDetailsTmpl 20 | templates.DocumentDetails = documentDetailsTmpl 21 | templates.Footnotes = footnotesTmpl 22 | templates.IconImage = iconImageTmpl 23 | templates.InlineImage = inlineImageTmpl 24 | templates.LabeledListHorizontalElement = labeledListHorizontalItemTmpl 25 | templates.Table = tableTmpl 26 | templates.ThematicBreak = thematicBreakTmpl 27 | templates.QuoteBlock = quoteBlockTmpl 28 | templates.QuoteParagraph = quoteParagraphTmpl 29 | templates.VerseBlock = verseBlockTmpl 30 | templates.VerseParagraph = verseParagraphTmpl 31 | 32 | return sgml.Render(doc, config, output, templates) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/types/document_xrefs.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // ElementReferences the element references in the document 4 | type ElementReferences map[string]interface{} 5 | 6 | // ElementReferencesCollector the visitor that traverses the whole document structure in search for elements with an ID 7 | -------------------------------------------------------------------------------- /pkg/types/footnotes.go: -------------------------------------------------------------------------------- 1 | package types 2 | -------------------------------------------------------------------------------- /pkg/types/predefined_attributes.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // PredefinedAttributes the predefined document attributes 4 | // May be converted into HTML entities 5 | var predefinedAttributes = []string{ 6 | "sp", 7 | "blank", 8 | "empty", 9 | "nbsp", 10 | "zwsp", 11 | "wj", 12 | "apos", 13 | "quot", 14 | "lsquo", 15 | "rsquo", 16 | "ldquo", 17 | "rdquo", 18 | "deg", 19 | "plus", 20 | "brvbar", 21 | "vbar", 22 | "amp", 23 | "lt", 24 | "gt", 25 | "startsb", 26 | "endsb", 27 | "caret", 28 | "asterisk", 29 | "tilde", 30 | "backslash", 31 | "backtick", 32 | "two-colons", 33 | "two-semicolons", 34 | "cpp", 35 | } 36 | 37 | func isPrefedinedAttribute(a string) bool { 38 | for _, v := range predefinedAttributes { 39 | if v == a { 40 | return true 41 | } 42 | } 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /pkg/types/types_suite_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | 6 | _ "github.com/bytesparadise/libasciidoc/testsupport" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestTypes(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Types Suite") 15 | } 16 | -------------------------------------------------------------------------------- /pkg/validator/validator_suite_test.go: -------------------------------------------------------------------------------- 1 | package validator_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestValidator(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Validator Suite") 13 | } 14 | -------------------------------------------------------------------------------- /test/compat/article.adoc: -------------------------------------------------------------------------------- 1 | = AsciiDoc Article Title 2 | Firstname Lastname 3 | 1.0, July 29, 2014, Asciidoctor 1.5 article template 4 | :toc: 5 | :icons: font 6 | :quick-uri: https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/ 7 | 8 | Content entered directly below the header but before the first section heading is called the preamble. 9 | 10 | == First level heading 11 | 12 | This is a paragraph with a *bold* word and an _italicized_ word. 13 | 14 | .Image caption 15 | image::image-file-name.png[I am the image alt text.] 16 | 17 | This is another paragraph.footnote:[I am footnote text and will be displayed at the bottom of the article.] 18 | 19 | === Second level heading 20 | 21 | .Unordered list title 22 | * list item 1 23 | ** nested list item 24 | *** nested nested list item 1 25 | *** nested nested list item 2 26 | * list item 2 27 | 28 | This is a paragraph. 29 | 30 | .Example block title 31 | ==== 32 | Content in an example block is subject to normal substitutions. 33 | ==== 34 | 35 | .Sidebar title 36 | **** 37 | Sidebars contain aside text and are subject to normal substitutions. 38 | **** 39 | 40 | ==== Third level heading 41 | 42 | [#id-for-listing-block] 43 | .Listing block title 44 | ---- 45 | Content in a listing block is subject to verbatim substitutions. 46 | Listing block content is commonly used to preserve code input. 47 | ---- 48 | 49 | ===== Fourth level heading 50 | 51 | .Table title 52 | |=== 53 | |Column heading 1 |Column heading 2 54 | 55 | |Column 1, row 1 56 | |Column 2, row 1 57 | 58 | |Column 1, row 2 59 | |Column 2, row 2 60 | |=== 61 | 62 | ====== Fifth level heading 63 | 64 | [quote, firstname lastname, movie title] 65 | ____ 66 | I am a block quote or a prose excerpt. 67 | I am subject to normal substitutions. 68 | ____ 69 | 70 | [verse, firstname lastname, poem title and more] 71 | ____ 72 | I am a verse block. 73 | Indents and endlines are preserved in verse blocks. 74 | ____ 75 | 76 | == First level heading 77 | 78 | TIP: There are five admonition labels: Tip, Note, Important, Caution and Warning. 79 | 80 | // I am a comment and won't be rendered. 81 | 82 | . ordered list item 83 | .. nested ordered list item 84 | . ordered list item 85 | 86 | The text at the end of this sentence is cross referenced to <<_third_level_heading,the third level heading>> 87 | 88 | == First level heading 89 | 90 | This is a link to the https://asciidoctor.org/docs/user-manual/[Asciidoctor User Manual]. 91 | This is an attribute reference {quick-uri}[which links this text to the Asciidoctor Quick Reference Guide]. 92 | -------------------------------------------------------------------------------- /test/compat/basic.adoc: -------------------------------------------------------------------------------- 1 | = Introduction to AsciiDoc 2 | Doc Writer 3 | 4 | A preface about https://asciidoc.org[AsciiDoc]. 5 | 6 | == First Section 7 | 8 | * item 1 9 | * item 2 10 | 11 | [source,ruby] 12 | puts "Hello, World!" 13 | -------------------------------------------------------------------------------- /test/compat/basic.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    A preface about AsciiDoc.

    5 |
    6 |
    7 |
    8 |
    9 |

    First Section

    10 |
    11 |
    12 |
      13 |
    • 14 |

      item 1

      15 |
    • 16 |
    • 17 |

      item 2

      18 |
    • 19 |
    20 |
    21 |
    22 |
    23 |
    puts "Hello, World!"
    24 |
    25 |
    26 |
    27 |
    28 | -------------------------------------------------------------------------------- /test/compat/demo-shorter.adoc: -------------------------------------------------------------------------------- 1 | [[purpose]] 2 | .Purpose 3 | **** 4 | This document exercises many of the features of AsciiDoc to test the {library} implementation. 5 | **** -------------------------------------------------------------------------------- /test/compat/demo-shorter.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    Purpose
    4 |
    5 |

    This document exercises many of the features of AsciiDoc to test the {library} implementation.

    6 |
    7 |
    8 |
    9 | -------------------------------------------------------------------------------- /test/compat/encoding.adoc: -------------------------------------------------------------------------------- 1 | Gregory Romé has written an AsciiDoc plugin for the Redmine project management application. 2 | 3 | https://github.com/foo-users/foo 4 | へと `vicmd` キーマップを足してみている試み、 5 | アニメーションgifです。 6 | 7 | tag::romé[] 8 | Gregory Romé has written an AsciiDoc plugin for the Redmine project management application. 9 | end::romé[] 10 | 11 | == Überschrift 12 | 13 | * Codierungen sind verrückt auf älteren Versionen von Ruby 14 | -------------------------------------------------------------------------------- /test/compat/encoding.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Gregory Romé has written an AsciiDoc plugin for the Redmine project management application.

    3 |
    4 |
    5 |

    https://github.com/foo-users/foo 6 | へと vicmd キーマップを足してみている試み、 7 | アニメーションgifです。

    8 |
    9 |
    10 |

    tag::romé[] 11 | Gregory Romé has written an AsciiDoc plugin for the Redmine project management application. 12 | end::romé[]

    13 |
    14 |
    15 |

    Überschrift

    16 |
    17 |
    18 |
      19 |
    • 20 |

      Codierungen sind verrückt auf älteren Versionen von Ruby

      21 |
    • 22 |
    23 |
    24 |
    25 |
    26 | -------------------------------------------------------------------------------- /test/compat/exline-mono.adoc: -------------------------------------------------------------------------------- 1 | :encoding: UTF-8 2 | 3 | The document is assumed to be encoded as `{encoding}`. 4 | -------------------------------------------------------------------------------- /test/compat/exline-mono.html: -------------------------------------------------------------------------------- 1 |
    2 |

    The document is assumed to be encoded as UTF-8.

    3 |
    4 | -------------------------------------------------------------------------------- /test/compat/include.adoc: -------------------------------------------------------------------------------- 1 | I come from an include file. -------------------------------------------------------------------------------- /test/compat/includes/chapter-a.adoc: -------------------------------------------------------------------------------- 1 | = Chapter A 2 | 3 | content 4 | -------------------------------------------------------------------------------- /test/compat/includes/child-include.adoc: -------------------------------------------------------------------------------- 1 | first line of child 2 | 3 | include::grandchild-include.adoc[] 4 | 5 | last line of child 6 | -------------------------------------------------------------------------------- /test/compat/includes/grandchild-include.adoc: -------------------------------------------------------------------------------- 1 | first line of grandchild 2 | 3 | last line of grandchild 4 | -------------------------------------------------------------------------------- /test/compat/lists.adoc: -------------------------------------------------------------------------------- 1 | = Document Title 2 | Doc Writer 3 | 4 | Preamble paragraph. 5 | 6 | NOTE: This is test, only a test. 7 | 8 | == Lists 9 | 10 | .Unordered, basic 11 | * Edgar Allen Poe 12 | * Sheri S. Tepper 13 | * Bill Bryson 14 | 15 | .Unordered, max nesting 16 | * level 1 17 | ** level 2 18 | *** level 3 19 | **** level 4 20 | ***** level 5 21 | * level 1 22 | 23 | .Checklist 24 | - [*] checked 25 | - [x] also checked 26 | - [ ] not checked 27 | - normal list item 28 | 29 | .Ordered, basic 30 | . Step 1 31 | . Step 2 32 | . Step 3 33 | 34 | .Ordered, nested 35 | . Step 1 36 | . Step 2 37 | .. Step 2a 38 | .. Step 2b 39 | . Step 3 40 | 41 | .Ordered, max nesting 42 | . level 1 43 | .. level 2 44 | ... level 3 45 | .... level 4 46 | ..... level 5 47 | . level 1 48 | 49 | .Labeled, single-line 50 | first term:: definition of first term 51 | section term:: definition of second term 52 | 53 | .Labeled, multi-line 54 | first term:: 55 | definition of first term 56 | second term:: 57 | definition of second term 58 | 59 | .Q&A 60 | [qanda] 61 | What is Asciidoctor?:: 62 | An implementation of the AsciiDoc processor in Ruby. 63 | What is the answer to the Ultimate Question?:: 42 64 | 65 | .Mixed 66 | Operating Systems:: 67 | Linux::: 68 | . Fedora 69 | * Desktop 70 | . Ubuntu 71 | * Desktop 72 | * Server 73 | BSD::: 74 | . FreeBSD 75 | . NetBSD 76 | 77 | Cloud Providers:: 78 | PaaS::: 79 | . OpenShift 80 | . CloudBees 81 | IaaS::: 82 | . Amazon EC2 83 | . Rackspace 84 | 85 | .Unordered, complex 86 | * level 1 87 | ** level 2 88 | *** level 3 89 | This is a new line inside an unordered list using {plus} symbol. 90 | We can even force content to start on a separate line... + 91 | Amazing, isn't it? 92 | **** level 4 93 | + 94 | The {plus} symbol is on a new line. 95 | 96 | ***** level 5 97 | -------------------------------------------------------------------------------- /test/compat/master.adoc: -------------------------------------------------------------------------------- 1 | = Master Document 2 | 3 | preamble 4 | 5 | include::includes/chapter-a.adoc[leveloffset=+1] 6 | -------------------------------------------------------------------------------- /test/compat/master.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    preamble

    5 |
    6 |
    7 |
    8 |
    9 |

    Chapter A

    10 |
    11 |
    12 |

    content

    13 |
    14 |
    15 |
    16 | -------------------------------------------------------------------------------- /test/compat/parent-include.adoc: -------------------------------------------------------------------------------- 1 | first line of parent 2 | 3 | include::includes/child-include.adoc[] 4 | 5 | last line of parent 6 | -------------------------------------------------------------------------------- /test/compat/parent-include.html: -------------------------------------------------------------------------------- 1 |
    2 |

    first line of parent

    3 |
    4 |
    5 |

    first line of child

    6 |
    7 |
    8 |

    first line of grandchild

    9 |
    10 |
    11 |

    last line of grandchild

    12 |
    13 |
    14 |

    last line of child

    15 |
    16 |
    17 |

    last line of parent

    18 |
    19 | -------------------------------------------------------------------------------- /test/compat/sample.adoc: -------------------------------------------------------------------------------- 1 | = Document Title 2 | Doc Writer 3 | :idprefix: id_ 4 | 5 | Preamble paragraph. 6 | 7 | NOTE: This is test, only a test. 8 | 9 | == Section A 10 | 11 | *Section A* paragraph. 12 | 13 | === Section A Subsection 14 | 15 | *Section A* 'subsection' paragraph. 16 | 17 | == Section B 18 | 19 | *Section B* paragraph. 20 | 21 | |=== 22 | |a |b |c 23 | |1 |2 |3 24 | |=== 25 | 26 | .Section B list 27 | * Item 1 28 | * Item 2 29 | * Item 3 30 | -------------------------------------------------------------------------------- /test/compat/sample.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |

    Preamble paragraph.

    5 |
    6 |
    7 | 8 | 9 | 12 | 15 | 16 |
    10 |
    Note
    11 |
    13 | This is test, only a test. 14 |
    17 |
    18 |
    19 |
    20 |
    21 |

    Section A

    22 |
    23 |
    24 |

    Section A paragraph.

    25 |
    26 |
    27 |

    Section A Subsection

    28 |
    29 |

    Section A 'subsection' paragraph.

    30 |
    31 |
    32 |
    33 |
    34 |
    35 |

    Section B

    36 |
    37 |
    38 |

    Section B paragraph.

    39 |
    40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |

    a

    b

    c

    1

    2

    3

    59 |
    60 |
    Section B list
    61 |
      62 |
    • 63 |

      Item 1

      64 |
    • 65 |
    • 66 |

      Item 2

      67 |
    • 68 |
    • 69 |

      Item 3

      70 |
    • 71 |
    72 |
    73 |
    74 |
    75 | -------------------------------------------------------------------------------- /test/compat/sscript.adoc: -------------------------------------------------------------------------------- 1 | _v_~rocket~ is the value 2 | ^3^He is the isotope 3 | log~4~x^n^ is the expression 4 | M^me^ White is the address 5 | the 10^th^ point has coordinate (x~10~, y~10~) 6 | -------------------------------------------------------------------------------- /test/compat/sscript.html: -------------------------------------------------------------------------------- 1 |
    2 |

    vrocket is the value 3 | 3He is the isotope 4 | log4xn is the expression 5 | Mme White is the address 6 | the 10th point has coordinate (x10, y10)

    7 |
    8 | -------------------------------------------------------------------------------- /test/compat_test.go: -------------------------------------------------------------------------------- 1 | package test_test 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/bytesparadise/libasciidoc" 12 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 13 | . "github.com/bytesparadise/libasciidoc/testsupport" 14 | 15 | . "github.com/onsi/ginkgo/v2" 16 | . "github.com/onsi/gomega" 17 | log "github.com/sirupsen/logrus" 18 | ) 19 | 20 | var autocrlf string 21 | 22 | func init() { 23 | cmd := exec.Command("git", "config", "--get", "core.autocrlf") 24 | // Attach buffer to command 25 | cmdOutput := bytes.NewBuffer(nil) 26 | cmd.Stdout = cmdOutput 27 | // execute command 28 | err := cmd.Run() 29 | if err != nil { 30 | log.Errorf("failed to check the `git config --get autocrlf': %v", err) 31 | return 32 | } 33 | autocrlf = strings.Trim(cmdOutput.String(), "\r\n") 34 | log.Warnf("git autocrlf='%s'", autocrlf) 35 | 36 | } 37 | 38 | var _ = DescribeTable("compat", args("compat/*.adoc")...) 39 | 40 | func args(pattern string) []interface{} { 41 | result := []interface{}{} 42 | result = append(result, compare) 43 | files, _ := filepath.Glob(pattern) 44 | for _, file := range files { 45 | if filepath.Base(file) == "include.adoc" { 46 | // skip 47 | continue 48 | } 49 | result = append(result, Entry(file, file)) 50 | } 51 | return result 52 | 53 | } 54 | 55 | func compare(filename string) { 56 | actual := &strings.Builder{} 57 | _, err := libasciidoc.ConvertFile(actual, configuration.NewConfiguration( 58 | configuration.WithFilename(filename), 59 | configuration.WithBackEnd("html5"), 60 | configuration.WithAttribute("libasciidoc-version", "0.7.0"), 61 | )) 62 | Expect(err).NotTo(HaveOccurred()) 63 | // retrieve the reference document 64 | path := strings.TrimSuffix(filename, ".adoc") + ".html" 65 | content, err := os.ReadFile(path) 66 | Expect(err).NotTo(HaveOccurred()) 67 | expected := string(content) 68 | // if tests are executed on windows platform and git 'autocrlf' is set to 'true', 69 | // then we need to remove the `\r` characters that were added in the 'expected' 70 | // source at the time of the checkout 71 | if runtime.GOOS == "windows" && autocrlf == "true" { 72 | expected = strings.Replace(expected, "\r", "", -1) 73 | } 74 | // compare actual vs reference 75 | Expect(actual.String()).To(MatchHTML(expected)) 76 | } 77 | -------------------------------------------------------------------------------- /test/doc.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | // this package contains adoc file with various features that are verified to be supported by Libasciidoc 4 | // i.e., they should produce the same output as Asciidoctor 5 | -------------------------------------------------------------------------------- /test/images/favicon-glasses-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytesparadise/libasciidoc/d90d46fa316478894fb43f9e4277807140e563ea/test/images/favicon-glasses-16x16.png -------------------------------------------------------------------------------- /test/includes/attributes.adoc: -------------------------------------------------------------------------------- 1 | :author: Xavier 2 | :leveloffset: +1 3 | 4 | some content -------------------------------------------------------------------------------- /test/includes/chapter-a.adoc: -------------------------------------------------------------------------------- 1 | = Chapter A 2 | 3 | content -------------------------------------------------------------------------------- /test/includes/child-include-preamble.adoc: -------------------------------------------------------------------------------- 1 | child preamble 2 | 3 | == child section 1 4 | 5 | first line of child 6 | 7 | include::grandchild-include.adoc[leveloffset=+1] 8 | 9 | === child section 2 10 | 11 | last line of child 12 | -------------------------------------------------------------------------------- /test/includes/child-include.adoc: -------------------------------------------------------------------------------- 1 | = child title 2 | 3 | first line of child 4 | 5 | include::grandchild-include.adoc[] 6 | 7 | last line of child 8 | -------------------------------------------------------------------------------- /test/includes/grandchild-include.adoc: -------------------------------------------------------------------------------- 1 | == grandchild title 2 | 3 | first line of grandchild 4 | 5 | last line of grandchild 6 | -------------------------------------------------------------------------------- /test/includes/hello_world.go.txt: -------------------------------------------------------------------------------- 1 | package includes 2 | 3 | import "fmt" 4 | 5 | func helloworld() { 6 | fmt.Println("hello, world!") 7 | } 8 | -------------------------------------------------------------------------------- /test/includes/include.foo: -------------------------------------------------------------------------------- 1 | *some strong content* 2 | 3 | include::hello_world.go.txt[] 4 | -------------------------------------------------------------------------------- /test/includes/parent-include-absolute-offset.adoc: -------------------------------------------------------------------------------- 1 | = parent title 2 | 3 | first line of parent 4 | 5 | include::child-include-preamble.adoc[leveloffset=3] 6 | 7 | last line of parent 8 | -------------------------------------------------------------------------------- /test/includes/parent-include-relative-offset.adoc: -------------------------------------------------------------------------------- 1 | = parent title 2 | 3 | first line of parent 4 | 5 | include::child-include-preamble.adoc[leveloffset=+1] 6 | 7 | last line of parent 8 | -------------------------------------------------------------------------------- /test/includes/parent-include.adoc: -------------------------------------------------------------------------------- 1 | :leveloffset: +1 2 | 3 | = parent title 4 | 5 | first line of parent 6 | 7 | include::child-include.adoc[] 8 | 9 | last line of parent <1> 10 | 11 | :leveloffset!: -------------------------------------------------------------------------------- /test/includes/sample.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | *foo* 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/includes/table_common.txt: -------------------------------------------------------------------------------- 1 | | Header A | Header B 2 | 3 | -------------------------------------------------------------------------------- /test/includes/table_parent.adoc: -------------------------------------------------------------------------------- 1 | |=== 2 | include::table_common.txt[] 3 | include::table_specific.txt[] 4 | |=== -------------------------------------------------------------------------------- /test/includes/table_specific.txt: -------------------------------------------------------------------------------- 1 | | Column A.1 | Column A.2 2 | | Column B.1 | Column B.2 -------------------------------------------------------------------------------- /test/includes/tag-include-unclosed.adoc: -------------------------------------------------------------------------------- 1 | // tag::doc[] 2 | // tag::section[] 3 | == Section 1 4 | // end::section[] 5 | 6 | // tag::unclosed[] 7 | 8 | // tag::content[] 9 | content 10 | 11 | // end::content[] 12 | // end::doc[] 13 | 14 | end -------------------------------------------------------------------------------- /test/includes/tag-include.adoc: -------------------------------------------------------------------------------- 1 | // tag::doc[] 2 | // tag::section[] 3 | == Section 1 4 | // end::section[] 5 | 6 | // tag::content[] 7 | content 8 | 9 | // end::content[] 10 | // end::doc[] 11 | 12 | end -------------------------------------------------------------------------------- /test/test_suite_test.go: -------------------------------------------------------------------------------- 1 | package test_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | 9 | _ "github.com/bytesparadise/libasciidoc/testsupport" 10 | ) 11 | 12 | func TestTest(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Test Suite") 15 | } 16 | -------------------------------------------------------------------------------- /testsupport/document_fragment_matcher.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | 8 | "github.com/davecgh/go-spew/spew" 9 | "github.com/google/go-cmp/cmp" 10 | gomegatypes "github.com/onsi/gomega/types" 11 | "github.com/pkg/errors" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // MatchDocumentFragment a custom matcher to verify that a document matches the given expectation 16 | // Similar to the standard `Equal` matcher, but display a diff when the values don't match 17 | func MatchDocumentFragment(expected types.DocumentFragment) gomegatypes.GomegaMatcher { 18 | return &documentFragmentMatcher{ 19 | expected: expected, 20 | } 21 | } 22 | 23 | type documentFragmentMatcher struct { 24 | expected types.DocumentFragment 25 | diffs string 26 | } 27 | 28 | func (m *documentFragmentMatcher) Match(actual interface{}) (success bool, err error) { 29 | if _, ok := actual.(types.DocumentFragment); !ok { 30 | return false, errors.Errorf("MatchDocumentFragment matcher expects a 'types.DocumentFragment' (actual: %T)", actual) 31 | } 32 | if diff := cmp.Diff(m.expected, actual, opts...); diff != "" { 33 | if log.IsLevelEnabled(log.DebugLevel) { 34 | log.Debugf("actual document fragment:\n%s", spew.Sdump(actual)) 35 | log.Debugf("expected document fragment:\n%s", spew.Sdump(m.expected)) 36 | } 37 | m.diffs = diff 38 | return false, nil 39 | } 40 | return true, nil 41 | } 42 | 43 | func (m *documentFragmentMatcher) FailureMessage(_ interface{}) (message string) { 44 | return fmt.Sprintf("expected document fragments to match:\n%s", m.diffs) 45 | } 46 | 47 | func (m *documentFragmentMatcher) NegatedFailureMessage(_ interface{}) (message string) { 48 | return fmt.Sprintf("expected document fragments not to match:\n%s", m.diffs) 49 | } 50 | -------------------------------------------------------------------------------- /testsupport/document_fragment_matcher_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/bytesparadise/libasciidoc/testsupport" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("document fragments matcher", func() { 15 | 16 | // given 17 | expected := types.DocumentFragment{ 18 | Elements: []interface{}{ 19 | &types.RawLine{ 20 | Content: "a paragraph.", 21 | }, 22 | }, 23 | } 24 | matcher := testsupport.MatchDocumentFragment(expected) 25 | 26 | It("should match", func() { 27 | // given 28 | actual := types.DocumentFragment{ 29 | Elements: []interface{}{ 30 | &types.RawLine{ 31 | Content: "a paragraph.", 32 | }, 33 | }, 34 | } 35 | // when 36 | result, err := matcher.Match(actual) 37 | // then 38 | Expect(err).ToNot(HaveOccurred()) 39 | Expect(result).To(BeTrue()) 40 | }) 41 | 42 | It("should not match", func() { 43 | // given 44 | actual := types.DocumentFragment{ 45 | Elements: []interface{}{ 46 | &types.RawLine{ 47 | Content: "something else", 48 | }, 49 | }, 50 | } 51 | // when 52 | result, err := matcher.Match(actual) 53 | // then 54 | Expect(err).ToNot(HaveOccurred()) 55 | Expect(result).To(BeFalse()) 56 | diffs := cmp.Diff(expected, actual) 57 | Expect(matcher.FailureMessage(actual)).To(Equal(fmt.Sprintf("expected document fragments to match:\n%s", diffs))) 58 | Expect(matcher.NegatedFailureMessage(actual)).To(Equal(fmt.Sprintf("expected document fragments not to match:\n%s", diffs))) 59 | }) 60 | 61 | It("should return error when invalid type is input", func() { 62 | // when 63 | result, err := matcher.Match(1) 64 | // then 65 | Expect(err).To(HaveOccurred()) 66 | Expect(err.Error()).To(Equal("MatchDocumentFragment matcher expects a 'types.DocumentFragment' (actual: int)")) 67 | Expect(result).To(BeFalse()) 68 | }) 69 | 70 | }) 71 | -------------------------------------------------------------------------------- /testsupport/document_fragments_matcher.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/davecgh/go-spew/spew" 8 | "github.com/google/go-cmp/cmp" 9 | gomegatypes "github.com/onsi/gomega/types" 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // MatchDocumentFragments a custom matcher to verify that a document matches the given expectation 15 | // Similar to the standard `Equal` matcher, but display a diff when the values don't match 16 | func MatchDocumentFragments(expected []types.DocumentFragment) gomegatypes.GomegaMatcher { 17 | return &documentFragmentsMatcher{ 18 | expected: expected, 19 | } 20 | } 21 | 22 | type documentFragmentsMatcher struct { 23 | expected []types.DocumentFragment 24 | diffs string 25 | } 26 | 27 | func (m *documentFragmentsMatcher) Match(actual interface{}) (success bool, err error) { 28 | if _, ok := actual.([]types.DocumentFragment); !ok { 29 | return false, errors.Errorf("MatchDocumentFragments matcher expects a '[]types.DocumentFragment' (actual: %T)", actual) 30 | } 31 | if diff := cmp.Diff(m.expected, actual, opts...); diff != "" { 32 | if log.IsLevelEnabled(log.DebugLevel) { 33 | log.Debugf("actual document fragments:\n%s", spew.Sdump(actual)) 34 | log.Debugf("expected document fragments:\n%s", spew.Sdump(m.expected)) 35 | } 36 | m.diffs = diff 37 | return false, nil 38 | } 39 | return true, nil 40 | } 41 | 42 | func (m *documentFragmentsMatcher) FailureMessage(_ interface{}) (message string) { 43 | return fmt.Sprintf("expected document fragments to match:\n%s", m.diffs) 44 | } 45 | 46 | func (m *documentFragmentsMatcher) NegatedFailureMessage(_ interface{}) (message string) { 47 | return fmt.Sprintf("expected document fragments not to match:\n%s", m.diffs) 48 | } 49 | -------------------------------------------------------------------------------- /testsupport/document_fragments_matcher_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/bytesparadise/libasciidoc/testsupport" 8 | "github.com/google/go-cmp/cmp" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("document fragment matcher", func() { 15 | 16 | // given 17 | expected := []types.DocumentFragment{ 18 | { 19 | Elements: []interface{}{ 20 | &types.RawLine{ 21 | Content: "a paragraph.", 22 | }, 23 | }, 24 | }, 25 | } 26 | matcher := testsupport.MatchDocumentFragments(expected) 27 | 28 | It("should match", func() { 29 | // given 30 | actual := []types.DocumentFragment{ 31 | { 32 | Elements: []interface{}{ 33 | &types.RawLine{ 34 | Content: "a paragraph.", 35 | }, 36 | }, 37 | }, 38 | } 39 | // when 40 | result, err := matcher.Match(actual) 41 | // then 42 | Expect(err).ToNot(HaveOccurred()) 43 | Expect(result).To(BeTrue()) 44 | }) 45 | 46 | It("should not match", func() { 47 | // given 48 | actual := []types.DocumentFragment{ 49 | { 50 | Elements: []interface{}{ 51 | &types.RawLine{ 52 | Content: "something else", 53 | }, 54 | }, 55 | }, 56 | } 57 | // when 58 | result, err := matcher.Match(actual) 59 | // then 60 | Expect(err).ToNot(HaveOccurred()) 61 | Expect(result).To(BeFalse()) 62 | diffs := cmp.Diff(expected, actual) 63 | Expect(matcher.FailureMessage(actual)).To(Equal(fmt.Sprintf("expected document fragments to match:\n%s", diffs))) 64 | Expect(matcher.NegatedFailureMessage(actual)).To(Equal(fmt.Sprintf("expected document fragments not to match:\n%s", diffs))) 65 | }) 66 | 67 | It("should return error when invalid type is input", func() { 68 | // when 69 | result, err := matcher.Match(1) 70 | // then 71 | Expect(err).To(HaveOccurred()) 72 | Expect(err.Error()).To(Equal("MatchDocumentFragments matcher expects a '[]types.DocumentFragment' (actual: int)")) 73 | Expect(result).To(BeFalse()) 74 | }) 75 | 76 | }) 77 | -------------------------------------------------------------------------------- /testsupport/document_matcher.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | 8 | "github.com/davecgh/go-spew/spew" 9 | "github.com/google/go-cmp/cmp" 10 | "github.com/google/go-cmp/cmp/cmpopts" 11 | gomegatypes "github.com/onsi/gomega/types" 12 | "github.com/pkg/errors" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // MatchDocument a custom matcher to verify that a document matches the given expectation 17 | // Similar to the standard `Equal` matcher, but display a diff when the values don't match 18 | func MatchDocument(expected *types.Document) gomegatypes.GomegaMatcher { 19 | return &documentMatcher{ 20 | expected: expected, 21 | } 22 | } 23 | 24 | type documentMatcher struct { 25 | expected *types.Document 26 | diffs string 27 | } 28 | 29 | var opts = []cmp.Option{cmpopts.IgnoreUnexported( 30 | types.List{}, 31 | types.DelimitedBlock{}, 32 | types.Footnotes{}, 33 | types.TableOfContents{}, 34 | types.AttributeDeclaration{}, 35 | types.AttributeReference{}, 36 | types.AttributeReset{}, 37 | types.CounterSubstitution{}, 38 | types.PredefinedAttribute{}, 39 | )} 40 | 41 | func (m *documentMatcher) Match(actual interface{}) (success bool, err error) { 42 | if _, ok := actual.(*types.Document); !ok { 43 | return false, errors.Errorf("MatchDocument matcher expects a 'types.Document' (actual: %T)", actual) 44 | } 45 | if diff := cmp.Diff(m.expected, actual, opts...); diff != "" { 46 | if log.IsLevelEnabled(log.DebugLevel) { 47 | log.Debugf("actual document:\n%s", spew.Sdump(actual)) 48 | log.Debugf("expected document:\n%s", spew.Sdump(m.expected)) 49 | } 50 | m.diffs = diff 51 | return false, nil 52 | } 53 | return true, nil 54 | } 55 | 56 | func (m *documentMatcher) FailureMessage(_ interface{}) (message string) { 57 | return fmt.Sprintf("expected documents to match:\n%s", m.diffs) 58 | } 59 | 60 | func (m *documentMatcher) NegatedFailureMessage(_ interface{}) (message string) { 61 | return fmt.Sprintf("expected documents not to match:\n%s", m.diffs) 62 | } 63 | -------------------------------------------------------------------------------- /testsupport/document_matcher_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/bytesparadise/libasciidoc/testsupport" 8 | "github.com/google/go-cmp/cmp" 9 | 10 | . "github.com/onsi/ginkgo/v2" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var _ = Describe("document matcher", func() { 15 | 16 | // given 17 | expected := &types.Document{ 18 | Elements: []interface{}{ 19 | &types.Paragraph{ 20 | Elements: []interface{}{ 21 | &types.StringElement{ 22 | Content: "a paragraph.", 23 | }, 24 | }, 25 | }, 26 | }, 27 | } 28 | matcher := testsupport.MatchDocument(expected) 29 | 30 | It("should match", func() { 31 | // given 32 | actual := &types.Document{ 33 | Elements: []interface{}{ 34 | &types.Paragraph{ 35 | Elements: []interface{}{ 36 | &types.StringElement{ 37 | Content: "a paragraph.", 38 | }, 39 | }, 40 | }, 41 | }, 42 | } 43 | // when 44 | result, err := matcher.Match(actual) 45 | // then 46 | Expect(err).ToNot(HaveOccurred()) 47 | Expect(result).To(BeTrue()) 48 | }) 49 | 50 | It("should not match", func() { 51 | // given 52 | actual := &types.Document{ 53 | Elements: []interface{}{ 54 | &types.Paragraph{ 55 | Elements: []interface{}{ 56 | &types.StringElement{ 57 | Content: "another paragraph.", 58 | }, 59 | }, 60 | }, 61 | }, 62 | } 63 | // when 64 | result, err := matcher.Match(actual) 65 | // then 66 | Expect(err).ToNot(HaveOccurred()) 67 | Expect(result).To(BeFalse()) 68 | diffs := cmp.Diff(expected, actual) 69 | Expect(matcher.FailureMessage(actual)).To(Equal(fmt.Sprintf("expected documents to match:\n%s", diffs))) 70 | Expect(matcher.NegatedFailureMessage(actual)).To(Equal(fmt.Sprintf("expected documents not to match:\n%s", diffs))) 71 | }) 72 | 73 | It("should return error when invalid type is input", func() { 74 | // when 75 | result, err := matcher.Match(1) 76 | // then 77 | Expect(err).To(HaveOccurred()) 78 | Expect(err.Error()).To(Equal("MatchDocument matcher expects a 'types.Document' (actual: int)")) 79 | Expect(result).To(BeFalse()) 80 | }) 81 | 82 | }) 83 | -------------------------------------------------------------------------------- /testsupport/inline_elements_matcher.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/davecgh/go-spew/spew" 8 | "github.com/google/go-cmp/cmp" 9 | gomegatypes "github.com/onsi/gomega/types" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // MatchInlineElements a custom matcher to verify that a document matches the given expectation 14 | // Similar to the standard `Equal` matcher, but display a diff when the values don't match 15 | func MatchInlineElements(expected []interface{}) gomegatypes.GomegaMatcher { 16 | return &inlineElementsMatcher{ 17 | expected: expected, 18 | } 19 | } 20 | 21 | type inlineElementsMatcher struct { 22 | expected []interface{} 23 | diffs string 24 | } 25 | 26 | func (m *inlineElementsMatcher) Match(actual interface{}) (success bool, err error) { 27 | if _, ok := actual.([]interface{}); !ok { 28 | return false, errors.Errorf("MatchInlineElements matcher expects a '[]interface{}' (actual: %T)", actual) 29 | } 30 | if !reflect.DeepEqual(m.expected, actual) { 31 | m.diffs = cmp.Diff(spew.Sdump(m.expected), spew.Sdump(actual)) 32 | return false, nil 33 | } 34 | return true, nil 35 | } 36 | 37 | func (m *inlineElementsMatcher) FailureMessage(_ interface{}) (message string) { 38 | return fmt.Sprintf("expected elements to match:\n%s", m.diffs) 39 | } 40 | 41 | func (m *inlineElementsMatcher) NegatedFailureMessage(_ interface{}) (message string) { 42 | return fmt.Sprintf("expected elements not to match:\n%s", m.diffs) 43 | } 44 | -------------------------------------------------------------------------------- /testsupport/inline_elements_matcher_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/bytesparadise/libasciidoc/testsupport" 8 | "github.com/google/go-cmp/cmp" 9 | 10 | "github.com/davecgh/go-spew/spew" 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("inline elements matcher", func() { 16 | 17 | // given 18 | expected := []interface{}{ 19 | &types.StringElement{ 20 | Content: "a paragraph.", 21 | }, 22 | } 23 | matcher := testsupport.MatchInlineElements(expected) 24 | 25 | It("should match", func() { 26 | // given 27 | actual := []interface{}{ 28 | &types.StringElement{ 29 | Content: "a paragraph.", 30 | }, 31 | } 32 | // when 33 | result, err := matcher.Match(actual) 34 | // then 35 | Expect(err).ToNot(HaveOccurred()) 36 | Expect(result).To(BeTrue()) 37 | }) 38 | 39 | It("should not match", func() { 40 | // given 41 | actual := []interface{}{ 42 | &types.StringElement{ 43 | Content: "another paragraph.", 44 | }, 45 | } 46 | // when 47 | result, err := matcher.Match(actual) 48 | // then 49 | Expect(err).ToNot(HaveOccurred()) 50 | Expect(result).To(BeFalse()) 51 | diffs := cmp.Diff(spew.Sdump(expected), spew.Sdump(actual)) 52 | Expect(matcher.FailureMessage(actual)).To(Equal(fmt.Sprintf("expected elements to match:\n%s", diffs))) 53 | Expect(matcher.NegatedFailureMessage(actual)).To(Equal(fmt.Sprintf("expected elements not to match:\n%s", diffs))) 54 | }) 55 | 56 | It("should return error when invalid type is input", func() { 57 | // when 58 | result, err := matcher.Match(1) 59 | // then 60 | Expect(err).To(HaveOccurred()) 61 | Expect(err.Error()).To(Equal("MatchInlineElements matcher expects a '[]interface{}' (actual: int)")) 62 | Expect(result).To(BeFalse()) 63 | }) 64 | 65 | }) 66 | -------------------------------------------------------------------------------- /testsupport/log_init.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | "github.com/davecgh/go-spew/spew" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/pflag" 10 | ) 11 | 12 | func init() { 13 | lvl := parseLogLevel() 14 | log.SetLevel(lvl) 15 | log.Warnf("Running test with logs in '%s' level", lvl.String()) 16 | log.SetFormatter(&log.TextFormatter{ 17 | DisableQuote: true, // see https://github.com/sirupsen/logrus/issues/608#issuecomment-745137306 18 | }) 19 | 20 | // also, configuration for spew (when dumping structures to compare results) 21 | spew.Config.DisableCapacities = true 22 | spew.Config.DisablePointerAddresses = true 23 | spew.Config.DisablePointerMethods = true 24 | spew.Config.DisableUnexported = true 25 | } 26 | 27 | func parseLogLevel() log.Level { 28 | var logLevel string 29 | // needed to let ginkgo parse the flag, otherwise, `ginkgo -- --loglevel=...` will fail with `flag provided but not defined: -loglevel` 30 | flag.StringVar(&logLevel, "loglevel", "error", "log level to set [debug|info|warn|error|fatal|panic]") 31 | // parse with a custom flagset in which all other flags (ginkgo's) are ignored 32 | f := pflag.NewFlagSet("passthroughs", pflag.ContinueOnError) 33 | f.ParseErrorsWhitelist.UnknownFlags = true 34 | f.StringVarP(&logLevel, "loglevel", "l", "error", "log level to set [debug|info|warn|error|fatal|panic]") 35 | if err := f.Parse(os.Args[1:]); err != nil { 36 | panic(err) 37 | } 38 | lvl, err := log.ParseLevel(logLevel) 39 | if err != nil { 40 | panic(err) 41 | } 42 | return lvl 43 | } 44 | -------------------------------------------------------------------------------- /testsupport/metadata_matcher.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/bytesparadise/libasciidoc/pkg/types" 8 | "github.com/google/go-cmp/cmp" 9 | 10 | "github.com/davecgh/go-spew/spew" 11 | . "github.com/onsi/ginkgo/v2" 12 | gomegatypes "github.com/onsi/gomega/types" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | func MatchMetadata(expected types.Metadata) gomegatypes.GomegaMatcher { 17 | return &metadataMatcher{ 18 | expected: expected, 19 | } 20 | } 21 | 22 | type metadataMatcher struct { 23 | expected types.Metadata 24 | diffs string 25 | } 26 | 27 | func (m *metadataMatcher) Match(actual interface{}) (success bool, err error) { 28 | if _, ok := actual.(types.Metadata); !ok { 29 | return false, errors.Errorf("MatchMetadata matcher expects a 'types.Metadata' (actual: %T)", actual) 30 | } 31 | if !reflect.DeepEqual(m.expected, actual) { 32 | GinkgoT().Logf("actual HTML:\n'%s'", actual) 33 | GinkgoT().Logf("expected HTML:\n'%s'", m.expected) 34 | m.diffs = cmp.Diff(spew.Sdump(m.expected), spew.Sdump(actual)) 35 | return false, nil 36 | } 37 | return true, nil 38 | } 39 | 40 | func (m *metadataMatcher) FailureMessage(_ interface{}) (message string) { 41 | return fmt.Sprintf("expected document metadata to match:\n%s", m.diffs) 42 | } 43 | 44 | func (m *metadataMatcher) NegatedFailureMessage(_ interface{}) (message string) { 45 | return fmt.Sprintf("expected document metadata not to match:\n%s", m.diffs) 46 | } 47 | -------------------------------------------------------------------------------- /testsupport/metadata_matcher_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 8 | "github.com/bytesparadise/libasciidoc/pkg/types" 9 | "github.com/bytesparadise/libasciidoc/testsupport" 10 | "github.com/davecgh/go-spew/spew" 11 | "github.com/google/go-cmp/cmp" 12 | 13 | . "github.com/onsi/ginkgo/v2" 14 | . "github.com/onsi/gomega" 15 | ) 16 | 17 | var _ = Describe("metadata matcher", func() { 18 | 19 | now := time.Now() 20 | 21 | It("should match", func() { 22 | // given 23 | actual := types.Metadata{ 24 | Title: "cheesecake", 25 | LastUpdated: now.Format(configuration.LastUpdatedFormat), 26 | } 27 | matcher := testsupport.MatchMetadata(types.Metadata{ 28 | Title: "cheesecake", 29 | LastUpdated: now.Format(configuration.LastUpdatedFormat), 30 | }) // same content 31 | // when 32 | result, err := matcher.Match(actual) 33 | // then 34 | Expect(err).ToNot(HaveOccurred()) 35 | Expect(result).To(BeTrue()) 36 | }) 37 | 38 | It("should not match", func() { 39 | // given 40 | actual := types.Metadata{ 41 | Title: "cheesecake", 42 | LastUpdated: now.Format(configuration.LastUpdatedFormat), 43 | } 44 | expected := types.Metadata{ 45 | Title: "chocolate", 46 | LastUpdated: now.Format(configuration.LastUpdatedFormat), 47 | } // not the same content 48 | matcher := testsupport.MatchMetadata(expected) 49 | // when 50 | result, err := matcher.Match(actual) 51 | // then 52 | Expect(err).ToNot(HaveOccurred()) 53 | Expect(result).To(BeFalse()) 54 | diffs := cmp.Diff(spew.Sdump(expected), spew.Sdump(actual)) 55 | Expect(matcher.FailureMessage(actual)).To(Equal(fmt.Sprintf("expected document metadata to match:\n%s", diffs))) 56 | Expect(matcher.NegatedFailureMessage(actual)).To(Equal(fmt.Sprintf("expected document metadata not to match:\n%s", diffs))) 57 | }) 58 | 59 | It("should return error when invalid type is input", func() { 60 | // given 61 | matcher := testsupport.MatchMetadata(types.Metadata{ 62 | Title: "cheesecake", 63 | LastUpdated: now.Format(configuration.LastUpdatedFormat), 64 | }) 65 | // when 66 | result, err := matcher.Match(1) 67 | // then 68 | Expect(err).To(HaveOccurred()) 69 | Expect(err.Error()).To(Equal("MatchMetadata matcher expects a 'types.Metadata' (actual: int)")) 70 | Expect(result).To(BeFalse()) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /testsupport/parse_document.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 7 | "github.com/bytesparadise/libasciidoc/pkg/parser" 8 | "github.com/bytesparadise/libasciidoc/pkg/types" 9 | 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // ParseDocument parses the actual value into a Document 15 | func ParseDocument(actual string, options ...interface{}) (*types.Document, error) { 16 | allSettings := []configuration.Setting{ 17 | configuration.WithFilename("test.adoc"), 18 | } 19 | opts := []parser.Option{} 20 | for _, o := range options { 21 | switch o := o.(type) { 22 | case configuration.Setting: 23 | allSettings = append(allSettings, o) 24 | case parser.Option: 25 | opts = append(opts, o) 26 | default: 27 | return nil, errors.Errorf("unexpected type of option: '%T'", o) 28 | } 29 | } 30 | c := configuration.NewConfiguration(allSettings...) 31 | p, err := parser.Preprocess(strings.NewReader(actual), c, opts...) 32 | if err != nil { 33 | return nil, err 34 | } 35 | if log.IsLevelEnabled(log.DebugLevel) { 36 | log.Debugf("preparsed document:\n%s", p) 37 | } 38 | return parser.ParseDocument(strings.NewReader(p), c, opts...) 39 | } 40 | -------------------------------------------------------------------------------- /testsupport/parse_document_fragments.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 7 | "github.com/bytesparadise/libasciidoc/pkg/parser" 8 | "github.com/bytesparadise/libasciidoc/pkg/types" 9 | ) 10 | 11 | // ParseDocumentFragments parses the actual source with the options 12 | func ParseDocumentFragments(actual string, options ...parser.Option) ([]types.DocumentFragment, error) { 13 | r := strings.NewReader(actual) 14 | ctx := parser.NewParseContext(configuration.NewConfiguration(), options...) 15 | done := make(chan interface{}) 16 | defer close(done) 17 | // ctx.Opts = append(ctx.Opts, parser.Debug(true)) 18 | fragmentStream := parser.ParseDocumentFragments(ctx, r, done) 19 | result := []types.DocumentFragment{} 20 | for f := range fragmentStream { 21 | result = append(result, f) 22 | } 23 | return result, nil 24 | } 25 | -------------------------------------------------------------------------------- /testsupport/parse_document_fragments_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | "github.com/bytesparadise/libasciidoc/testsupport" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("parse document fragment groups", func() { 12 | 13 | expected := []types.DocumentFragment{ 14 | { 15 | Position: types.Position{ 16 | Start: 0, 17 | End: 13, 18 | }, 19 | Elements: []interface{}{ 20 | &types.Paragraph{ 21 | Elements: []interface{}{ 22 | &types.RawLine{ 23 | Content: "hello, world!", 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | } 30 | 31 | It("should match", func() { 32 | // given 33 | actual := "hello, world!" 34 | // when 35 | result, err := testsupport.ParseDocumentFragments(actual) 36 | // then 37 | Expect(err).ToNot(HaveOccurred()) 38 | Expect(result).To(Equal(expected)) 39 | }) 40 | 41 | It("should not match", func() { 42 | // given 43 | actual := "foo" 44 | // when 45 | result, err := testsupport.ParseDocumentFragments(actual) 46 | // then 47 | Expect(err).ToNot(HaveOccurred()) 48 | Expect(result).NotTo(Equal(expected)) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /testsupport/parse_document_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/pkg/types" 5 | "github.com/bytesparadise/libasciidoc/testsupport" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("parse document", func() { 12 | 13 | expected := &types.Document{ 14 | Elements: []interface{}{ 15 | &types.Paragraph{ 16 | Elements: []interface{}{ 17 | &types.StringElement{ 18 | Content: "hello, world!", 19 | }, 20 | }, 21 | }, 22 | }, 23 | } 24 | 25 | It("should match", func() { 26 | // given 27 | actual := "hello, world!" 28 | // when 29 | result, err := testsupport.ParseDocument(actual) 30 | // then 31 | Expect(err).ToNot(HaveOccurred()) 32 | Expect(result).To(Equal(expected)) 33 | }) 34 | 35 | It("should not match", func() { 36 | // given 37 | actual := "foo" 38 | // when 39 | result, err := testsupport.ParseDocument(actual) 40 | // then 41 | Expect(err).ToNot(HaveOccurred()) 42 | Expect(result).NotTo(Equal(expected)) 43 | }) 44 | 45 | }) 46 | -------------------------------------------------------------------------------- /testsupport/preparse_document.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 7 | "github.com/bytesparadise/libasciidoc/pkg/parser" 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func PreparseDocument(source string, options ...interface{}) (string, error) { 14 | settings := []configuration.Setting{ 15 | configuration.WithFilename("test.adoc"), 16 | configuration.WithAttribute("basebackend-html", true), // TODO: still needed? 17 | } 18 | opts := []parser.Option{} 19 | for _, o := range options { 20 | switch o := o.(type) { 21 | case configuration.Setting: 22 | settings = append(settings, o) 23 | case parser.Option: 24 | opts = append(opts, o) 25 | default: 26 | return "", errors.Errorf("unexpected type of option: '%T'", o) 27 | } 28 | } 29 | result, err := parser.Preprocess(strings.NewReader(source), configuration.NewConfiguration(settings...), opts...) 30 | if log.IsLevelEnabled(log.DebugLevel) && err == nil { 31 | log.Debugf("preparsed document:\n%s", result) 32 | } 33 | return result, err 34 | 35 | } 36 | -------------------------------------------------------------------------------- /testsupport/render_html5.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "strings" 7 | 8 | "github.com/bytesparadise/libasciidoc" 9 | "github.com/bytesparadise/libasciidoc/pkg/configuration" 10 | "github.com/bytesparadise/libasciidoc/pkg/types" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // RenderHTML renders the HTML body using the given source 15 | func RenderHTML(actual string, settings ...configuration.Setting) (string, error) { 16 | output, _, err := RenderHTMLWithMetadata(actual, settings...) 17 | return output, err 18 | } 19 | 20 | // RenderHTML renders the HTML body using the given source 21 | func RenderHTMLWithMetadata(actual string, settings ...configuration.Setting) (string, types.Metadata, error) { 22 | allSettings := append([]configuration.Setting{configuration.WithFilename("test.adoc"), configuration.WithBackEnd("html5")}, settings...) 23 | config := configuration.NewConfiguration(allSettings...) 24 | contentReader := strings.NewReader(actual) 25 | resultWriter := bytes.NewBuffer(nil) 26 | metadata, err := libasciidoc.Convert(contentReader, resultWriter, config) 27 | if err != nil { 28 | log.Error(err) 29 | return "", types.Metadata{}, err 30 | } 31 | if log.IsLevelEnabled(log.DebugLevel) { 32 | log.Debug(resultWriter.String()) 33 | } 34 | return resultWriter.String(), metadata, nil 35 | } 36 | 37 | // RenderHTML renders the HTML body using the given source 38 | func RenderHTMLFromFile(filename string, settings ...configuration.Setting) (string, types.Metadata, error) { 39 | info, err := os.Stat(filename) 40 | if err != nil { 41 | log.Error(err) 42 | return "", types.Metadata{}, err 43 | } 44 | 45 | allSettings := append([]configuration.Setting{ 46 | configuration.WithLastUpdated(info.ModTime()), 47 | configuration.WithFilename(filename), 48 | configuration.WithBackEnd("html5")}, 49 | settings...) 50 | config := configuration.NewConfiguration(allSettings...) 51 | f, err := os.Open(filename) 52 | if err != nil { 53 | log.Error(err) 54 | return "", types.Metadata{}, err 55 | } 56 | defer func() { f.Close() }() 57 | resultWriter := bytes.NewBuffer(nil) 58 | metadata, err := libasciidoc.Convert(f, resultWriter, config) 59 | if err != nil { 60 | log.Error(err) 61 | return "", types.Metadata{}, err 62 | } 63 | if log.IsLevelEnabled(log.DebugLevel) { 64 | log.Debug(resultWriter.String()) 65 | } 66 | return resultWriter.String(), metadata, nil 67 | } 68 | -------------------------------------------------------------------------------- /testsupport/render_html5_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "github.com/bytesparadise/libasciidoc/testsupport" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("html5 body renderer", func() { 11 | 12 | It("should match", func() { 13 | // given 14 | actual := "hello, world!" 15 | // when 16 | result, err := testsupport.RenderHTML(actual) 17 | // then 18 | expected := `
    19 |

    hello, world!

    20 |
    21 | ` 22 | Expect(err).NotTo(HaveOccurred()) 23 | Expect(result).To(Equal(expected)) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /testsupport/table_of_contents_matcher.go: -------------------------------------------------------------------------------- 1 | package testsupport 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/davecgh/go-spew/spew" 8 | "github.com/google/go-cmp/cmp" 9 | gomegatypes "github.com/onsi/gomega/types" 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // MatchTableOfContents a custom matcher to verify that a TableOfContents matches the given expectation 15 | // Similar to the standard `Equal` matcher, but display a diff when the values don't match 16 | func MatchTableOfContents(expected *types.TableOfContents) gomegatypes.GomegaMatcher { 17 | return &tableOfContentsMatcher{ 18 | expected: expected, 19 | } 20 | } 21 | 22 | type tableOfContentsMatcher struct { 23 | expected *types.TableOfContents 24 | diffs string 25 | } 26 | 27 | func (m *tableOfContentsMatcher) Match(actual interface{}) (success bool, err error) { 28 | if _, ok := actual.(*types.TableOfContents); !ok { 29 | return false, errors.Errorf("MatchDocumentFragment matcher expects a '*types.TableOfContents' (actual: %T)", actual) 30 | } 31 | if diff := cmp.Diff(m.expected, actual, opts...); diff != "" { 32 | if log.IsLevelEnabled(log.DebugLevel) { 33 | log.Debugf("actual table of contents:\n%s", spew.Sdump(actual)) 34 | log.Debugf("expected table of contents:\n%s", spew.Sdump(m.expected)) 35 | } 36 | m.diffs = diff 37 | return false, nil 38 | } 39 | return true, nil 40 | } 41 | 42 | func (m *tableOfContentsMatcher) FailureMessage(_ interface{}) (message string) { 43 | return fmt.Sprintf("expected table of contents to match:\n%s", m.diffs) 44 | } 45 | 46 | func (m *tableOfContentsMatcher) NegatedFailureMessage(_ interface{}) (message string) { 47 | return fmt.Sprintf("expected table of contents not to match:\n%s", m.diffs) 48 | } 49 | -------------------------------------------------------------------------------- /testsupport/table_of_contents_matcher_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/bytesparadise/libasciidoc/pkg/types" 7 | "github.com/bytesparadise/libasciidoc/testsupport" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/google/go-cmp/cmp/cmpopts" 10 | 11 | . "github.com/onsi/ginkgo/v2" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | var _ = Describe("document fragments matcher", func() { 16 | 17 | // given 18 | expected := &types.TableOfContents{ 19 | Sections: []*types.ToCSection{ 20 | { 21 | Title: "root", 22 | }, 23 | }, 24 | } 25 | matcher := testsupport.MatchTableOfContents(expected) 26 | 27 | It("should match", func() { 28 | // given 29 | actual := &types.TableOfContents{ 30 | Sections: []*types.ToCSection{ 31 | { 32 | Title: "root", 33 | }, 34 | }, 35 | } 36 | // when 37 | result, err := matcher.Match(actual) 38 | // then 39 | Expect(err).ToNot(HaveOccurred()) 40 | Expect(result).To(BeTrue()) 41 | }) 42 | 43 | It("should not match", func() { 44 | // given 45 | actual := &types.TableOfContents{ 46 | Sections: []*types.ToCSection{ 47 | { 48 | Title: "something else", 49 | }, 50 | }, 51 | } 52 | // when 53 | result, err := matcher.Match(actual) 54 | // then 55 | Expect(err).ToNot(HaveOccurred()) 56 | Expect(result).To(BeFalse()) 57 | diffs := cmp.Diff(expected, actual, cmpopts.IgnoreUnexported(types.TableOfContents{})) 58 | Expect(matcher.FailureMessage(actual)).To(Equal(fmt.Sprintf("expected table of contents to match:\n%s", diffs))) 59 | Expect(matcher.NegatedFailureMessage(actual)).To(Equal(fmt.Sprintf("expected table of contents not to match:\n%s", diffs))) 60 | }) 61 | 62 | It("should return error when invalid type is input", func() { 63 | // when 64 | result, err := matcher.Match(1) 65 | // then 66 | Expect(err).To(HaveOccurred()) 67 | Expect(err.Error()).To(Equal("MatchDocumentFragment matcher expects a '*types.TableOfContents' (actual: int)")) 68 | Expect(result).To(BeFalse()) 69 | }) 70 | 71 | }) 72 | -------------------------------------------------------------------------------- /testsupport/testsupport_suite_test.go: -------------------------------------------------------------------------------- 1 | package testsupport_test 2 | 3 | import ( 4 | "testing" 5 | 6 | _ "github.com/bytesparadise/libasciidoc/testsupport" 7 | 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestTestsupport(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Testsupport Suite") 15 | } 16 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //+build tools 2 | 3 | package libasciidoc 4 | 5 | import ( 6 | _ "github.com/mna/pigeon" 7 | _ "github.com/onsi/ginkgo/v2/ginkgo" 8 | ) 9 | --------------------------------------------------------------------------------