├── src
├── markd
│ ├── version.cr
│ ├── parser.cr
│ ├── rules
│ │ ├── document.cr
│ │ ├── paragraph.cr
│ │ ├── thematic_break.cr
│ │ ├── item.cr
│ │ ├── block_quote.cr
│ │ ├── html_block.cr
│ │ ├── heading.cr
│ │ ├── code_block.cr
│ │ ├── table.cr
│ │ └── list.cr
│ ├── utils.cr
│ ├── mappings
│ │ ├── decode.cr
│ │ └── legacy.cr
│ ├── html_entities.cr
│ ├── options.cr
│ ├── node.cr
│ ├── renderer.cr
│ ├── rule.cr
│ ├── parsers
│ │ ├── block.cr
│ │ └── inline.cr
│ └── renderers
│ │ └── html_renderer.cr
└── markd.cr
├── .github
├── trafico.yml
└── workflows
│ ├── release.yml
│ └── ci.yml
├── shard.yml
├── .gitignore
├── .vscode
└── launch.json
├── .ameba.yml
├── spec
├── fixtures
│ ├── emoji.txt
│ ├── alert.txt
│ ├── regression.txt
│ ├── smart_punct.txt
│ ├── gfm-regression.txt
│ └── gfm-extensions.txt
├── markd_spec.cr
├── api_spec.cr
└── spec_helper.cr
├── LICENSE
├── CHANGELOG.md
└── README.md
/src/markd/version.cr:
--------------------------------------------------------------------------------
1 | module Markd
2 | VERSION = "0.5.0"
3 | end
4 |
--------------------------------------------------------------------------------
/.github/trafico.yml:
--------------------------------------------------------------------------------
1 | addWipLabel: true
2 | reviewers:
3 | icyleaf:
4 | name: "icyleaf"
5 | color: "#000000"
6 |
--------------------------------------------------------------------------------
/shard.yml:
--------------------------------------------------------------------------------
1 | name: markd
2 | version: 0.5.0
3 |
4 | authors:
5 | - icyleaf
6 |
7 | crystal: ">= 0.36.1, < 2.0.0"
8 |
9 | license: MIT
10 |
--------------------------------------------------------------------------------
/src/markd/parser.cr:
--------------------------------------------------------------------------------
1 | module Markd
2 | module Parser
3 | def self.parse(source : String, options = Options.new)
4 | Block.parse(source, options)
5 | end
6 | end
7 | end
8 |
9 | require "./parsers/*"
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /doc/
2 | /lib/
3 | /bin/
4 | /.shards/
5 | /src/main.cr
6 |
7 | # Libraries don't need dependency lock
8 | # Dependencies will be locked in application that uses them
9 | /shard.lock
10 |
11 | # vscode
12 | /.history/
13 | /.vscode/settings.json
14 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "type": "lldb",
6 | "request": "launch",
7 | "name": "Launch",
8 | "program": "${workspaceRoot}/bin/main",
9 | "args": [],
10 | "cwd": "${workspaceRoot}"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.ameba.yml:
--------------------------------------------------------------------------------
1 | Metrics/CyclomaticComplexity:
2 | Excluded:
3 | - spec/**/*
4 | - src/markd/utils.cr
5 | - src/markd/rules/heading.cr
6 | - src/markd/rules/list.cr
7 | - src/markd/parsers/inline.cr
8 | - src/markd/parsers/block.cr
9 | - src/markd/renderer.cr
10 |
11 | Naming/BlockParameterName:
12 | Enabled: false
13 |
14 | Style/ParenthesesAroundCondition:
15 | Enabled: true
16 | AllowSafeAssignment: true
17 |
18 | Lint/NotNil:
19 | Excluded:
20 | - src/markd/parsers/inline.cr
21 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Deploy new release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*"
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@master
14 |
15 | - name: Create Release
16 | id: create_release
17 | uses: actions/create-release@v1
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
20 | with:
21 | tag_name: ${{ github.ref }}
22 | release_name: Release ${{ github.ref }}
23 | draft: false
24 | prerelease: false
25 |
26 |
--------------------------------------------------------------------------------
/src/markd/rules/document.cr:
--------------------------------------------------------------------------------
1 | module Markd::Rule
2 | struct Document
3 | include Rule
4 |
5 | def match(parser : Parser, container : Node) : MatchValue
6 | MatchValue::None
7 | end
8 |
9 | def continue(parser : Parser, container : Node) : ContinueStatus
10 | ContinueStatus::Continue
11 | end
12 |
13 | def token(parser : Parser, container : Node) : Nil
14 | # do nothing
15 | end
16 |
17 | def can_contain?(type : Node::Type) : Bool
18 | !type.item?
19 | end
20 |
21 | def accepts_lines? : Bool
22 | false
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/src/markd/utils.cr:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | module Markd
4 | module Utils
5 | def self.timer(label : String, measure_time : Bool, &)
6 | return yield unless measure_time
7 |
8 | start_time = Time.utc
9 | yield
10 |
11 | puts "#{label}: #{(Time.utc - start_time).total_milliseconds}ms"
12 | end
13 |
14 | DECODE_ENTITIES_REGEX = Regex.new("\\\\" + Rule::ESCAPABLE_STRING, Regex::Options::IGNORE_CASE)
15 |
16 | def self.decode_entities_string(text : String) : String
17 | HTML.decode_entities(text).gsub(DECODE_ENTITIES_REGEX, &.[1].to_s)
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/src/markd/mappings/decode.cr:
--------------------------------------------------------------------------------
1 | module Markd::HTMLEntities
2 | DECODE_MAPPINGS = {
3 | 0 => 65533,
4 | 128 => 8364,
5 | 130 => 8218,
6 | 131 => 402,
7 | 132 => 8222,
8 | 133 => 8230,
9 | 134 => 8224,
10 | 135 => 8225,
11 | 136 => 710,
12 | 137 => 8240,
13 | 138 => 352,
14 | 139 => 8249,
15 | 140 => 338,
16 | 142 => 381,
17 | 145 => 8216,
18 | 146 => 8217,
19 | 147 => 8220,
20 | 148 => 8221,
21 | 149 => 8226,
22 | 150 => 8211,
23 | 151 => 8212,
24 | 152 => 732,
25 | 153 => 8482,
26 | 154 => 353,
27 | 155 => 8250,
28 | 156 => 339,
29 | 158 => 382,
30 | 159 => 376,
31 | }
32 | end
33 |
--------------------------------------------------------------------------------
/spec/fixtures/emoji.txt:
--------------------------------------------------------------------------------
1 | ## Emoji
2 |
3 | ```````````````````````````````` example emoji
4 | :100:
5 | .
6 | 💯
7 | ````````````````````````````````
8 |
9 | ```````````````````````````````` example emoji
10 | :gb:
11 | .
12 | 🇬🇧
13 | ````````````````````````````````
14 |
15 | ```````````````````````````````` example emoji
16 | :people_holding_hands:
17 | .
18 | 🧑🤝🧑
19 | ````````````````````````````````
20 |
21 | ```````````````````````````````` example emoji
22 | :scotland:
23 | .
24 | 🏴
25 | ````````````````````````````````
26 |
27 | ```````````````````````````````` example emoji
28 | :emoji_doesnt_exist:
29 | .
30 | :emoji_doesnt_exist:
31 | ````````````````````````````````
32 |
33 | ```````````````````````````````` example emoji
34 | :family_man_boy_boy:
35 | .
36 | 👨👦👦
37 | ````````````````````````````````
38 |
--------------------------------------------------------------------------------
/src/markd/rules/paragraph.cr:
--------------------------------------------------------------------------------
1 | module Markd::Rule
2 | struct Paragraph
3 | include Rule
4 |
5 | def match(parser : Parser, container : Node) : MatchValue
6 | MatchValue::None
7 | end
8 |
9 | def continue(parser : Parser, container : Node) : ContinueStatus
10 | parser.blank ? ContinueStatus::Stop : ContinueStatus::Continue
11 | end
12 |
13 | def token(parser : Parser, container : Node) : Nil
14 | has_reference_defs = false
15 |
16 | while container.text[0]? == '[' &&
17 | (pos = parser.inline_lexer.reference(container.text, parser.refmap)) && pos > 0
18 | container.text = container.text.byte_slice(pos)
19 | has_reference_defs = true
20 | end
21 |
22 | container.unlink if has_reference_defs && container.text.each_char.all? &.ascii_whitespace?
23 | end
24 |
25 | def can_contain?(type)
26 | false
27 | end
28 |
29 | def accepts_lines? : Bool
30 | true
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/markd_spec.cr:
--------------------------------------------------------------------------------
1 | require "./spec_helper"
2 |
3 | # Commonmark spec examples
4 | describe_spec("fixtures/spec.txt")
5 |
6 | # Smart punctuation examples
7 | describe_spec("fixtures/smart_punct.txt", smart: true)
8 |
9 | # Regression examples
10 | describe_spec("fixtures/regression.txt")
11 |
12 | describe_spec("fixtures/emoji.txt")
13 |
14 | describe_spec("fixtures/gfm-spec.txt", gfm: true)
15 |
16 | describe_spec("fixtures/gfm-extensions.txt", gfm: true)
17 |
18 | describe_spec("fixtures/gfm-regression.txt", gfm: true)
19 |
20 | # Alert spec examples
21 | describe_spec("fixtures/alert.txt", gfm: true)
22 |
23 | describe Markd do
24 | # Thanks Ryan Westlund feedback via email.
25 | it "should escape unsafe html" do
26 | raw = %Q(```">\n```)
27 | html = %Q(\n)
28 |
29 | Markd.to_html(raw).should eq(html)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/api_spec.cr:
--------------------------------------------------------------------------------
1 | require "spec"
2 | require "../src/markd"
3 |
4 | describe Markd::Options do
5 | describe "#base_url" do
6 | it "it disabled by default" do
7 | options = Markd::Options.new
8 | Markd.to_html("[foo](bar)", options).should eq %(foo
\n)
9 | Markd.to_html("", options).should eq %(
\n)
10 | end
11 |
12 | it "absolutizes relative urls" do
13 | options = Markd::Options.new
14 | options.base_url = URI.parse("http://example.com")
15 | Markd.to_html("[foo](bar)", options).should eq %(foo
\n)
16 | Markd.to_html("[foo](https://example.com/baz)", options).should eq %(foo
\n)
17 | Markd.to_html("", options).should eq %(
\n)
18 | Markd.to_html("", options).should eq %(
\n)
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/src/markd/rules/thematic_break.cr:
--------------------------------------------------------------------------------
1 | module Markd::Rule
2 | struct ThematicBreak
3 | include Rule
4 |
5 | THEMATIC_BREAK = /^(?:(?:\*[ \t]*){3,}|(?:_[ \t]*){3,}|(?:-[ \t]*){3,})[ \t]*$/
6 |
7 | def match(parser : Parser, container : Node) : MatchValue
8 | if !parser.indented && parser.line[parser.next_nonspace..-1].match(THEMATIC_BREAK)
9 | parser.close_unmatched_blocks
10 | parser.add_child(Node::Type::ThematicBreak, parser.next_nonspace)
11 | parser.advance_offset(parser.line.size - parser.offset, false)
12 | MatchValue::Leaf
13 | else
14 | MatchValue::None
15 | end
16 | end
17 |
18 | def continue(parser : Parser, container : Node) : ContinueStatus
19 | # a thematic break can never container > 1 line, so fail to match:
20 | ContinueStatus::Stop
21 | end
22 |
23 | def token(parser : Parser, container : Node) : Nil
24 | # do nothing
25 | end
26 |
27 | def can_contain?(type)
28 | false
29 | end
30 |
31 | def accepts_lines? : Bool
32 | false
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/src/markd/rules/item.cr:
--------------------------------------------------------------------------------
1 | module Markd::Rule
2 | struct Item
3 | include Rule
4 |
5 | def match(parser : Parser, container : Node) : MatchValue
6 | # match and parse in Rule::List
7 | MatchValue::None
8 | end
9 |
10 | def continue(parser : Parser, container : Node) : ContinueStatus
11 | indent_offset = container.data["marker_offset"].as(Int32) + container.data["padding"].as(Int32)
12 |
13 | if parser.blank
14 | if container.first_child?
15 | parser.advance_next_nonspace
16 | else
17 | # Blank line after empty list item
18 | return ContinueStatus::Stop
19 | end
20 | elsif parser.indent >= indent_offset
21 | parser.advance_offset(indent_offset, true)
22 | else
23 | return ContinueStatus::Stop
24 | end
25 |
26 | ContinueStatus::Continue
27 | end
28 |
29 | def token(parser : Parser, container : Node) : Nil
30 | # do nothing
31 | end
32 |
33 | def can_contain?(type : Node::Type)
34 | !type.item?
35 | end
36 |
37 | def accepts_lines? : Bool
38 | false
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-present icyleaf
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/markd.cr:
--------------------------------------------------------------------------------
1 | require "./markd/html_entities"
2 | require "./markd/utils"
3 | require "./markd/node"
4 | require "./markd/rule"
5 | require "./markd/options"
6 | require "./markd/renderer"
7 | require "./markd/parser"
8 | require "./markd/version"
9 |
10 | module Markd
11 | {% if @top_level.has_constant?("Tartrazine") %}
12 | def self.to_html(
13 | source : String,
14 | options = Options.new,
15 | *,
16 | formatter : Tartrazine::Formatter | String = "catppuccin-macchiato",
17 | )
18 | return "" if source.empty?
19 |
20 | if formatter.is_a?(String)
21 | formatter = Tartrazine::Html.new(
22 | theme: Tartrazine.theme(formatter),
23 | line_numbers: true,
24 | standalone: true,
25 | )
26 | end
27 |
28 | document = Parser.parse(source, options)
29 | renderer = HTMLRenderer.new(options)
30 | renderer.render(document, formatter)
31 | end
32 | {% else %}
33 | def self.to_html(
34 | source : String,
35 | options = Options.new,
36 | formatter = nil,
37 | )
38 | return "" if source.empty?
39 |
40 | document = Parser.parse(source, options)
41 | renderer = HTMLRenderer.new(options)
42 | renderer.render(document, formatter)
43 | end
44 | {% end %}
45 | end
46 |
--------------------------------------------------------------------------------
/src/markd/rules/block_quote.cr:
--------------------------------------------------------------------------------
1 | module Markd::Rule
2 | struct BlockQuote
3 | include Rule
4 |
5 | def match(parser : Parser, container : Node) : MatchValue
6 | if match?(parser)
7 | seek(parser)
8 | parser.close_unmatched_blocks
9 | if parser.gfm? && (match = parser.line.match(Rule::ADMONITION_START))
10 | node = parser.add_child(Node::Type::Alert, parser.next_nonspace)
11 | # This is an alert
12 | node.data["alert"] = match[1]
13 | node.data["title"] = (match[2]? && !match[2].strip.empty?) ? match[2].strip : match[1]
14 | parser.advance_offset(parser.line.size, false)
15 | else
16 | parser.add_child(Node::Type::BlockQuote, parser.next_nonspace)
17 | end
18 |
19 | MatchValue::Container
20 | else
21 | MatchValue::None
22 | end
23 | end
24 |
25 | def continue(parser : Parser, container : Node) : ContinueStatus
26 | if match?(parser)
27 | seek(parser)
28 | ContinueStatus::Continue
29 | else
30 | ContinueStatus::Stop
31 | end
32 | end
33 |
34 | def token(parser : Parser, container : Node) : Nil
35 | # do nothing
36 | end
37 |
38 | def can_contain?(type : Node::Type) : Bool
39 | !type.item?
40 | end
41 |
42 | def accepts_lines? : Bool
43 | false
44 | end
45 |
46 | private def match?(parser)
47 | !parser.indented && parser.line[parser.next_nonspace]? == '>'
48 | end
49 |
50 | private def seek(parser : Parser)
51 | parser.advance_next_nonspace
52 | parser.advance_offset(1, false)
53 |
54 | if space_or_tab?(parser.line[parser.offset]?)
55 | parser.advance_offset(1, true)
56 | end
57 | end
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/src/markd/rules/html_block.cr:
--------------------------------------------------------------------------------
1 | module Markd::Rule
2 | struct HTMLBlock
3 | include Rule
4 |
5 | def match(parser : Parser, container : Node) : MatchValue
6 | if !parser.indented && parser.line[parser.next_nonspace]? == '<'
7 | text = parser.line[parser.next_nonspace..-1]
8 | block_type_size = Rule::HTML_BLOCK_OPEN.size - 1
9 |
10 | Rule::HTML_BLOCK_OPEN.each_with_index do |regex, index|
11 | if text.match(regex) &&
12 | (index < block_type_size || !container.type.paragraph?)
13 | parser.close_unmatched_blocks
14 | # We don't adjust parser.offset;
15 | # spaces are part of the HTML block:
16 | node = parser.add_child(Node::Type::HTMLBlock, parser.offset)
17 | node.data["html_block_type"] = index
18 |
19 | return MatchValue::Leaf
20 | end
21 | end
22 | end
23 |
24 | MatchValue::None
25 | end
26 |
27 | def continue(parser : Parser, container : Node) : ContinueStatus
28 | (parser.blank && {5, 6}.includes?(container.data["html_block_type"])) ? ContinueStatus::Stop : ContinueStatus::Continue
29 | end
30 |
31 | def token(parser : Parser, container : Node) : Nil
32 | text = container.text.gsub(/(\n *)+$/, "")
33 |
34 | if parser.tagfilter?
35 | text = self.class.escape_disallowed_html(text)
36 | end
37 |
38 | container.text = text
39 | end
40 |
41 | def can_contain?(type)
42 | false
43 | end
44 |
45 | def accepts_lines? : Bool
46 | true
47 | end
48 |
49 | def self.escape_disallowed_html(text : String) : String
50 | String.build do |string|
51 | pos = 0
52 |
53 | text.scan(/<\/?\s*(#{GFM_DISALLOWED_HTML_TAGS.join('|')})\b/i) do |match|
54 | start = text.index(match[0], pos)
55 | next if start.nil?
56 |
57 | string << text[pos...start] << "<#{match[0][1..]}"
58 | pos = start + match[0].size
59 | end
60 |
61 | string << text[pos..-1]
62 | end
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | ameba_linter:
13 | runs-on: ubuntu-latest
14 | strategy:
15 | matrix:
16 | ameba-version: [v1.6.4]
17 | name: Ameba ${{ matrix.ameba-version }} linter check
18 | steps:
19 | - name: Install latest Crystal
20 | uses: crystal-lang/install-crystal@v1
21 | - name: Check out repository code
22 | uses: actions/checkout@master
23 | - name: Install dependencies
24 | run: shards install --without-development
25 | - name: Cache Ameba binary
26 | id: cache-ameba
27 | uses: actions/cache@v3
28 | with:
29 | path: bin/ameba
30 | key: ${{ runner.os }}-ameba-${{ matrix.ameba-version }}
31 |
32 | - name: Build Ameba
33 | if: steps.cache-ameba.outputs.cache-hit != 'true'
34 | run: |
35 | git clone --branch ${{ matrix.ameba-version }} --single-branch https://github.com/crystal-ameba/ameba.git
36 | cd ameba
37 | make bin/ameba CRFLAGS='-Dpreview_mt --no-debug'
38 | mkdir -p ../bin
39 | mv bin/ameba ../bin/ameba
40 | cd ..
41 | rm -rf ameba
42 |
43 | - name: Run Ameba Linter
44 | run: bin/ameba -c .ameba.yml
45 | specs:
46 | strategy:
47 | fail-fast: false
48 | matrix:
49 | include:
50 | - { os: ubuntu-latest, crystal: latest }
51 | - { os: ubuntu-latest, crystal: nightly }
52 | - { os: macos-latest }
53 | - { os: windows-latest }
54 | runs-on: ${{matrix.os}}
55 |
56 | name: Crystal ${{ matrix.crystal }} specs on ${{ matrix.os }}
57 | steps:
58 | - name: Checkout
59 | uses: actions/checkout@master
60 |
61 | - name: Install Crystal
62 | uses: crystal-lang/install-crystal@v1
63 | with:
64 | crystal: ${{ matrix.crystal }}
65 |
66 | - name: Install dependencies
67 | run: shards install --without-development
68 |
69 | - name: Run specs
70 | run: crystal spec --error-on-warnings --error-trace
71 |
--------------------------------------------------------------------------------
/spec/fixtures/alert.txt:
--------------------------------------------------------------------------------
1 | ## Alert
2 |
3 | Alerts are a Markdown extension based on the blockquote syntax that
4 | you can use to emphasize critical information. On GitHub, they are
5 | displayed with distinctive colors and icons to indicate the significance
6 | of the content.
7 |
8 | Use alerts only when they are crucial for user success and limit them
9 | to one or two per article to prevent overloading the reader. Additionally,
10 | you should avoid placing alerts consecutively. Alerts cannot be nested
11 | within other elements.
12 |
13 | To add an alert, use a special blockquote line specifying the alert type
14 | and an optional title, followed by the alert information in a standard
15 | blockquote.
16 |
17 | There are five types of alert:
18 |
19 | * NOTE
20 | * TIP
21 | * IMPORTANT
22 | * WARNING
23 | * CAUTION
24 |
25 | ```````````````````````````````` example alert
26 | > [!NOTE]
27 | > Useful information that users should know, even when skimming content.
28 | .
29 | NOTE
30 |
Useful information that users should know, even when skimming content.
31 |
32 | ````````````````````````````````
33 |
34 | An optional title can be added after the closing bracket.
35 |
36 | ```````````````````````````````` example alert
37 | > [!NOTE] What is a note?
38 | > Useful information that users should know, even when skimming content.
39 | .
40 | What is a note?
41 |
Useful information that users should know, even when skimming content.
42 |
43 | ````````````````````````````````
44 |
45 | Empty spaces after the brackets are ignored.
46 |
47 | ```````````````````````````````` example alert
48 | > [!NOTE]
49 | > Useful information that users should know, even when skimming content.
50 | .
51 | NOTE
52 |
Useful information that users should know, even when skimming content.
53 |
54 | ````````````````````````````````
55 |
56 | Alert-like block quotes which don't use one of the five listed
57 | alert types are just block quotes.
58 |
59 | ```````````````````````````````` example alert
60 | > [!FOO]
61 | > Not a real alert.
62 | .
63 |
64 | [!FOO]
65 | Not a real alert.
66 |
67 | ````````````````````````````````
68 |
--------------------------------------------------------------------------------
/src/markd/rules/heading.cr:
--------------------------------------------------------------------------------
1 | module Markd::Rule
2 | struct Heading
3 | include Rule
4 |
5 | ATX_HEADING_MARKER = /^\#{1,6}(?:[ \t]+|$)/
6 | SETEXT_HEADING_MARKER = /^(?:=+|-+)[ \t]*$/
7 |
8 | def match(parser : Parser, container : Node) : MatchValue
9 | if (match = match?(parser, ATX_HEADING_MARKER))
10 | # ATX Heading matched
11 | parser.advance_next_nonspace
12 | parser.advance_offset(match[0].size, false)
13 | parser.close_unmatched_blocks
14 |
15 | container = parser.add_child(Node::Type::Heading, parser.next_nonspace)
16 | container.data["level"] = match[0].strip.size
17 | container.text = parser.line[parser.offset..-1]
18 | .sub(/^[ \t]*#+[ \t]*$/, "")
19 | .sub(/[ \t]+#+[ \t]*$/, "")
20 |
21 | parser.advance_offset(parser.line.size - parser.offset)
22 |
23 | MatchValue::Leaf
24 | elsif (match = match?(parser, SETEXT_HEADING_MARKER)) &&
25 | container.type.paragraph? && (parent = container.parent?) &&
26 | !parent.type.block_quote?
27 | # Setext Heading matched
28 | parser.close_unmatched_blocks
29 |
30 | while container.text[0]? == '[' &&
31 | (pos = parser.inline_lexer.reference(container.text, parser.refmap)) && pos > 0
32 | container.text = container.text.byte_slice(pos)
33 | end
34 | return MatchValue::None if container.text.empty?
35 |
36 | heading = Node.new(Node::Type::Heading)
37 | heading.source_pos = container.source_pos
38 | heading.data["level"] = match[0][0] == '=' ? 1 : 2
39 | heading.text = container.text
40 |
41 | container.insert_after(heading)
42 | container.unlink
43 |
44 | parser.tip = heading
45 | parser.advance_offset(parser.line.size - parser.offset, false)
46 |
47 | MatchValue::Leaf
48 | else
49 | MatchValue::None
50 | end
51 | end
52 |
53 | def token(parser : Parser, container : Node) : Nil
54 | # do nothing
55 | end
56 |
57 | def continue(parser : Parser, container : Node) : ContinueStatus
58 | # a heading can never container > 1 line, so fail to match
59 | ContinueStatus::Stop
60 | end
61 |
62 | def can_contain?(type)
63 | false
64 | end
65 |
66 | def accepts_lines? : Bool
67 | false
68 | end
69 |
70 | private def match?(parser : Parser, regex : Regex) : Regex::MatchData?
71 | match = parser.line[parser.next_nonspace..-1].match(regex)
72 | !parser.indented && match ? match : nil
73 | end
74 | end
75 | end
76 |
--------------------------------------------------------------------------------
/src/markd/html_entities.cr:
--------------------------------------------------------------------------------
1 | require "./mappings/*"
2 |
3 | module Markd::HTMLEntities
4 | module ExtendToHTML
5 | def decode_entities(source : String)
6 | Decoder.decode(source)
7 | end
8 |
9 | def decode_entity(source : String)
10 | Decoder.decode_entity(source)
11 | end
12 |
13 | def encode_entities(source)
14 | Encoder.encode(source)
15 | end
16 | end
17 |
18 | module Decoder
19 | REGEX = /&(?:([a-zA-Z0-9]{2,32};)|(#[xX][\da-fA-F]+;?|#\d+;?))/
20 |
21 | def self.decode(source)
22 | source.gsub(REGEX) do |chars|
23 | decode_entity(chars[1..-2])
24 | end
25 | end
26 |
27 | def self.decode_entity(chars)
28 | if chars[0] == '#'
29 | if chars.size > 1
30 | if chars[1].downcase == 'x'
31 | if chars.size > 2
32 | return decode_codepoint(chars[2..-1].to_i(16))
33 | end
34 | else
35 | return decode_codepoint(chars[1..-1].to_i(10))
36 | end
37 | end
38 | else
39 | entities_key = chars[0..-1]
40 | if (resolved_entity = Markd::HTMLEntities::ENTITIES_MAPPINGS[entities_key]?)
41 | return resolved_entity
42 | end
43 | end
44 |
45 | "{chars};"
46 | end
47 |
48 | def self.decode_codepoint(codepoint)
49 | return "\uFFFD" if codepoint >= 0xD800 && codepoint <= 0xDFFF || codepoint > 0x10FFF
50 |
51 | if (decoded = Markd::HTMLEntities::DECODE_MAPPINGS[codepoint]?)
52 | codepoint = decoded
53 | end
54 |
55 | codepoint.unsafe_chr
56 | end
57 | end
58 |
59 | module Encoder
60 | ENTITIES_REGEX = Regex.union(HTMLEntities::ENTITIES_MAPPINGS.values)
61 | ASTRAL_REGEX = Regex.new("[\xED\xA0\x80-\xED\xAF\xBF][\xED\xB0\x80-\xED\xBF\xBF]")
62 | ENCODE_REGEX = /[^\x{20}-\x{7E}]/
63 |
64 | def self.encode(source : String)
65 | source.gsub(ENTITIES_REGEX) { |chars| encode_entities(chars) }
66 | .gsub(ASTRAL_REGEX) { |chars| encode_astral(chars) }
67 | .gsub(ENCODE_REGEX) { |chars| encode_extend(chars) }
68 | end
69 |
70 | private def self.encode_entities(chars : String)
71 | entity = HTMLEntities::ENTITIES_MAPPINGS.key(chars)
72 | "{entity};"
73 | end
74 |
75 | private def self.encode_astral(chars : String)
76 | high = chars.char_at(0).ord
77 | low = chars.char_at(0).ord
78 | codepoint = (high - 0xD800) * -0x400 + low - 0xDC00 + 0x10000
79 |
80 | "#{codepoint.to_s(16).upcase};"
81 | end
82 |
83 | private def self.encode_extend(char : String)
84 | "#{char[0].ord.to_s(16).upcase};"
85 | end
86 | end
87 | end
88 |
89 | module HTML
90 | extend Markd::HTMLEntities::ExtendToHTML
91 | end
92 |
--------------------------------------------------------------------------------
/src/markd/rules/code_block.cr:
--------------------------------------------------------------------------------
1 | module Markd::Rule
2 | struct CodeBlock
3 | include Rule
4 |
5 | CODE_FENCE = /^`{3,}(?!.*`)|^~{3,}/
6 | CLOSING_CODE_FENCE = /^(?:`{3,}|~{3,})(?= *$)/
7 |
8 | def match(parser : Parser, container : Node) : MatchValue
9 | if !parser.indented &&
10 | (match = parser.line[parser.next_nonspace..-1].match(CODE_FENCE))
11 | # fenced
12 | fence_length = match[0].size
13 |
14 | parser.close_unmatched_blocks
15 | node = parser.add_child(Node::Type::CodeBlock, parser.next_nonspace)
16 | node.fenced = true
17 | node.fence_length = fence_length
18 | node.fence_char = match[0][0].to_s
19 | node.fence_offset = parser.indent
20 |
21 | parser.advance_next_nonspace
22 | parser.advance_offset(fence_length, false)
23 |
24 | MatchValue::Leaf
25 | elsif parser.indented && !parser.blank && (tip = parser.tip) &&
26 | !tip.type.paragraph?
27 | # indented
28 | parser.advance_offset(Rule::CODE_INDENT, true)
29 | parser.close_unmatched_blocks
30 | parser.add_child(Node::Type::CodeBlock, parser.offset)
31 |
32 | MatchValue::Leaf
33 | else
34 | MatchValue::None
35 | end
36 | end
37 |
38 | def continue(parser : Parser, container : Node) : ContinueStatus
39 | line = parser.line
40 | indent = parser.indent
41 | if container.fenced?
42 | # fenced
43 | match = indent <= 3 &&
44 | line[parser.next_nonspace]? == container.fence_char[0] &&
45 | line[parser.next_nonspace..-1].match(CLOSING_CODE_FENCE)
46 |
47 | if match && match.as(Regex::MatchData)[0].size >= container.fence_length
48 | # closing fence - we're at end of line, so we can return
49 | parser.token(container, parser.current_line)
50 | return ContinueStatus::Return
51 | else
52 | # skip optional spaces of fence offset
53 | index = container.fence_offset
54 | while index > 0 && space_or_tab?(parser.line[parser.offset]?)
55 | parser.advance_offset(1, true)
56 | index -= 1
57 | end
58 | end
59 | else
60 | # indented
61 | if indent >= Rule::CODE_INDENT
62 | parser.advance_offset(Rule::CODE_INDENT, true)
63 | elsif parser.blank
64 | parser.advance_next_nonspace
65 | else
66 | return ContinueStatus::Stop
67 | end
68 | end
69 |
70 | ContinueStatus::Continue
71 | end
72 |
73 | def token(parser : Parser, container : Node) : Nil
74 | if container.fenced?
75 | # fenced
76 | first_line, _, text = container.text.partition('\n')
77 |
78 | container.fence_language = Utils.decode_entities_string(first_line.strip)
79 | container.text = text
80 | else
81 | # indented
82 | container.text = container.text.gsub(/(\n *)+$/, "\n")
83 | end
84 | end
85 |
86 | def can_contain?(type)
87 | false
88 | end
89 |
90 | def accepts_lines? : Bool
91 | true
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/src/markd/mappings/legacy.cr:
--------------------------------------------------------------------------------
1 | module Markd::HTMLEntities
2 | LEGACY_MAPPINGS = {
3 | "Aacute" => '\u00C1',
4 | "aacute" => '\u00E1',
5 | "Acirc" => '\u00C2',
6 | "acirc" => '\u00E2',
7 | "acute" => '\u00B4',
8 | "AElig" => '\u00C6',
9 | "aelig" => '\u00E6',
10 | "Agrave" => '\u00C0',
11 | "agrave" => '\u00E0',
12 | "amp" => '&',
13 | "AMP" => '&',
14 | "Aring" => '\u00C5',
15 | "aring" => '\u00E5',
16 | "Atilde" => '\u00C3',
17 | "atilde" => '\u00E3',
18 | "Auml" => '\u00C4',
19 | "auml" => '\u00E4',
20 | "brvbar" => '\u00A6',
21 | "Ccedil" => '\u00C7',
22 | "ccedil" => '\u00E7',
23 | "cedil" => '\u00B8',
24 | "cent" => '\u00A2',
25 | "copy" => '\u00A9',
26 | "COPY" => '\u00A9',
27 | "curren" => '\u00A4',
28 | "deg" => '\u00B0',
29 | "divide" => '\u00F7',
30 | "Eacute" => '\u00C9',
31 | "eacute" => '\u00E9',
32 | "Ecirc" => '\u00CA',
33 | "ecirc" => '\u00EA',
34 | "Egrave" => '\u00C8',
35 | "egrave" => '\u00E8',
36 | "ETH" => '\u00D0',
37 | "eth" => '\u00F0',
38 | "Euml" => '\u00CB',
39 | "euml" => '\u00EB',
40 | "frac12" => '\u00BD',
41 | "frac14" => '\u00BC',
42 | "frac34" => '\u00BE',
43 | "gt" => '>',
44 | "GT" => '>',
45 | "Iacute" => '\u00CD',
46 | "iacute" => '\u00ED',
47 | "Icirc" => '\u00CE',
48 | "icirc" => '\u00EE',
49 | "iexcl" => '\u00A1',
50 | "Igrave" => '\u00CC',
51 | "igrave" => '\u00EC',
52 | "iquest" => '\u00BF',
53 | "Iuml" => '\u00CF',
54 | "iuml" => '\u00EF',
55 | "laquo" => '\u00AB',
56 | "lt" => '<',
57 | "LT" => '<',
58 | "macr" => '\u00AF',
59 | "micro" => '\u00B5',
60 | "middot" => '\u00B7',
61 | "nbsp" => '\u00A0',
62 | "not" => '\u00AC',
63 | "Ntilde" => '\u00D1',
64 | "ntilde" => '\u00F1',
65 | "Oacute" => '\u00D3',
66 | "oacute" => '\u00F3',
67 | "Ocirc" => '\u00D4',
68 | "ocirc" => '\u00F4',
69 | "Ograve" => '\u00D2',
70 | "ograve" => '\u00F2',
71 | "ordf" => '\u00AA',
72 | "ordm" => '\u00BA',
73 | "Oslash" => '\u00D8',
74 | "oslash" => '\u00F8',
75 | "Otilde" => '\u00D5',
76 | "otilde" => '\u00F5',
77 | "Ouml" => '\u00D6',
78 | "ouml" => '\u00F6',
79 | "para" => '\u00B6',
80 | "plusmn" => '\u00B1',
81 | "pound" => '\u00A3',
82 | "quot" => "\"",
83 | "QUOT" => "\"",
84 | "raquo" => '\u00BB',
85 | "reg" => '\u00AE',
86 | "REG" => '\u00AE',
87 | "sect" => '\u00A7',
88 | "shy" => '\u00AD',
89 | "sup1" => '\u00B9',
90 | "sup2" => '\u00B2',
91 | "sup3" => '\u00B3',
92 | "szlig" => '\u00DF',
93 | "THORN" => '\u00DE',
94 | "thorn" => '\u00FE',
95 | "times" => '\u00D7',
96 | "Uacute" => '\u00DA',
97 | "uacute" => '\u00FA',
98 | "Ucirc" => '\u00DB',
99 | "ucirc" => '\u00FB',
100 | "Ugrave" => '\u00D9',
101 | "ugrave" => '\u00F9',
102 | "uml" => '\u00A8',
103 | "Uuml" => '\u00DC',
104 | "uuml" => '\u00FC',
105 | "Yacute" => '\u00DD',
106 | "yacute" => '\u00FD',
107 | "yen" => '\u00A5',
108 | "yuml" => '\u00FF',
109 | }
110 | end
111 |
--------------------------------------------------------------------------------
/src/markd/options.cr:
--------------------------------------------------------------------------------
1 | require "uri"
2 |
3 | module Markd
4 | # Markdown rendering options.
5 | class Options
6 | # Render parsing cost time for reading the source, parsing blocks, and parsing inline.
7 | property? time : Bool
8 |
9 | # Enables GitHub Flavored Markdown support.
10 | #
11 | # https://github.github.com/gfm/
12 | property? gfm : Bool
13 |
14 | # Not supported for now.
15 | property? toc : Bool
16 |
17 | # If `true`:
18 | # - straight quotes will be made curly
19 | # - `--` will be changed to an en dash
20 | # - `---` will be changed to an em dash
21 | # - `...` will be changed to ellipses
22 | property? smart : Bool
23 |
24 | # If `true`, source position information for block-level elements
25 | # will be rendered in the `data-sourcepos` attribute (for HTML).
26 | property? source_pos : Bool
27 |
28 | # If `true`, raw HTML will not be passed through to HTML output
29 | # (it will be replaced by comments).
30 | property? safe : Bool
31 |
32 | # If `true`, code tags generated by code blocks will have a
33 | # prettyprint class added to them, to be used by
34 | # [Google code-prettify](https://github.com/google/code-prettify).
35 | property? prettyprint : Bool
36 |
37 | # If `base_url` is not `nil`, it is used to resolve URLs of relative
38 | # links. It act's like HTML's `` in the context
39 | # of a Markdown document.
40 | property base_url : URI?
41 |
42 | # Enables GFM emoji support.
43 | #
44 | # For example:
45 | #
46 | # ```
47 | # @octocat :+1: This PR looks great - it's ready to merge! :ship:
48 | # ```
49 | #
50 | # Becomes:
51 | #
52 | # ```
53 | # @octocat 👍 This PR looks great - it's ready to merge! 🚢
54 | # ```
55 | # https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#using-emojis
56 | property? emoji : Bool
57 |
58 | # If `true`, the following HTML tags will be filtered when rendering HTML output:
59 | #
60 | # * ``
61 | # * `
228 | [a](<<b)
229 | [a](<b
230 | )
231 | ````````````````````````````````
232 |
233 | Issue commonmark#526 - unescaped ( in link title
234 |
235 | ```````````````````````````````` example pending
236 | [link](url ((title))
237 | .
238 | [link](url ((title))
239 | ````````````````````````````````
240 |
241 | Issue commonamrk#517 - script, pre, style close tag without
242 | opener.
243 |
244 | ```````````````````````````````` example
245 |
246 |
247 |
248 |
249 |
250 | .
251 |
252 |
253 |
254 | ````````````````````````````````
255 |
256 | Issue #289.
257 |
258 | ```````````````````````````````` example
259 | [a](
260 | .
261 | [a](<b) c>
262 | ````````````````````````````````
263 |
264 | Pull request #128 - Buffer overread in tables extension
265 |
266 | ```````````````````````````````` example table
267 | |
268 | -|
269 | .
270 | |
271 | -|
272 | ````````````````````````````````
273 |
274 | Footnotes may be nested inside other footnotes.
275 |
276 | ```````````````````````````````` example footnotes pending
277 | This is some text. It has a citation.[^citation]
278 |
279 | [^another-citation]: My second citation.
280 |
281 | [^citation]: This is a long winded parapgraph that also has another citation.[^another-citation]
282 | .
283 | This is some text. It has a citation.
284 |
294 | ````````````````````````````````
295 |
296 | Footnotes are similar to, but should not be confused with, link references
297 |
298 | ```````````````````````````````` example footnotes pending
299 | This is some text. It has two footnotes references, side-by-side without any spaces,[^footnote1][^footnote2] which are definitely not link references.
300 |
301 | [^footnote1]: Hello.
302 |
303 | [^footnote2]: Goodbye.
304 | .
305 | This is some text. It has two footnotes references, side-by-side without any spaces, which are definitely not link references.
306 |
316 | ````````````````````````````````
317 |
318 | Footnotes may begin with or have a 'w' or a '_' in their reference label.
319 |
320 | ```````````````````````````````` example footnotes autolink pending
321 | This is some text. Sometimes the autolinker splits up text into multiple nodes, hoping it will find a hyperlink, so this text has a footnote whose reference label begins with a `w`.[^widely-cited]
322 |
323 | It has another footnote that contains many different characters (the autolinker was also breaking on `_`).[^sphinx-of-black-quartz_judge-my-vow-0123456789]
324 |
325 | [^sphinx-of-black-quartz_judge-my-vow-0123456789]: so does this.
326 |
327 | [^widely-cited]: this renders properly.
328 | .
329 | This is some text. Sometimes the autolinker splits up text into multiple nodes, hoping it will find a hyperlink, so this text has a footnote whose reference label begins with a w.
330 | It has another footnote that contains many different characters (the autolinker was also breaking on _).
331 |
341 | ````````````````````````````````
342 |
343 | Footnotes interacting with strikethrough should not lead to a use-after-free
344 |
345 | ```````````````````````````````` example footnotes autolink strikethrough table pending
346 | |Tot.....[^_a_]|
347 | .
348 | |Tot.....[^_a_]|
349 | ````````````````````````````````
350 |
351 | Footnotes interacting with strikethrough should not lead to a use-after-free pt2
352 |
353 | ```````````````````````````````` example footnotes autolink strikethrough table pending
354 | [^~~is~~1]
355 | .
356 | [^~~is~~1]
357 | ````````````````````````````````
358 |
359 | Adjacent unused footnotes definitions should not lead to a use after free
360 |
361 | ```````````````````````````````` example footnotes autolink strikethrough table
362 | Hello world
363 |
364 |
365 | [^a]:[^b]:
366 | .
367 | Hello world
368 | ````````````````````````````````
369 |
370 | Issue #424 - emphasis before links
371 |
372 | ```````````````````````````````` example
373 | *text* [link](#section)
374 | .
375 | text link
376 | ````````````````````````````````
377 |
--------------------------------------------------------------------------------
/spec/fixtures/gfm-extensions.txt:
--------------------------------------------------------------------------------
1 | ---
2 | title: Extensions test
3 | author: Yuki Izumi
4 | version: 0.1
5 | date: '2016-08-31'
6 | license: '[CC-BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/)'
7 | ...
8 |
9 | ## Tables
10 |
11 | Here's a well-formed table, doing everything it should.
12 |
13 | ```````````````````````````````` example
14 | | abc | def |
15 | | --- | --- |
16 | | ghi | jkl |
17 | | mno | pqr |
18 | .
19 |
20 |
21 |
22 | | abc |
23 | def |
24 |
25 |
26 |
27 |
28 | | ghi |
29 | jkl |
30 |
31 |
32 | | mno |
33 | pqr |
34 |
35 |
36 |
37 | ````````````````````````````````
38 |
39 | We're going to mix up the table now; we'll demonstrate that inline formatting
40 | works fine, but block elements don't. You can also have empty cells, and the
41 | textual alignment of the columns is shown to be irrelevant.
42 |
43 | ```````````````````````````````` example
44 | Hello!
45 |
46 | | _abc_ | セン |
47 | | ----- | ---- |
48 | | 1. Block elements inside cells don't work. | |
49 | | But _**inline elements do**_. | x |
50 |
51 | Hi!
52 | .
53 | Hello!
54 |
55 |
56 |
57 | | abc |
58 | セン |
59 |
60 |
61 |
62 |
63 | | 1. Block elements inside cells don't work. |
64 | |
65 |
66 |
67 | | But inline elements do. |
68 | x |
69 |
70 |
71 |
72 | Hi!
73 | ````````````````````````````````
74 |
75 | Here we demonstrate some edge cases about what is and isn't a table.
76 |
77 | ```````````````````````````````` example
78 | | Not enough table | to be considered table |
79 |
80 | | Not enough table | to be considered table |
81 | | Not enough table | to be considered table |
82 |
83 | | Just enough table | to be considered table |
84 | | ----------------- | ---------------------- |
85 |
86 | | ---- | --- |
87 |
88 | |x|
89 | |-|
90 |
91 | | xyz |
92 | | --- |
93 | .
94 | | Not enough table | to be considered table |
95 | | Not enough table | to be considered table |
96 | | Not enough table | to be considered table |
97 |
98 |
99 |
100 | | Just enough table |
101 | to be considered table |
102 |
103 |
104 |
105 | | ---- | --- |
106 |
107 |
108 |
109 | | x |
110 |
111 |
112 |
113 |
114 |
115 |
116 | | xyz |
117 |
118 |
119 |
120 | ````````````````````````````````
121 |
122 | A "simpler" table, GFM style:
123 |
124 | ```````````````````````````````` example
125 | abc | def
126 | --- | ---
127 | xyz | ghi
128 | .
129 |
130 |
131 |
132 | | abc |
133 | def |
134 |
135 |
136 |
137 |
138 | | xyz |
139 | ghi |
140 |
141 |
142 |
143 | ````````````````````````````````
144 |
145 | We are making the parser slighly more lax here. Here is a table with spaces at
146 | the end:
147 |
148 | ```````````````````````````````` example
149 | Hello!
150 |
151 | | _abc_ | セン |
152 | | ----- | ---- |
153 | | this row has a space at the end | |
154 | | But _**inline elements do**_. | x |
155 |
156 | Hi!
157 | .
158 | Hello!
159 |
160 |
161 |
162 | | abc |
163 | セン |
164 |
165 |
166 |
167 |
168 | | this row has a space at the end |
169 | |
170 |
171 |
172 | | But inline elements do. |
173 | x |
174 |
175 |
176 |
177 | Hi!
178 | ````````````````````````````````
179 |
180 | Table alignment:
181 |
182 | ```````````````````````````````` example
183 | aaa | bbb | ccc | ddd | eee
184 | :-- | --- | :-: | --- | --:
185 | fff | ggg | hhh | iii | jjj
186 | .
187 |
188 |
189 |
190 | | aaa |
191 | bbb |
192 | ccc |
193 | ddd |
194 | eee |
195 |
196 |
197 |
198 |
199 | | fff |
200 | ggg |
201 | hhh |
202 | iii |
203 | jjj |
204 |
205 |
206 |
207 | ````````````````````````````````
208 |
209 | ### Table cell count mismatches
210 |
211 | The header and delimiter row must match.
212 |
213 | ```````````````````````````````` example
214 | | a | b | c |
215 | | --- | --- |
216 | | this | isn't | okay |
217 | .
218 | | a | b | c |
219 | | --- | --- |
220 | | this | isn't | okay |
221 | ````````````````````````````````
222 |
223 | But any of the body rows can be shorter. Rows longer
224 | than the header are truncated.
225 |
226 | ```````````````````````````````` example
227 | | a | b | c |
228 | | --- | --- | ---
229 | | x
230 | | a | b
231 | | 1 | 2 | 3 | 4 | 5 |
232 | .
233 |
234 |
235 |
236 | | a |
237 | b |
238 | c |
239 |
240 |
241 |
242 |
243 | | x |
244 | |
245 | |
246 |
247 |
248 | | a |
249 | b |
250 | |
251 |
252 |
253 | | 1 |
254 | 2 |
255 | 3 |
256 |
257 |
258 |
259 | ````````````````````````````````
260 |
261 | ### Embedded pipes
262 |
263 | Tables with embedded pipes could be tricky.
264 |
265 | ```````````````````````````````` example
266 | | a | b |
267 | | --- | --- |
268 | | Escaped pipes are \|okay\|. | Like \| this. |
269 | | Within `\|code\| is okay` too. |
270 | | _**`c\|`**_ \| complex
271 | | don't **\_reparse\_**
272 | .
273 |
274 |
275 |
276 | | a |
277 | b |
278 |
279 |
280 |
281 |
282 | | Escaped pipes are |okay|. |
283 | Like | this. |
284 |
285 |
286 | Within |code| is okay too. |
287 | |
288 |
289 |
290 | c| | complex |
291 | |
292 |
293 |
294 | | don't _reparse_ |
295 | |
296 |
297 |
298 |
299 | ````````````````````````````````
300 |
301 | ### Oddly-formatted markers
302 |
303 | This shouldn't assert.
304 |
305 | ```````````````````````````````` example
306 | | a |
307 | --- |
308 | .
309 |
310 |
311 |
312 | | a |
313 |
314 |
315 |
316 | ````````````````````````````````
317 |
318 | ### Escaping
319 |
320 | ```````````````````````````````` example
321 | | a | b |
322 | | --- | --- |
323 | | \\ | `\\` |
324 | | \\\\ | `\\\\` |
325 | | \_ | `\_` |
326 | | \| | `\|` |
327 | | \a | `\a` |
328 |
329 | \\ `\\`
330 |
331 | \\\\ `\\\\`
332 |
333 | \_ `\_`
334 |
335 | \| `\|`
336 |
337 | \a `\a`
338 | .
339 |
340 |
341 |
342 | | a |
343 | b |
344 |
345 |
346 |
347 |
348 | | \ |
349 | \\ |
350 |
351 |
352 | | \\ |
353 | \\\\ |
354 |
355 |
356 | | _ |
357 | \_ |
358 |
359 |
360 | | | |
361 | | |
362 |
363 |
364 | | \a |
365 | \a |
366 |
367 |
368 |
369 | \ \\
370 | \\ \\\\
371 | _ \_
372 | | \|
373 | \a \a
374 | ````````````````````````````````
375 |
376 | ### Embedded HTML
377 |
378 | ```````````````````````````````` example
379 | | a |
380 | | --- |
381 | | hello |
382 | | ok
sure |
383 | .
384 |
385 |
386 |
387 | | a |
388 |
389 |
390 |
391 |
392 | | hello |
393 |
394 |
395 | ok sure |
396 |
397 |
398 |
399 | ````````````````````````````````
400 |
401 | ### Reference-style links
402 |
403 | ```````````````````````````````` example
404 | Here's a link to [Freedom Planet 2][].
405 |
406 | | Here's a link to [Freedom Planet 2][] in a table header. |
407 | | --- |
408 | | Here's a link to [Freedom Planet 2][] in a table row. |
409 |
410 | [Freedom Planet 2]: http://www.freedomplanet2.com/
411 | .
412 | Here's a link to Freedom Planet 2.
413 |
414 |
415 |
416 | | Here's a link to Freedom Planet 2 in a table header. |
417 |
418 |
419 |
420 |
421 | | Here's a link to Freedom Planet 2 in a table row. |
422 |
423 |
424 |
425 | ````````````````````````````````
426 |
427 | ### Sequential cells
428 |
429 | ```````````````````````````````` example
430 | | a | b | c |
431 | | --- | --- | --- |
432 | | d || e |
433 | .
434 |
435 |
436 |
437 | | a |
438 | b |
439 | c |
440 |
441 |
442 |
443 |
444 | | d |
445 | |
446 | e |
447 |
448 |
449 |
450 | ````````````````````````````````
451 |
452 | ### Interaction with emphasis
453 |
454 | ```````````````````````````````` example
455 | | a | b |
456 | | --- | --- |
457 | |***(a)***|
458 | .
459 |
460 |
461 |
462 | | a |
463 | b |
464 |
465 |
466 |
467 |
468 | | (a) |
469 | |
470 |
471 |
472 |
473 | ````````````````````````````````
474 |
475 | ### a table can be recognised when separated from a paragraph of text without an empty line
476 |
477 | ```````````````````````````````` example
478 | 123
479 | 456
480 | | a | b |
481 | | ---| --- |
482 | d | e
483 | .
484 | 123
485 | 456
486 |
487 |
488 |
489 | | a |
490 | b |
491 |
492 |
493 |
494 |
495 | | d |
496 | e |
497 |
498 |
499 |
500 | ````````````````````````````````
501 |
502 | ## Strikethroughs
503 |
504 | A well-formed strikethrough.
505 |
506 | ```````````````````````````````` example
507 | A proper ~strikethrough~.
508 | .
509 | A proper strikethrough.
510 | ````````````````````````````````
511 |
512 | Some strikethrough edge cases.
513 |
514 | ```````````````````````````````` example
515 | These are ~not strikethroughs.
516 |
517 | No, they are not~
518 |
519 | This ~is ~ legit~ isn't ~ legit.
520 |
521 | This is not ~~~~~one~~~~~ huge strikethrough.
522 |
523 | ~one~ ~~two~~ ~~~three~~~
524 |
525 | No ~mismatch~~
526 | .
527 | These are ~not strikethroughs.
528 | No, they are not~
529 | This is ~ legit isn't ~ legit.
530 | This is not ~~~~~one~~~~~ huge strikethrough.
531 | one two ~~~three~~~
532 | No ~mismatch~~
533 | ````````````````````````````````
534 |
535 | Using 200 tilde since it overflows the internal buffer
536 | size (100) for parsing delimiters in inlines.c
537 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~striked~
538 |
539 | ## Autolinks
540 |
541 | ```````````````````````````````` example autolink
542 | : http://google.com https://google.com
543 |
544 | http://google.com/å
545 |
546 | scyther@pokemon.com
547 |
548 | scy.the_rbe-edr+ill@pokemon.com
549 |
550 | scyther@pokemon.com.
551 |
552 | scyther@pokemon.com/
553 |
554 | scyther@pokemon.com/beedrill@pokemon.com
555 |
556 | mailto:scyther@pokemon.com
557 |
558 | This is a mailto:scyther@pokemon.com
559 |
560 | mailto:scyther@pokemon.com.
561 |
562 | mailto:scyther@pokemon.com/
563 |
564 | mailto:scyther@pokemon.com/message
565 |
566 | mailto:scyther@pokemon.com/mailto:beedrill@pokemon.com
567 |
568 | xmpp:scyther@pokemon.com
569 |
570 | xmpp:scyther@pokemon.com.
571 |
572 | xmpp:scyther@pokemon.com/message
573 |
574 | xmpp:scyther@pokemon.com/message.
575 |
576 | Email me at:scyther@pokemon.com
577 |
578 | www.github.com www.github.com/á
579 |
580 | www.google.com/a_b
581 |
582 | Underscores not allowed in host name www.xxx.yyy._zzz
583 |
584 | Underscores not allowed in host name www.xxx._yyy.zzz
585 |
586 | Underscores allowed in domain name www._xxx.yyy.zzz
587 |
588 | **Autolink and http://inlines.com**
589 |
590 | 
591 |
592 | a.w@b.c
593 |
594 | Full stop outside parens shouldn't be included http://google.com/ok.
595 |
596 | (Full stop inside parens shouldn't be included http://google.com/ok.)
597 |
598 | "http://google.com"
599 |
600 | 'http://google.com'
601 |
602 | http://🍄.ga/ http://x🍄.ga/
603 | .
604 | : http://google.com https://google.com
605 | http://google.com/å http://google.com/å
606 | scyther@pokemon.com
607 | scy.the_rbe-edr+ill@pokemon.com
608 | scyther@pokemon.com.
609 | scyther@pokemon.com/
610 | scyther@pokemon.com/beedrill@pokemon.com
611 | mailto:scyther@pokemon.com
612 | This is a mailto:scyther@pokemon.com
613 | mailto:scyther@pokemon.com.
614 | mailto:scyther@pokemon.com/
615 | mailto:scyther@pokemon.com/message
616 | mailto:scyther@pokemon.com/mailto:beedrill@pokemon.com
617 | xmpp:scyther@pokemon.com
618 | xmpp:scyther@pokemon.com.
619 | xmpp:scyther@pokemon.com/message
620 | xmpp:scyther@pokemon.com/message.
621 | Email me at:scyther@pokemon.com
622 | www.github.com www.github.com/á
623 | www.google.com/a_b
624 | Underscores not allowed in host name www.xxx.yyy._zzz
625 | Underscores not allowed in host name www.xxx._yyy.zzz
626 | Underscores allowed in domain name www._xxx.yyy.zzz
627 | Autolink and http://inlines.com
628 | 
629 | a.w@b.c
630 | Full stop outside parens shouldn't be included http://google.com/ok.
631 | (Full stop inside parens shouldn't be included http://google.com/ok.)
632 | "http://google.com"
633 | 'http://google.com'
634 | http://🍄.ga/ http://x🍄.ga/
635 | ````````````````````````````````
636 |
637 | ```````````````````````````````` example pending
638 | mmmmailto:scyther@pokemon.com
639 | .
640 | mmmmailto:scyther@pokemon.com
641 | ````````````````````````````````
642 |
643 | ```````````````````````````````` example
644 | This shouldn't crash everything: (_A_@_.A
645 | .
646 |
647 | ````````````````````````````````
648 |
649 | ```````````````````````````````` example
650 | These should not link:
651 |
652 | * @a.b.c@. x
653 | * n@. b
654 | .
655 | These should not link:
656 |
657 | - @a.b.c@. x
658 | - n@. b
659 |
660 | ````````````````````````````````
661 |
662 | ## HTML tag filter
663 |
664 |
665 | ```````````````````````````````` example tagfilter
666 | This is not okay, but **this** is.
667 |
668 | This is
not okay, but **this** is.
669 |
670 | Nope, I won't have