├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── .ruby-version ├── .travis.yml ├── .travis └── motion_setup.sh ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── benchmark ├── mdbasics.text └── mdsyntax.text ├── lib ├── motion-markdown-it.rb └── motion-markdown-it │ ├── common │ ├── entities.rb │ ├── html_blocks.rb │ ├── html_re.rb │ ├── simpleidn.rb │ └── utils.rb │ ├── helpers │ ├── helper_wrapper.rb │ ├── parse_link_destination.rb │ ├── parse_link_label.rb │ └── parse_link_title.rb │ ├── index.rb │ ├── parser_block.rb │ ├── parser_core.rb │ ├── parser_inline.rb │ ├── presets │ ├── commonmark.rb │ ├── default.rb │ └── zero.rb │ ├── renderer.rb │ ├── ruler.rb │ ├── rules_block │ ├── blockquote.rb │ ├── code.rb │ ├── fence.rb │ ├── heading.rb │ ├── hr.rb │ ├── html_block.rb │ ├── lheading.rb │ ├── list.rb │ ├── paragraph.rb │ ├── reference.rb │ ├── state_block.rb │ └── table.rb │ ├── rules_core │ ├── block.rb │ ├── inline.rb │ ├── linkify.rb │ ├── normalize.rb │ ├── replacements.rb │ ├── smartquotes.rb │ ├── state_core.rb │ └── text_join.rb │ ├── rules_inline │ ├── autolink.rb │ ├── backticks.rb │ ├── balance_pairs.rb │ ├── emphasis.rb │ ├── entity.rb │ ├── escape.rb │ ├── fragments_join.rb │ ├── html_inline.rb │ ├── image.rb │ ├── link.rb │ ├── linkify.rb │ ├── newline.rb │ ├── state_inline.rb │ ├── strikethrough.rb │ └── text.rb │ ├── token.rb │ └── version.rb ├── motion-markdown-it.gemspec ├── rubymotion ├── Gemfile ├── Rakefile ├── app │ └── app_delegate.rb └── spec │ ├── motion-markdown-it │ ├── _helpers │ │ └── _testgen_helper.rb │ ├── bench_mark_spec.rb │ ├── commonmark_spec.rb │ ├── markdown_it_spec.rb │ ├── misc_spec.rb │ ├── ruler_spec.rb │ ├── token_spec.rb │ └── utils_spec.rb │ └── spec_helper.rb └── spec ├── motion-markdown-it ├── commonmark_spec.rb ├── fixtures │ ├── commonmark │ │ ├── bad.txt │ │ ├── good.txt │ │ └── spec.txt │ └── markdown-it │ │ ├── commonmark_extras.txt │ │ ├── fatal.txt │ │ ├── linkify.txt │ │ ├── normalize.txt │ │ ├── proto.txt │ │ ├── smartquotes.txt │ │ ├── strikethrough.txt │ │ ├── tables.txt │ │ ├── typographer.txt │ │ └── xss.txt ├── markdown_it_spec.rb ├── misc_spec.rb ├── ruler_spec.rb ├── testgen_helper.rb ├── token_spec.rb └── utils_spec.rb └── spec_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | ruby: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby: ['2.7', '3.0', '3.1'] 11 | 12 | env: 13 | BUNDLE_GEMFILE: ${{ github.workspace }}/Gemfile 14 | BUNDLE_PATH_RELATIVE_TO_CWD: true 15 | 16 | steps: 17 | - uses: actions/checkout@master 18 | - name: Set up Ruby 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby }} 22 | bundler: default 23 | bundler-cache: true 24 | 25 | - name: Run regular ruby specs 26 | run: | 27 | bundle exec rspec 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .repl_history 3 | .idea 4 | .nova 5 | Gemfile.lock 6 | rubymotion/build 7 | rubymotion/build-log 8 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format documentation 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # test against both regular Ruby and RubyMotion 2 | jobs: 3 | include: 4 | - stage: ruby 2.5 5 | rvm: 2.5.5 6 | script: 7 | - bundle install --jobs=3 --retry=3 8 | - bundle exec rake spec 9 | - stage: ruby 2.6 10 | rvm: 2.6.6 11 | script: 12 | - bundle install --jobs=3 --retry=3 13 | - bundle exec rake spec 14 | - stage: macOS 15 | os: osx 16 | osx_image: xcode11.5 17 | language: objective-c 18 | env: 19 | - RUBYMOTION_LICENSE=1dcac45cc434293009f74b33037bdf7361a3a1ff # Official license key for open-source projects 20 | - TMP_DIR=./tmp # For motion repo, so it doesn't attempt to use /tmp, to which it has no access 21 | - OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES 22 | cache: 23 | bundler: false 24 | install: 25 | - ./.travis/motion_setup.sh 26 | script: 27 | - cd rubymotion 28 | - export BUNDLE_GEMFILE=$PWD/Gemfile 29 | - bundle install --jobs=3 --retry=3 30 | - bundle exec rake spec 31 | - stage: iOS 32 | os: osx 33 | osx_image: xcode11.5 34 | language: objective-c 35 | env: 36 | - RUBYMOTION_LICENSE=1dcac45cc434293009f74b33037bdf7361a3a1ff # Official license key for open-source projects 37 | - TMP_DIR=./tmp # For motion repo, so it doesn't attempt to use /tmp, to which it has no access 38 | - OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES 39 | cache: 40 | bundler: false 41 | install: 42 | - ./.travis/motion_setup.sh 43 | script: 44 | - cd rubymotion 45 | - export BUNDLE_GEMFILE=$PWD/Gemfile 46 | - bundle install --jobs=3 --retry=3 47 | - bundle exec rake spec platform=ios -------------------------------------------------------------------------------- /.travis/motion_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$TRAVIS_OS_NAME" = "osx" ]; then 4 | brew update 5 | brew outdated xctool || brew upgrade xctool 6 | (xcrun simctl list) 7 | wget http://travisci.rubymotion.com/ -O RubyMotion-TravisCI.pkg 8 | sudo installer -pkg RubyMotion-TravisCI.pkg -target / 9 | cp -r /usr/lib/swift/*.dylib /Applications/Xcode.app/Contents/Frameworks/ 10 | touch /Applications/Xcode.app/Contents/Frameworks/.swift-5-staged 11 | sudo mkdir -p ~/Library/RubyMotion/build 12 | sudo chown -R travis ~/Library/RubyMotion 13 | eval "sudo motion activate $RUBYMOTION_LICENSE" 14 | sudo motion update 15 | (motion --version) 16 | (ruby --version) 17 | motion repo 18 | fi -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 13.0.1 - 2023-01-18 2 | ------- 3 | 4 | Synced with markdown-it 13.0.1, see the [CHANGELOG](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) 5 | 6 | 12.3.2 7 | ------- 8 | 9 | Synced with markdown-it 12.3.2, see the [CHANGELOG](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) 10 | 11 | 12.0.6 12 | ------- 13 | 14 | Synced with markdown-it 12.0.6, see the [CHANGELOG](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) 15 | 16 | 11.0.0 17 | ------- 18 | 19 | Synced with markdown-it 11.0.0, see the [CHANGELOG](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) 20 | 21 | 10.0.0 22 | ------- 23 | 24 | Synced with markdown-it 10.0.0, see the [CHANGELOG](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) 25 | 26 | 9.0.1 27 | ------- 28 | 29 | Synced with markdown-it 9.0.1, see the [CHANGELOG](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) 30 | 31 | 8.4.1 32 | ------- 33 | 34 | Synced with markdown-it 8.4.1, see the [CHANGELOG](https://github.com/markdown-it/markdown-it/blob/master/CHANGELOG.md) 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'rspec' 5 | 6 | gem 'kramdown', require: 'kramdown' 7 | gem 'commonmarker' 8 | gem 'redcarpet' 9 | 10 | group :development, :test do 11 | gem 'pry-byebug' 12 | end 13 | 14 | gemspec 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Ruby version: 2 | Copyright (c) 2015 Brett Walker 3 | 4 | Javascript version: 5 | Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin. 6 | 7 | Permission is hereby granted, free of charge, to any person 8 | obtaining a copy of this software and associated documentation 9 | files (the "Software"), to deal in the Software without 10 | restriction, including without limitation the rights to use, 11 | copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the 13 | Software is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 21 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 23 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 24 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new 5 | 6 | task :default => :spec 7 | task :test => :spec 8 | 9 | desc 'Run benchmarks for motion-markdown-it, kramdown, and commonmarker' 10 | task :benchmark do 11 | require 'benchmark' 12 | require 'motion-markdown-it' 13 | require 'kramdown' 14 | require 'commonmarker' 15 | require 'redcarpet' 16 | 17 | runs = 500 18 | files = ['mdsyntax.text', 'mdbasics.text'] 19 | benchmark_dir = File.join(File.dirname(__FILE__), 'benchmark') 20 | 21 | puts 22 | puts "Running tests on #{Time.now.strftime("%Y-%m-%d")} under #{RUBY_DESCRIPTION}" 23 | 24 | files.each do |file| 25 | data = File.read(File.join(benchmark_dir, file)) 26 | puts 27 | puts "==> Test using file #{file} and #{runs} runs" 28 | 29 | results = Benchmark.bmbm(25) do |b| 30 | # results = Benchmark.bm(25) do |b| 31 | b.report("motion-markdown-it #{MotionMarkdownIt::VERSION}") do 32 | parser = MarkdownIt::Parser.new({ html: true, linkify: true, typographer: true }) 33 | runs.times { parser.render(data) } 34 | end 35 | b.report("commonmarker #{CommonMarker::VERSION}") { runs.times { CommonMarker.render_html(data, :DEFAULT) } } 36 | b.report("kramdown #{Kramdown::VERSION}") { runs.times { Kramdown::Document.new(data).to_html } } 37 | b.report("redcarpet 3.4.0") do 38 | parser = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true) 39 | runs.times { parser.render(data) } 40 | end 41 | end 42 | 43 | puts 44 | puts "Real time as a factor of motion-markdown-it" 45 | 46 | md_real = results.first.real 47 | results.each do |result| 48 | puts result.label.ljust(28) << (result.real / md_real).round(4).to_s 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /benchmark/mdbasics.text: -------------------------------------------------------------------------------- 1 | Markdown: Basics 2 | ================ 3 | 4 | 11 | 12 | 13 | Getting the Gist of Markdown's Formatting Syntax 14 | ------------------------------------------------ 15 | 16 | This page offers a brief overview of what it's like to use Markdown. 17 | The [syntax page] [s] provides complete, detailed documentation for 18 | every feature, but Markdown should be very easy to pick up simply by 19 | looking at a few examples of it in action. The examples on this page 20 | are written in a before/after style, showing example syntax and the 21 | HTML output produced by Markdown. 22 | 23 | It's also helpful to simply try Markdown out; the [Dingus] [d] is a 24 | web application that allows you type your own Markdown-formatted text 25 | and translate it to XHTML. 26 | 27 | **Note:** This document is itself written using Markdown; you 28 | can [see the source for it by adding '.text' to the URL] [src]. 29 | 30 | [s]: /projects/markdown/syntax "Markdown Syntax" 31 | [d]: /projects/markdown/dingus "Markdown Dingus" 32 | [src]: /projects/markdown/basics.text 33 | 34 | 35 | ## Paragraphs, Headers, Blockquotes ## 36 | 37 | A paragraph is simply one or more consecutive lines of text, separated 38 | by one or more blank lines. (A blank line is any line that looks like a 39 | blank line -- a line containing nothing spaces or tabs is considered 40 | blank.) Normal paragraphs should not be intended with spaces or tabs. 41 | 42 | Markdown offers two styles of headers: *Setext* and *atx*. 43 | Setext-style headers for `

` and `

` are created by 44 | "underlining" with equal signs (`=`) and hyphens (`-`), respectively. 45 | To create an atx-style header, you put 1-6 hash marks (`#`) at the 46 | beginning of the line -- the number of hashes equals the resulting 47 | HTML header level. 48 | 49 | Blockquotes are indicated using email-style '`>`' angle brackets. 50 | 51 | Markdown: 52 | 53 | A First Level Header 54 | ==================== 55 | 56 | A Second Level Header 57 | --------------------- 58 | 59 | Now is the time for all good men to come to 60 | the aid of their country. This is just a 61 | regular paragraph. 62 | 63 | The quick brown fox jumped over the lazy 64 | dog's back. 65 | 66 | ### Header 3 67 | 68 | > This is a blockquote. 69 | > 70 | > This is the second paragraph in the blockquote. 71 | > 72 | > ## This is an H2 in a blockquote 73 | 74 | 75 | Output: 76 | 77 |

A First Level Header

78 | 79 |

A Second Level Header

80 | 81 |

Now is the time for all good men to come to 82 | the aid of their country. This is just a 83 | regular paragraph.

84 | 85 |

The quick brown fox jumped over the lazy 86 | dog's back.

87 | 88 |

Header 3

89 | 90 |
91 |

This is a blockquote.

92 | 93 |

This is the second paragraph in the blockquote.

94 | 95 |

This is an H2 in a blockquote

96 |
97 | 98 | 99 | 100 | ### Phrase Emphasis ### 101 | 102 | Markdown uses asterisks and underscores to indicate spans of emphasis. 103 | 104 | Markdown: 105 | 106 | Some of these words *are emphasized*. 107 | Some of these words _are emphasized also_. 108 | 109 | Use two asterisks for **strong emphasis**. 110 | Or, if you prefer, __use two underscores instead__. 111 | 112 | Output: 113 | 114 |

Some of these words are emphasized. 115 | Some of these words are emphasized also.

116 | 117 |

Use two asterisks for strong emphasis. 118 | Or, if you prefer, use two underscores instead.

119 | 120 | 121 | 122 | ## Lists ## 123 | 124 | Unordered (bulleted) lists use asterisks, pluses, and hyphens (`*`, 125 | `+`, and `-`) as list markers. These three markers are 126 | interchangable; this: 127 | 128 | * Candy. 129 | * Gum. 130 | * Booze. 131 | 132 | this: 133 | 134 | + Candy. 135 | + Gum. 136 | + Booze. 137 | 138 | and this: 139 | 140 | - Candy. 141 | - Gum. 142 | - Booze. 143 | 144 | all produce the same output: 145 | 146 | 151 | 152 | Ordered (numbered) lists use regular numbers, followed by periods, as 153 | list markers: 154 | 155 | 1. Red 156 | 2. Green 157 | 3. Blue 158 | 159 | Output: 160 | 161 |
    162 |
  1. Red
  2. 163 |
  3. Green
  4. 164 |
  5. Blue
  6. 165 |
166 | 167 | If you put blank lines between items, you'll get `

` tags for the 168 | list item text. You can create multi-paragraph list items by indenting 169 | the paragraphs by 4 spaces or 1 tab: 170 | 171 | * A list item. 172 | 173 | With multiple paragraphs. 174 | 175 | * Another item in the list. 176 | 177 | Output: 178 | 179 |

184 | 185 | 186 | 187 | ### Links ### 188 | 189 | Markdown supports two styles for creating links: *inline* and 190 | *reference*. With both styles, you use square brackets to delimit the 191 | text you want to turn into a link. 192 | 193 | Inline-style links use parentheses immediately after the link text. 194 | For example: 195 | 196 | This is an [example link](http://example.com/). 197 | 198 | Output: 199 | 200 |

This is an 201 | example link.

202 | 203 | Optionally, you may include a title attribute in the parentheses: 204 | 205 | This is an [example link](http://example.com/ "With a Title"). 206 | 207 | Output: 208 | 209 |

This is an 210 | example link.

211 | 212 | Reference-style links allow you to refer to your links by names, which 213 | you define elsewhere in your document: 214 | 215 | I get 10 times more traffic from [Google][1] than from 216 | [Yahoo][2] or [MSN][3]. 217 | 218 | [1]: http://google.com/ "Google" 219 | [2]: http://search.yahoo.com/ "Yahoo Search" 220 | [3]: http://search.msn.com/ "MSN Search" 221 | 222 | Output: 223 | 224 |

I get 10 times more traffic from Google than from Yahoo or MSN.

228 | 229 | The title attribute is optional. Link names may contain letters, 230 | numbers and spaces, but are *not* case sensitive: 231 | 232 | I start my morning with a cup of coffee and 233 | [The New York Times][NY Times]. 234 | 235 | [ny times]: http://www.nytimes.com/ 236 | 237 | Output: 238 | 239 |

I start my morning with a cup of coffee and 240 | The New York Times.

241 | 242 | 243 | ### Images ### 244 | 245 | Image syntax is very much like link syntax. 246 | 247 | Inline (titles are optional): 248 | 249 | ![alt text](/path/to/img.jpg "Title") 250 | 251 | Reference-style: 252 | 253 | ![alt text][id] 254 | 255 | [id]: /path/to/img.jpg "Title" 256 | 257 | Both of the above examples produce the same output: 258 | 259 | alt text 260 | 261 | 262 | 263 | ### Code ### 264 | 265 | In a regular paragraph, you can create code span by wrapping text in 266 | backtick quotes. Any ampersands (`&`) and angle brackets (`<` or 267 | `>`) will automatically be translated into HTML entities. This makes 268 | it easy to use Markdown to write about HTML example code: 269 | 270 | I strongly recommend against using any `` tags. 271 | 272 | I wish SmartyPants used named entities like `—` 273 | instead of decimal-encoded entites like `—`. 274 | 275 | Output: 276 | 277 |

I strongly recommend against using any 278 | <blink> tags.

279 | 280 |

I wish SmartyPants used named entities like 281 | &mdash; instead of decimal-encoded 282 | entites like &#8212;.

283 | 284 | 285 | To specify an entire block of pre-formatted code, indent every line of 286 | the block by 4 spaces or 1 tab. Just like with code spans, `&`, `<`, 287 | and `>` characters will be escaped automatically. 288 | 289 | Markdown: 290 | 291 | If you want your page to validate under XHTML 1.0 Strict, 292 | you've got to put paragraph tags in your blockquotes: 293 | 294 |
295 |

For example.

296 |
297 | 298 | Output: 299 | 300 |

If you want your page to validate under XHTML 1.0 Strict, 301 | you've got to put paragraph tags in your blockquotes:

302 | 303 |
<blockquote>
304 |         <p>For example.</p>
305 |     </blockquote>
306 |     
307 | -------------------------------------------------------------------------------- /lib/motion-markdown-it.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | if defined?(Motion::Project::Config) 4 | 5 | lib_dir_path = File.dirname(File.expand_path(__FILE__)) 6 | Motion::Project::App.setup do |app| 7 | app.files.unshift(Dir.glob(File.join(lib_dir_path, "motion-markdown-it/**/*.rb"))) 8 | end 9 | 10 | require 'linkify-it-rb' 11 | require 'mdurl-rb' 12 | require 'uc.micro-rb' 13 | 14 | else 15 | 16 | require 'mdurl-rb' 17 | require 'uc.micro-rb' 18 | require 'linkify-it-rb' 19 | require 'motion-markdown-it/version' 20 | require 'motion-markdown-it/presets/default' 21 | require 'motion-markdown-it/presets/zero' 22 | require 'motion-markdown-it/presets/commonmark' 23 | require 'motion-markdown-it/common/entities' 24 | require 'motion-markdown-it/common/utils' 25 | require 'motion-markdown-it/common/html_blocks' 26 | require 'motion-markdown-it/common/html_re' 27 | require 'motion-markdown-it/common/simpleidn' 28 | require 'motion-markdown-it/helpers/parse_link_destination' 29 | require 'motion-markdown-it/helpers/parse_link_label' 30 | require 'motion-markdown-it/helpers/parse_link_title' 31 | require 'motion-markdown-it/helpers/helper_wrapper' 32 | require 'motion-markdown-it/parser_inline' 33 | require 'motion-markdown-it/parser_block' 34 | require 'motion-markdown-it/parser_core' 35 | require 'motion-markdown-it/renderer' 36 | require 'motion-markdown-it/rules_core/block' 37 | require 'motion-markdown-it/rules_core/inline' 38 | require 'motion-markdown-it/rules_core/linkify' 39 | require 'motion-markdown-it/rules_core/normalize' 40 | require 'motion-markdown-it/rules_core/replacements' 41 | require 'motion-markdown-it/rules_core/smartquotes' 42 | require 'motion-markdown-it/rules_core/state_core' 43 | require 'motion-markdown-it/rules_core/text_join' 44 | require 'motion-markdown-it/rules_block/blockquote' 45 | require 'motion-markdown-it/rules_block/code' 46 | require 'motion-markdown-it/rules_block/fence' 47 | require 'motion-markdown-it/rules_block/heading' 48 | require 'motion-markdown-it/rules_block/hr' 49 | require 'motion-markdown-it/rules_block/html_block' 50 | require 'motion-markdown-it/rules_block/lheading' 51 | require 'motion-markdown-it/rules_block/list' 52 | require 'motion-markdown-it/rules_block/paragraph' 53 | require 'motion-markdown-it/rules_block/reference' 54 | require 'motion-markdown-it/rules_block/state_block' 55 | require 'motion-markdown-it/rules_block/table' 56 | require 'motion-markdown-it/rules_inline/autolink' 57 | require 'motion-markdown-it/rules_inline/backticks' 58 | require 'motion-markdown-it/rules_inline/balance_pairs' 59 | require 'motion-markdown-it/rules_inline/emphasis' 60 | require 'motion-markdown-it/rules_inline/entity' 61 | require 'motion-markdown-it/rules_inline/escape' 62 | require 'motion-markdown-it/rules_inline/fragments_join' 63 | require 'motion-markdown-it/rules_inline/html_inline' 64 | require 'motion-markdown-it/rules_inline/image' 65 | require 'motion-markdown-it/rules_inline/link' 66 | require 'motion-markdown-it/rules_inline/linkify' 67 | require 'motion-markdown-it/rules_inline/newline' 68 | require 'motion-markdown-it/rules_inline/state_inline' 69 | require 'motion-markdown-it/rules_inline/strikethrough' 70 | require 'motion-markdown-it/rules_inline/text' 71 | 72 | require 'motion-markdown-it/ruler' 73 | require 'motion-markdown-it/token' 74 | require 'motion-markdown-it/index' 75 | 76 | end 77 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/common/html_blocks.rb: -------------------------------------------------------------------------------- 1 | # List of valid html blocks names, accorting to commonmark spec 2 | # http://jgm.github.io/CommonMark/spec.html#html-blocks 3 | #------------------------------------------------------------------------------ 4 | module MarkdownIt 5 | HTML_BLOCKS = [ 6 | 'address', 7 | 'article', 8 | 'aside', 9 | 'base', 10 | 'basefont', 11 | 'blockquote', 12 | 'body', 13 | 'caption', 14 | 'center', 15 | 'col', 16 | 'colgroup', 17 | 'dd', 18 | 'details', 19 | 'dialog', 20 | 'dir', 21 | 'div', 22 | 'dl', 23 | 'dt', 24 | 'fieldset', 25 | 'figcaption', 26 | 'figure', 27 | 'footer', 28 | 'form', 29 | 'frame', 30 | 'frameset', 31 | 'h1', 32 | 'h2', 33 | 'h3', 34 | 'h4', 35 | 'h5', 36 | 'h6', 37 | 'head', 38 | 'header', 39 | 'hr', 40 | 'html', 41 | 'iframe', 42 | 'legend', 43 | 'li', 44 | 'link', 45 | 'main', 46 | 'menu', 47 | 'menuitem', 48 | 'nav', 49 | 'noframes', 50 | 'ol', 51 | 'optgroup', 52 | 'option', 53 | 'p', 54 | 'param', 55 | 'section', 56 | 'source', 57 | 'summary', 58 | 'table', 59 | 'tbody', 60 | 'td', 61 | 'tfoot', 62 | 'th', 63 | 'thead', 64 | 'title', 65 | 'tr', 66 | 'track', 67 | 'ul' 68 | ].freeze 69 | 70 | end 71 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/common/html_re.rb: -------------------------------------------------------------------------------- 1 | # Regexps to match html elements 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module Common 5 | module HtmlRe 6 | ATTR_NAME = '[a-zA-Z_:][a-zA-Z0-9:._-]*' 7 | 8 | UNQUOTED = '[^"\'=<>`\\x00-\\x20]+' 9 | SINGLE_QUOTED = "'[^']*'" 10 | DOUBLE_QUOTED = '"[^"]*"'; 11 | 12 | ATTR_VALUE = '(?:' + UNQUOTED + '|' + SINGLE_QUOTED + '|' + DOUBLE_QUOTED + ')' 13 | 14 | ATTRIBUTE = '(?:\\s+' + ATTR_NAME + '(?:\\s*=\\s*' + ATTR_VALUE + ')?)' 15 | 16 | OPEN_TAG = '<[A-Za-z][A-Za-z0-9\\-]*' + ATTRIBUTE + '*\\s*\\/?>' 17 | 18 | CLOSE_TAG = '<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>' 19 | COMMENT = '|' 20 | PROCESSING = '<[?][\\s\\S]*?[?]>' 21 | DECLARATION = ']*>' 22 | CDATA = '' 23 | 24 | HTML_TAG_RE = Regexp.new('^(?:' + OPEN_TAG + '|' + CLOSE_TAG + '|' + COMMENT + 25 | '|' + PROCESSING + '|' + DECLARATION + '|' + CDATA + ')') 26 | 27 | HTML_OPEN_CLOSE_TAG_RE = Regexp.new('^(?:' + OPEN_TAG + '|' + CLOSE_TAG + ')') 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/helpers/helper_wrapper.rb: -------------------------------------------------------------------------------- 1 | module MarkdownIt 2 | module Helpers 3 | class HelperWrapper 4 | include ParseLinkDestination 5 | include ParseLinkLabel 6 | include ParseLinkTitle 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/motion-markdown-it/helpers/parse_link_destination.rb: -------------------------------------------------------------------------------- 1 | # Parse link destination 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module Helpers 5 | module ParseLinkDestination 6 | include Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def parseLinkDestination(str, pos, max) 10 | lines = 0 11 | start = pos 12 | result = {ok: false, pos: 0, lines: 0, str: ''} 13 | 14 | if (charCodeAt(str, pos) == 0x3C ) # < 15 | pos += 1 16 | while (pos < max) 17 | code = charCodeAt(str, pos) 18 | return result if (code == 0x0A) # \n 19 | return result if (code == 0x3C) # < 20 | if (code == 0x3E) # > 21 | result[:pos] = pos + 1 22 | result[:str] = unescapeAll(str.slice((start + 1)...pos)) 23 | result[:ok] = true 24 | return result 25 | end 26 | if (code == 0x5C && pos + 1 < max) # \ 27 | pos += 2 28 | next 29 | end 30 | 31 | pos += 1 32 | end 33 | 34 | # no closing '>' 35 | return result 36 | end 37 | 38 | # this should be ... } else { ... branch 39 | 40 | level = 0 41 | while (pos < max) 42 | code = charCodeAt(str, pos) 43 | 44 | break if (code == 0x20) 45 | 46 | # ascii control characters 47 | break if (code < 0x20 || code == 0x7F) 48 | 49 | if (code == 0x5C && pos + 1 < max) # \ 50 | break if (charCodeAt(str, pos + 1) == 0x20) 51 | pos += 2 52 | next 53 | end 54 | 55 | if (code == 0x28) # ( 56 | level += 1 57 | return result if (level > 32) 58 | end 59 | 60 | if (code == 0x29) # ) 61 | break if (level == 0) 62 | level -= 1 63 | end 64 | 65 | pos += 1 66 | end 67 | 68 | return result if start == pos 69 | return result if level != 0 70 | 71 | result[:str] = unescapeAll(str.slice(start...pos)) 72 | result[:lines] = lines 73 | result[:pos] = pos 74 | result[:ok] = true 75 | return result 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/helpers/parse_link_label.rb: -------------------------------------------------------------------------------- 1 | # Parse link label 2 | # 3 | # this function assumes that first character ("[") already matches; 4 | # returns the end of the label 5 | # 6 | #------------------------------------------------------------------------------ 7 | module MarkdownIt 8 | module Helpers 9 | module ParseLinkLabel 10 | def parseLinkLabel(state, start, disableNested = false) 11 | labelEnd = -1 12 | max = state.posMax 13 | oldPos = state.pos 14 | state.pos = start + 1 15 | level = 1 16 | 17 | while (state.pos < max) 18 | marker = charCodeAt(state.src, state.pos) 19 | if (marker == 0x5D) # ] 20 | level -= 1 21 | if (level == 0) 22 | found = true 23 | break 24 | end 25 | end 26 | 27 | prevPos = state.pos 28 | state.md.inline.skipToken(state) 29 | if (marker == 0x5B) # [ 30 | if (prevPos == state.pos - 1) 31 | # increase level if we find text `[`, which is not a part of any token 32 | level += 1 33 | elsif (disableNested) 34 | state.pos = oldPos 35 | return -1 36 | end 37 | end 38 | end 39 | 40 | if (found) 41 | labelEnd = state.pos 42 | end 43 | 44 | # restore old state 45 | state.pos = oldPos 46 | 47 | return labelEnd 48 | end 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /lib/motion-markdown-it/helpers/parse_link_title.rb: -------------------------------------------------------------------------------- 1 | # Parse link title 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module Helpers 5 | module ParseLinkTitle 6 | 7 | #------------------------------------------------------------------------------ 8 | def parseLinkTitle(str, pos, max) 9 | lines = 0 10 | start = pos 11 | result = {ok: false, pos: 0, lines: 0, str: ''} 12 | 13 | return result if (pos >= max) 14 | 15 | marker = charCodeAt(str, pos) 16 | 17 | return result if (marker != 0x22 && marker != 0x27 && marker != 0x28) # " ' ( 18 | 19 | pos += 1 20 | 21 | # if opening marker is "(", switch it to closing marker ")" 22 | marker = 0x29 if (marker == 0x28) 23 | 24 | while (pos < max) 25 | code = charCodeAt(str, pos) 26 | if (code == marker) 27 | result[:pos] = pos + 1 28 | result[:lines] = lines 29 | result[:str] = unescapeAll(str.slice((start + 1)...pos)) 30 | result[:ok] = true 31 | return result 32 | elsif (code == 0x28 && marker == 0x29) # ( and ) 33 | return result 34 | elsif (code == 0x0A) 35 | lines += 1 36 | elsif (code == 0x5C && pos + 1 < max) # \ 37 | pos += 1 38 | if (charCodeAt(str, pos) == 0x0A) 39 | lines += 1 40 | end 41 | end 42 | 43 | pos += 1 44 | end 45 | 46 | return result 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/parser_block.rb: -------------------------------------------------------------------------------- 1 | # internal 2 | # class ParserBlock 3 | # 4 | # Block-level tokenizer. 5 | #------------------------------------------------------------------------------ 6 | module MarkdownIt 7 | class ParserBlock 8 | 9 | attr_accessor :ruler 10 | 11 | RULES = [ 12 | # First 2 params - rule name & source. Secondary array - list of rules, 13 | # which can be terminated by this one. 14 | [ 'table', lambda { |state, startLine, endLine, silent| RulesBlock::Table.table(state, startLine, endLine, silent) }, [ 'paragraph', 'reference' ] ], 15 | [ 'code', lambda { |state, startLine, endLine, silent| RulesBlock::Code.code(state, startLine, endLine, silent) } ], 16 | [ 'fence', lambda { |state, startLine, endLine, silent| RulesBlock::Fence.fence(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote', 'list' ] ], 17 | [ 'blockquote', lambda { |state, startLine, endLine, silent| RulesBlock::Blockquote.blockquote(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote', 'list' ] ], 18 | [ 'hr', lambda { |state, startLine, endLine, silent| RulesBlock::Hr.hr(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote', 'list' ] ], 19 | [ 'list', lambda { |state, startLine, endLine, silent| RulesBlock::List.list(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote' ] ], 20 | [ 'reference', lambda { |state, startLine, endLine, silent| RulesBlock::Reference.reference(state, startLine, endLine, silent) } ], 21 | [ 'html_block', lambda { |state, startLine, endLine, silent| RulesBlock::HtmlBlock.html_block(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote' ] ], 22 | [ 'heading', lambda { |state, startLine, endLine, silent| RulesBlock::Heading.heading(state, startLine, endLine, silent) }, [ 'paragraph', 'reference', 'blockquote' ] ], 23 | [ 'lheading', lambda { |state, startLine, endLine, silent| RulesBlock::Lheading.lheading(state, startLine, endLine, silent) } ], 24 | [ 'paragraph', lambda { |state, startLine, endLine, silent| RulesBlock::Paragraph.paragraph(state, startLine) } ] 25 | ] 26 | 27 | 28 | # new ParserBlock() 29 | #------------------------------------------------------------------------------ 30 | def initialize 31 | # ParserBlock#ruler -> Ruler 32 | # 33 | # [[Ruler]] instance. Keep configuration of block rules. 34 | @ruler = Ruler.new 35 | 36 | RULES.each do |rule| 37 | @ruler.push(rule[0], rule[1], {alt: (rule[2] || []) }) 38 | end 39 | end 40 | 41 | 42 | # Generate tokens for input range 43 | #------------------------------------------------------------------------------ 44 | def tokenize(state, startLine, endLine, ignored = false) 45 | rules = @ruler.getRules('') 46 | len = rules.length 47 | line = startLine 48 | hasEmptyLines = false 49 | maxNesting = state.md.options[:maxNesting] 50 | 51 | while line < endLine 52 | state.line = line = state.skipEmptyLines(line) 53 | break if line >= endLine 54 | 55 | # Termination condition for nested calls. 56 | # Nested calls currently used for blockquotes & lists 57 | break if state.sCount[line] < state.blkIndent 58 | 59 | # If nesting level exceeded - skip tail to the end. That's not ordinary 60 | # situation and we should not care about content. 61 | if state.level >= maxNesting 62 | state.line = endLine 63 | break 64 | end 65 | 66 | # Try all possible rules. 67 | # On success, rule should: 68 | # 69 | # - update `state.line` 70 | # - update `state.tokens` 71 | # - return true 72 | 0.upto(len - 1) do |i| 73 | ok = rules[i].call(state, line, endLine, false) 74 | break if ok 75 | end 76 | 77 | # set state.tight if we had an empty line before current tag 78 | # i.e. latest empty line should not count 79 | state.tight = !hasEmptyLines 80 | 81 | # paragraph might "eat" one newline after it in nested lists 82 | if state.isEmpty(state.line - 1) 83 | hasEmptyLines = true 84 | end 85 | 86 | line = state.line 87 | 88 | if line < endLine && state.isEmpty(line) 89 | hasEmptyLines = true 90 | line += 1 91 | state.line = line 92 | end 93 | end 94 | end 95 | 96 | # ParserBlock.parse(src, md, env, outTokens) 97 | # 98 | # Process input string and push block tokens into `outTokens` 99 | #------------------------------------------------------------------------------ 100 | def parse(src, md, env, outTokens) 101 | 102 | return if !src 103 | 104 | state = RulesBlock::StateBlock.new(src, md, env, outTokens) 105 | 106 | tokenize(state, state.line, state.lineMax) 107 | end 108 | 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/parser_core.rb: -------------------------------------------------------------------------------- 1 | # internal 2 | # class Core 3 | # 4 | # Top-level rules executor. Glues block/inline parsers and does intermediate 5 | # transformations. 6 | #------------------------------------------------------------------------------ 7 | module MarkdownIt 8 | class ParserCore 9 | 10 | attr_accessor :ruler 11 | 12 | RULES = [ 13 | [ 'normalize', lambda { |state| RulesCore::Normalize.normalize(state) } ], 14 | [ 'block', lambda { |state| RulesCore::Block.block(state) } ], 15 | [ 'inline', lambda { |state| RulesCore::Inline.inline(state) } ], 16 | [ 'linkify', lambda { |state| RulesCore::Linkify.linkify(state) } ], 17 | [ 'replacements', lambda { |state| RulesCore::Replacements.replace(state) } ], 18 | [ 'smartquotes', lambda { |state| RulesCore::Smartquotes.smartquotes(state) } ], 19 | # `text_join` finds `text_special` tokens (for escape sequences) 20 | # and joins them with the rest of the text 21 | [ 'text_join', lambda { |state| RulesCore::TextJoin.text_join(state) } ], 22 | ] 23 | 24 | 25 | # new Core() 26 | #------------------------------------------------------------------------------ 27 | def initialize 28 | # Core#ruler -> Ruler 29 | # 30 | # [[Ruler]] instance. Keep configuration of core rules. 31 | @ruler = Ruler.new 32 | 33 | RULES.each do |rule| 34 | @ruler.push(rule[0], rule[1]) 35 | end 36 | end 37 | 38 | # Core.process(state) 39 | # 40 | # Executes core chain rules. 41 | #------------------------------------------------------------------------------ 42 | def process(state) 43 | rules = @ruler.getRules('') 44 | rules.each do |rule| 45 | rule.call(state) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/parser_inline.rb: -------------------------------------------------------------------------------- 1 | # internal 2 | # class ParserInline 3 | # 4 | # Tokenizes paragraph content. 5 | #------------------------------------------------------------------------------ 6 | module MarkdownIt 7 | class ParserInline 8 | 9 | attr_accessor :ruler, :ruler2 10 | 11 | #------------------------------------------------------------------------------ 12 | # Parser rules 13 | 14 | RULES = [ 15 | [ 'text', lambda { |state, startLine| RulesInline::Text.text(state, startLine) } ], 16 | [ 'linkify', lambda { |state, silent| RulesInline::Linkify.linkify(state, silent) } ], 17 | [ 'newline', lambda { |state, startLine| RulesInline::Newline.newline(state, startLine) } ], 18 | [ 'escape', lambda { |state, startLine| RulesInline::Escape.escape(state, startLine) } ], 19 | [ 'backticks', lambda { |state, startLine| RulesInline::Backticks.backtick(state, startLine) } ], 20 | [ 'strikethrough', lambda { |state, silent| RulesInline::Strikethrough.tokenize(state, silent) } ], 21 | [ 'emphasis', lambda { |state, silent| RulesInline::Emphasis.tokenize(state, silent) } ], 22 | [ 'link', lambda { |state, startLine| RulesInline::Link.link(state, startLine) } ], 23 | [ 'image', lambda { |state, startLine| RulesInline::Image.image(state, startLine) } ], 24 | [ 'autolink', lambda { |state, startLine| RulesInline::Autolink.autolink(state, startLine) } ], 25 | [ 'html_inline', lambda { |state, startLine| RulesInline::HtmlInline.html_inline(state, startLine) } ], 26 | [ 'entity', lambda { |state, startLine| RulesInline::Entity.entity(state, startLine) } ], 27 | ] 28 | 29 | # `rule2` ruleset was created specifically for emphasis/strikethrough 30 | # post-processing and may be changed in the future. 31 | # 32 | # Don't use this for anything except pairs (plugins working with `balance_pairs`). 33 | # 34 | RULES2 = [ 35 | [ 'balance_pairs', lambda { |state| RulesInline::BalancePairs.link_pairs(state) } ], 36 | [ 'strikethrough', lambda { |state| RulesInline::Strikethrough.postProcess(state) } ], 37 | [ 'emphasis', lambda { |state| RulesInline::Emphasis.postProcess(state) } ], 38 | # [ 'text_collapse', lambda { |state| RulesInline::TextCollapse.text_collapse(state) } ] 39 | # rules for pairs separate '**' into its own text tokens, which may be left unused, 40 | # rule below merges unused segments back with the rest of the text 41 | [ 'fragments_join', lambda { |state| RulesInline::FragmentsJoin.fragments_join(state) } ] 42 | ]; 43 | 44 | #------------------------------------------------------------------------------ 45 | def initialize 46 | # ParserInline#ruler -> Ruler 47 | # 48 | # [[Ruler]] instance. Keep configuration of inline rules. 49 | @ruler = Ruler.new 50 | 51 | RULES.each do |rule| 52 | @ruler.push(rule[0], rule[1]) 53 | end 54 | 55 | # ParserInline#ruler2 -> Ruler 56 | # 57 | # [[Ruler]] instance. Second ruler used for post-processing 58 | # (e.g. in emphasis-like rules). 59 | @ruler2 = Ruler.new 60 | 61 | RULES2.each do |rule| 62 | @ruler2.push(rule[0], rule[1]) 63 | end 64 | end 65 | 66 | # Skip single token by running all rules in validation mode; 67 | # returns `true` if any rule reported success 68 | #------------------------------------------------------------------------------ 69 | def skipToken(state) 70 | pos = state.pos 71 | rules = @ruler.getRules('') 72 | len = rules.length 73 | maxNesting = state.md.options[:maxNesting] 74 | cache = state.cache 75 | ok = false 76 | 77 | if cache[pos] != nil 78 | state.pos = cache[pos] 79 | return 80 | end 81 | 82 | if state.level < maxNesting 83 | 0.upto(len -1) do |i| 84 | # Increment state.level and decrement it later to limit recursion. 85 | # It's harmless to do here, because no tokens are created. But ideally, 86 | # we'd need a separate private state variable for this purpose. 87 | state.level += 1 88 | ok = rules[i].call(state, true) 89 | state.level -= 1 90 | 91 | break if ok 92 | end 93 | else 94 | # Too much nesting, just skip until the end of the paragraph. 95 | # 96 | # NOTE: this will cause links to behave incorrectly in the following case, 97 | # when an amount of `[` is exactly equal to `maxNesting + 1`: 98 | # 99 | # [[[[[[[[[[[[[[[[[[[[[foo]() 100 | # 101 | # TODO: remove this workaround when CM standard will allow nested links 102 | # (we can replace it by preventing links from being parsed in 103 | # validation mode) 104 | state.pos = state.posMax 105 | end 106 | 107 | state.pos += 1 if !ok 108 | cache[pos] = state.pos 109 | end 110 | 111 | # Generate tokens for input range 112 | #------------------------------------------------------------------------------ 113 | def tokenize(state) 114 | rules = @ruler.getRules('') 115 | len = rules.length 116 | end_pos = state.posMax 117 | maxNesting = state.md.options[:maxNesting] 118 | 119 | while state.pos < end_pos 120 | # Try all possible rules. 121 | # On success, rule should: 122 | # 123 | # - update `state.pos` 124 | # - update `state.tokens` 125 | # - return true 126 | 127 | ok = false 128 | if state.level < maxNesting 129 | 0.upto(len - 1) do |i| 130 | ok = rules[i].call(state, false) 131 | break if ok 132 | end 133 | end 134 | 135 | if ok 136 | break if state.pos >= end_pos 137 | next 138 | end 139 | 140 | state.pending += state.src[state.pos] 141 | state.pos += 1 142 | end 143 | 144 | unless state.pending.empty? 145 | state.pushPending 146 | end 147 | end 148 | 149 | # ParserInline.parse(str, md, env, outTokens) 150 | # 151 | # Process input string and push inline tokens into `outTokens` 152 | #------------------------------------------------------------------------------ 153 | def parse(str, md, env, outTokens) 154 | state = RulesInline::StateInline.new(str, md, env, outTokens) 155 | 156 | tokenize(state) 157 | 158 | rules = @ruler2.getRules('') 159 | len = rules.length 160 | 161 | 0.upto(len - 1) do |i| 162 | rules[i].call(state) 163 | end 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/presets/commonmark.rb: -------------------------------------------------------------------------------- 1 | # Commonmark default options 2 | 3 | module MarkdownIt 4 | module Presets 5 | class Commonmark 6 | def self.options 7 | { 8 | options: { 9 | html: true, # Enable HTML tags in source 10 | xhtmlOut: true, # Use '/' to close single tags (
) 11 | breaks: false, # Convert '\n' in paragraphs into
12 | langPrefix: 'language-', # CSS language prefix for fenced blocks 13 | linkify: false, # autoconvert URL-like texts to links 14 | 15 | # Enable some language-neutral replacements + quotes beautification 16 | typographer: false, 17 | 18 | # Double + single quotes replacement pairs, when typographer enabled, 19 | # and smartquotes on. Could be either a String or an Array. 20 | # 21 | # For example, you can use '«»„“' for Russian, '„“‚‘' for German, 22 | # and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 23 | quotes: "\u201c\u201d\u2018\u2019", # “”‘’ 24 | 25 | # Highlighter function. Should return escaped HTML, 26 | # or '' if the source string is not changed and should be escaped externaly. 27 | # If result starts with ) 11 | breaks: false, # Convert '\n' in paragraphs into
12 | langPrefix: 'language-', # CSS language prefix for fenced blocks 13 | linkify: false, # autoconvert URL-like texts to links 14 | 15 | # Enable some language-neutral replacements + quotes beautification 16 | typographer: false, 17 | 18 | # Double + single quotes replacement pairs, when typographer enabled, 19 | # and smartquotes on. Could be either a String or an Array. 20 | # 21 | # For example, you can use '«»„“' for Russian, '„“‚‘' for German, 22 | # and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 23 | quotes: "\u201c\u201d\u2018\u2019", # “”‘’ 24 | 25 | # Highlighter function. Should return escaped HTML, 26 | # or '' if the source string is not changed and should be escaped externaly. 27 | # If result starts with ) 12 | breaks: false, # Convert '\n' in paragraphs into
13 | langPrefix: 'language-', # CSS language prefix for fenced blocks 14 | linkify: false, # autoconvert URL-like texts to links 15 | 16 | # Enable some language-neutral replacements + quotes beautification 17 | typographer: false, 18 | 19 | # Double + single quotes replacement pairs, when typographer enabled, 20 | # and smartquotes on. Could be either a String or an Array. 21 | # 22 | # For example, you can use '«»„“' for Russian, '„“‚‘' for German, 23 | # and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp). 24 | quotes: "\u201c\u201d\u2018\u2019", # “”‘’ 25 | 26 | # Highlighter function. Should return escaped HTML, 27 | # or '' if the source string is not changed and should be escaped externaly. 28 | # If result starts with = 4) 19 | nextLine += 1 20 | last = nextLine 21 | next 22 | end 23 | break 24 | end 25 | 26 | state.line = last 27 | 28 | token = state.push('code_block', 'code', 0) 29 | token.content = state.getLines(startLine, last, 4 + state.blkIndent, false) + "\n" 30 | token.map = [ startLine, state.line ] 31 | return true 32 | end 33 | 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_block/fence.rb: -------------------------------------------------------------------------------- 1 | # fences (``` lang, ~~~ lang) 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesBlock 5 | class Fence 6 | extend Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def self.fence(state, startLine, endLine, silent) 10 | haveEndMarker = false 11 | pos = state.bMarks[startLine] + state.tShift[startLine] 12 | max = state.eMarks[startLine] 13 | 14 | # if it's indented more than 3 spaces, it should be a code block 15 | return false if state.sCount[startLine] - state.blkIndent >= 4 16 | 17 | return false if pos + 3 > max 18 | 19 | marker = charCodeAt(state.src, pos) 20 | 21 | if marker != 0x7E && marker != 0x60 # != ~ && != ` 22 | return false 23 | end 24 | 25 | # scan marker length 26 | mem = pos; 27 | pos = state.skipChars(pos, marker) 28 | len = pos - mem 29 | 30 | return false if len < 3 31 | 32 | markup = state.src.slice(mem...pos) 33 | params = state.src.slice(pos...max) 34 | 35 | if (marker == 0x60) # ` 36 | return false if params.include?(fromCharCode(marker)) 37 | end 38 | 39 | # Since start is found, we can report success here in validation mode 40 | return true if silent 41 | 42 | # search end of block 43 | nextLine = startLine 44 | 45 | while true 46 | nextLine += 1 47 | if nextLine >= endLine 48 | # unclosed block should be autoclosed by end of document. 49 | # also block seems to be autoclosed by end of parent 50 | break 51 | end 52 | 53 | pos = mem = state.bMarks[nextLine] + state.tShift[nextLine] 54 | max = state.eMarks[nextLine]; 55 | 56 | if pos < max && state.sCount[nextLine] < state.blkIndent 57 | # non-empty line with negative indent should stop the list: 58 | # - ``` 59 | # test 60 | break 61 | end 62 | 63 | next if charCodeAt(state.src, pos) != marker 64 | 65 | if state.sCount[nextLine] - state.blkIndent >= 4 66 | # closing fence should be indented less than 4 spaces 67 | next 68 | end 69 | 70 | pos = state.skipChars(pos, marker) 71 | 72 | # closing code fence must be at least as long as the opening one 73 | next if pos - mem < len 74 | 75 | # make sure tail has spaces only 76 | pos = state.skipSpaces(pos) 77 | 78 | next if pos < max 79 | 80 | haveEndMarker = true 81 | # found! 82 | break 83 | end 84 | 85 | # If a fence has heading spaces, they should be removed from its inner block 86 | len = state.sCount[startLine] 87 | 88 | state.line = nextLine + (haveEndMarker ? 1 : 0) 89 | 90 | token = state.push('fence', 'code', 0) 91 | token.info = params 92 | token.content = state.getLines(startLine + 1, nextLine, len, true) 93 | token.markup = markup 94 | token.map = [ startLine, state.line ] 95 | 96 | return true 97 | end 98 | 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_block/heading.rb: -------------------------------------------------------------------------------- 1 | # heading (#, ##, ...) 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesBlock 5 | class Heading 6 | extend Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def self.heading(state, startLine, endLine, silent) 10 | pos = state.bMarks[startLine] + state.tShift[startLine] 11 | max = state.eMarks[startLine] 12 | 13 | # if it's indented more than 3 spaces, it should be a code block 14 | return false if state.sCount[startLine] - state.blkIndent >= 4 15 | 16 | ch = charCodeAt(state.src, pos) 17 | 18 | return false if (ch != 0x23 || pos >= max) 19 | 20 | # count heading level 21 | level = 1 22 | pos += 1 23 | ch = charCodeAt(state.src, pos) 24 | while (ch == 0x23 && pos < max && level <= 6) # '#' 25 | level += 1 26 | pos += 1 27 | ch = charCodeAt(state.src, pos) 28 | end 29 | 30 | return false if (level > 6 || (pos < max && !isSpace(ch))) 31 | 32 | return true if (silent) 33 | 34 | # Let's cut tails like ' ### ' from the end of string 35 | 36 | max = state.skipSpacesBack(max, pos) 37 | tmp = state.skipCharsBack(max, 0x23, pos) # '#' 38 | if (tmp > pos && isSpace(charCodeAt(state.src, tmp - 1))) 39 | max = tmp 40 | end 41 | 42 | state.line = startLine + 1 43 | 44 | token = state.push('heading_open', "h#{level.to_s}", 1) 45 | token.markup = '########'.slice(0...level) 46 | token.map = [ startLine, state.line ] 47 | 48 | token = state.push('inline', '', 0) 49 | token.content = state.src.slice(pos...max).strip 50 | token.map = [ startLine, state.line ] 51 | token.children = [] 52 | 53 | token = state.push('heading_close', "h#{level.to_s}", -1) 54 | token.markup = '########'.slice(0...level) 55 | 56 | return true 57 | end 58 | 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_block/hr.rb: -------------------------------------------------------------------------------- 1 | # Horizontal rule 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesBlock 5 | class Hr 6 | extend Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def self.hr(state, startLine, endLine, silent) 10 | pos = state.bMarks[startLine] + state.tShift[startLine] 11 | max = state.eMarks[startLine] 12 | 13 | # if it's indented more than 3 spaces, it should be a code block 14 | return false if (state.sCount[startLine] - state.blkIndent >= 4) 15 | 16 | marker = charCodeAt(state.src, pos) 17 | pos += 1 18 | 19 | # Check hr marker 20 | if (marker != 0x2A && # * 21 | marker != 0x2D && # - 22 | marker != 0x5F) # _ 23 | return false 24 | end 25 | 26 | # markers can be mixed with spaces, but there should be at least 3 of them 27 | 28 | cnt = 1 29 | while (pos < max) 30 | ch = charCodeAt(state.src, pos) 31 | pos += 1 32 | return false if ch != marker && !isSpace(ch) 33 | cnt += 1 if ch == marker 34 | end 35 | 36 | return false if cnt < 3 37 | return true if silent 38 | 39 | state.line = startLine + 1 40 | 41 | token = state.push('hr', 'hr', 0) 42 | token.map = [ startLine, state.line ] 43 | token.markup = marker.chr * (cnt + 1) 44 | 45 | return true 46 | end 47 | 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_block/html_block.rb: -------------------------------------------------------------------------------- 1 | # HTML block 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesBlock 5 | class HtmlBlock 6 | extend Common::Utils 7 | 8 | HTML_OPEN_CLOSE_TAG_RE = MarkdownIt::Common::HtmlRe::HTML_OPEN_CLOSE_TAG_RE 9 | 10 | # An array of opening and corresponding closing sequences for html tags, 11 | # last argument defines whether it can terminate a paragraph or not 12 | # 13 | HTML_SEQUENCES = [ 14 | [ /^<(script|pre|style|textarea)(?=(\s|>|$))/i, /<\/(script|pre|style|textarea)>/i, true ], 15 | [ /^/, true ], 16 | [ /^<\?/, /\?>/, true ], 17 | [ /^/, true ], 18 | [ /^/, true ], 19 | [ Regexp.new('^|$))', 'i'), /^$/, true ], 20 | [ Regexp.new(HTML_OPEN_CLOSE_TAG_RE.source + '\\s*$'), /^$/, false ] 21 | ]; 22 | 23 | #------------------------------------------------------------------------------ 24 | def self.html_block(state, startLine, endLine, silent) 25 | pos = state.bMarks[startLine] + state.tShift[startLine] 26 | max = state.eMarks[startLine] 27 | 28 | # if it's indented more than 3 spaces, it should be a code block 29 | return false if state.sCount[startLine] - state.blkIndent >= 4 30 | 31 | return false if !state.md.options[:html] 32 | return false if charCodeAt(state.src, pos) != 0x3C # < 33 | 34 | lineText = state.src.slice(pos...max) 35 | 36 | i = 0 37 | while i < HTML_SEQUENCES.length 38 | break if HTML_SEQUENCES[i][0].match(lineText) 39 | i += 1 40 | end 41 | 42 | return false if i == HTML_SEQUENCES.length 43 | 44 | if silent 45 | # true if this sequence can be a terminator, false otherwise 46 | return HTML_SEQUENCES[i][2] 47 | end 48 | 49 | nextLine = startLine + 1 50 | 51 | # If we are here - we detected HTML block. 52 | # Let's roll down till block end. 53 | if !HTML_SEQUENCES[i][1].match(lineText) 54 | while nextLine < endLine 55 | break if state.sCount[nextLine] < state.blkIndent 56 | 57 | pos = state.bMarks[nextLine] + state.tShift[nextLine] 58 | max = state.eMarks[nextLine] 59 | lineText = state.src.slice(pos...max) 60 | 61 | if HTML_SEQUENCES[i][1].match(lineText) 62 | nextLine += 1 if lineText.length != 0 63 | break 64 | end 65 | nextLine += 1 66 | end 67 | end 68 | 69 | state.line = nextLine 70 | 71 | token = state.push('html_block', '', 0) 72 | token.map = [ startLine, nextLine ] 73 | token.content = state.getLines(startLine, nextLine, state.blkIndent, true) 74 | 75 | return true 76 | end 77 | 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_block/lheading.rb: -------------------------------------------------------------------------------- 1 | # lheading (---, ===) 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesBlock 5 | class Lheading 6 | extend Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def self.lheading(state, startLine, endLine, silent = true) 10 | nextLine = startLine + 1 11 | terminatorRules = state.md.block.ruler.getRules('paragraph') 12 | 13 | # if it's indented more than 3 spaces, it should be a code block 14 | return false if state.sCount[startLine] - state.blkIndent >= 4 15 | 16 | oldParentType = state.parentType 17 | state.parentType = 'paragraph' # use paragraph to match terminatorRules 18 | 19 | # jump line-by-line until empty one or EOF 20 | while nextLine < endLine && !state.isEmpty(nextLine) 21 | # this would be a code block normally, but after paragraph 22 | # it's considered a lazy continuation regardless of what's there 23 | (nextLine += 1) and next if (state.sCount[nextLine] - state.blkIndent > 3) 24 | 25 | # 26 | # Check for underline in setext header 27 | # 28 | if state.sCount[nextLine] >= state.blkIndent 29 | pos = state.bMarks[nextLine] + state.tShift[nextLine] 30 | max = state.eMarks[nextLine] 31 | 32 | if pos < max 33 | marker = charCodeAt(state.src, pos) 34 | 35 | if marker == 0x2D || marker == 0x3D # - or = 36 | pos = state.skipChars(pos, marker) 37 | pos = state.skipSpaces(pos) 38 | 39 | if pos >= max 40 | level = (marker == 0x3D ? 1 : 2) # = 41 | break 42 | end 43 | end 44 | end 45 | end 46 | 47 | # quirk for blockquotes, this line should already be checked by that rule 48 | (nextLine += 1) and next if state.sCount[nextLine] < 0 49 | 50 | # Some tags can terminate paragraph without empty line. 51 | terminate = false 52 | l = terminatorRules.length 53 | 0.upto(l - 1) do |i| 54 | if terminatorRules[i].call(state, nextLine, endLine, true) 55 | terminate = true 56 | break 57 | end 58 | end 59 | break if terminate 60 | 61 | nextLine += 1 62 | end 63 | 64 | if !level 65 | # Didn't find valid underline 66 | return false 67 | end 68 | 69 | content = state.getLines(startLine, nextLine, state.blkIndent, false).strip 70 | 71 | state.line = nextLine + 1 72 | 73 | token = state.push('heading_open', "h#{level.to_s}", 1) 74 | token.markup = marker.chr 75 | token.map = [ startLine, state.line ] 76 | 77 | token = state.push('inline', '', 0) 78 | # token.content = state.src.slice(pos...state.eMarks[startLine]).strip 79 | token.content = content 80 | token.map = [ startLine, state.line - 1 ] 81 | token.children = [] 82 | 83 | token = state.push('heading_close', "h#{level.to_s}", -1) 84 | token.markup = marker.chr 85 | 86 | state.parentType = oldParentType 87 | 88 | return true 89 | end 90 | 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_block/paragraph.rb: -------------------------------------------------------------------------------- 1 | # Paragraph 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesBlock 5 | class Paragraph 6 | 7 | #------------------------------------------------------------------------------ 8 | def self.paragraph(state, startLine) 9 | nextLine = startLine + 1 10 | terminatorRules = state.md.block.ruler.getRules('paragraph') 11 | endLine = state.lineMax 12 | 13 | oldParentType = state.parentType 14 | state.parentType = 'paragraph' 15 | 16 | # jump line-by-line until empty one or EOF 17 | # for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { 18 | while nextLine < endLine && !state.isEmpty(nextLine) 19 | # this would be a code block normally, but after paragraph 20 | # it's considered a lazy continuation regardless of what's there 21 | (nextLine += 1) and next if (state.sCount[nextLine] - state.blkIndent > 3) 22 | 23 | # quirk for blockquotes, this line should already be checked by that rule 24 | (nextLine += 1) and next if state.sCount[nextLine] < 0 25 | 26 | # Some tags can terminate paragraph without empty line. 27 | terminate = false 28 | 0.upto(terminatorRules.length - 1) do |i| 29 | if terminatorRules[i].call(state, nextLine, endLine, true) 30 | terminate = true 31 | break 32 | end 33 | end 34 | break if terminate 35 | nextLine += 1 36 | end 37 | 38 | content = state.getLines(startLine, nextLine, state.blkIndent, false).strip 39 | 40 | state.line = nextLine 41 | 42 | token = state.push('paragraph_open', 'p', 1) 43 | token.map = [ startLine, state.line ] 44 | 45 | token = state.push('inline', '', 0) 46 | token.content = content 47 | token.map = [ startLine, state.line ] 48 | token.children = [] 49 | 50 | token = state.push('paragraph_close', 'p', -1) 51 | 52 | state.parentType = oldParentType 53 | 54 | return true 55 | end 56 | 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_block/reference.rb: -------------------------------------------------------------------------------- 1 | module MarkdownIt 2 | module RulesBlock 3 | class Reference 4 | extend Common::Utils 5 | 6 | #------------------------------------------------------------------------------ 7 | def self.reference(state, startLine, _endLine, silent) 8 | lines = 0 9 | pos = state.bMarks[startLine] + state.tShift[startLine] 10 | max = state.eMarks[startLine] 11 | nextLine = startLine + 1 12 | 13 | # if it's indented more than 3 spaces, it should be a code block 14 | return false if state.sCount[startLine] - state.blkIndent >= 4 15 | 16 | return false if charCodeAt(state.src, pos) != 0x5B # [ 17 | 18 | # Simple check to quickly interrupt scan on [link](url) at the start of line. 19 | # Can be useful on practice: https://github.com/markdown-it/markdown-it/issues/54 20 | pos += 1 21 | while (pos < max) 22 | if (charCodeAt(state.src, pos) == 0x5D && # ] 23 | charCodeAt(state.src, pos - 1) != 0x5C) # \ 24 | return false if (pos + 1 === max) 25 | return false if (charCodeAt(state.src, pos + 1) != 0x3A) # : 26 | break 27 | end 28 | pos += 1 29 | end 30 | 31 | endLine = state.lineMax 32 | 33 | # jump line-by-line until empty one or EOF 34 | terminatorRules = state.md.block.ruler.getRules('reference') 35 | 36 | oldParentType = state.parentType 37 | state.parentType = 'reference' 38 | 39 | while nextLine < endLine && !state.isEmpty(nextLine) 40 | # this would be a code block normally, but after paragraph 41 | # it's considered a lazy continuation regardless of what's there 42 | (nextLine += 1) and next if (state.sCount[nextLine] - state.blkIndent > 3) 43 | 44 | # quirk for blockquotes, this line should already be checked by that rule 45 | (nextLine += 1) and next if state.sCount[nextLine] < 0 46 | 47 | # Some tags can terminate paragraph without empty line. 48 | terminate = false 49 | (0...terminatorRules.length).each do |i| 50 | if (terminatorRules[i].call(state, nextLine, endLine, true)) 51 | terminate = true 52 | break 53 | end 54 | end 55 | break if (terminate) 56 | nextLine += 1 57 | end 58 | 59 | str = state.getLines(startLine, nextLine, state.blkIndent, false).strip 60 | max = str.length 61 | labelEnd = -1 62 | 63 | pos = 1 64 | while pos < max 65 | ch = charCodeAt(str, pos) 66 | if (ch == 0x5B ) # [ 67 | return false 68 | elsif (ch == 0x5D) # ] 69 | labelEnd = pos 70 | break 71 | elsif (ch == 0x0A) # \n 72 | lines += 1 73 | elsif (ch == 0x5C) # \ 74 | pos += 1 75 | if (pos < max && charCodeAt(str, pos) == 0x0A) 76 | lines += 1 77 | end 78 | end 79 | pos += 1 80 | end 81 | 82 | return false if (labelEnd < 0 || charCodeAt(str, labelEnd + 1) != 0x3A) # : 83 | 84 | # [label]: destination 'title' 85 | # ^^^ skip optional whitespace here 86 | pos = labelEnd + 2 87 | while pos < max 88 | ch = charCodeAt(str, pos) 89 | if (ch == 0x0A) 90 | lines += 1 91 | elsif isSpace(ch) 92 | else 93 | break 94 | end 95 | pos += 1 96 | end 97 | 98 | # [label]: destination 'title' 99 | # ^^^^^^^^^^^ parse this 100 | res = state.md.helpers.parseLinkDestination(str, pos, max) 101 | return false if (!res[:ok]) 102 | 103 | href = state.md.normalizeLink.call(res[:str]) 104 | return false if (!state.md.validateLink.call(href)) 105 | 106 | pos = res[:pos] 107 | lines += res[:lines] 108 | 109 | # save cursor state, we could require to rollback later 110 | destEndPos = pos 111 | destEndLineNo = lines 112 | 113 | # [label]: destination 'title' 114 | # ^^^ skipping those spaces 115 | start = pos 116 | while (pos < max) 117 | ch = charCodeAt(str, pos) 118 | if (ch == 0x0A) 119 | lines += 1 120 | elsif isSpace(ch) 121 | else 122 | break 123 | end 124 | pos += 1 125 | end 126 | 127 | # [label]: destination 'title' 128 | # ^^^^^^^ parse this 129 | res = state.md.helpers.parseLinkTitle(str, pos, max) 130 | if (pos < max && start != pos && res[:ok]) 131 | title = res[:str] 132 | pos = res[:pos] 133 | lines += res[:lines] 134 | else 135 | title = '' 136 | pos = destEndPos 137 | lines = destEndLineNo 138 | end 139 | 140 | # skip trailing spaces until the rest of the line 141 | while pos < max 142 | ch = charCodeAt(str, pos) 143 | break if !isSpace(ch) 144 | pos += 1 145 | end 146 | 147 | if (pos < max && charCodeAt(str, pos) != 0x0A) 148 | if (title) 149 | # garbage at the end of the line after title, 150 | # but it could still be a valid reference if we roll back 151 | title = '' 152 | pos = destEndPos 153 | lines = destEndLineNo 154 | while pos < max 155 | ch = charCodeAt(str, pos) 156 | break if !isSpace(ch) 157 | pos += 1 158 | end 159 | end 160 | end 161 | 162 | if (pos < max && charCodeAt(str, pos) != 0x0A) 163 | # garbage at the end of the line 164 | return false 165 | end 166 | 167 | label = normalizeReference(str.slice(1...labelEnd)) 168 | if label == '' 169 | # CommonMark 0.20 disallows empty labels 170 | return false 171 | end 172 | 173 | # Reference can not terminate anything. This check is for safety only. 174 | # istanbul ignore if 175 | return true if (silent) 176 | 177 | if (state.env[:references].nil?) 178 | state.env[:references] = {} 179 | end 180 | if state.env[:references][label].nil? 181 | state.env[:references][label] = { title: title, href: href } 182 | end 183 | 184 | state.parentType = oldParentType 185 | 186 | state.line = startLine + lines + 1 187 | return true 188 | end 189 | 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_block/state_block.rb: -------------------------------------------------------------------------------- 1 | # Parser state class 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesBlock 5 | class StateBlock 6 | include MarkdownIt::Common::Utils 7 | 8 | attr_accessor :src, :md, :env, :tokens, :bMarks, :eMarks, :tShift, :sCount, :bsCount 9 | attr_accessor :blkIndent, :line, :lineMax, :tight, :parentType, :ddIndent, :listIndent 10 | attr_accessor :level, :result 11 | 12 | #------------------------------------------------------------------------------ 13 | def initialize(src, md, env, tokens) 14 | @src = src 15 | 16 | # link to parser instance 17 | @md = md 18 | @env = env 19 | 20 | #--- Internal state variables 21 | 22 | @tokens = tokens 23 | 24 | @bMarks = [] # line begin offsets for fast jumps 25 | @eMarks = [] # line end offsets for fast jumps 26 | @tShift = [] # offsets of the first non-space characters (tabs not expanded) 27 | @sCount = [] # indents for each line (tabs expanded) 28 | 29 | # An amount of virtual spaces (tabs expanded) between beginning 30 | # of each line (bMarks) and real beginning of that line. 31 | # 32 | # It exists only as a hack because blockquotes override bMarks 33 | # losing information in the process. 34 | # 35 | # It's used only when expanding tabs, you can think about it as 36 | # an initial tab length, e.g. bsCount=21 applied to string `\t123` 37 | # means first tab should be expanded to 4-21%4 === 3 spaces. 38 | # 39 | @bsCount = [] 40 | 41 | # block parser variables 42 | @blkIndent = 0 # equired block content indent (for example, if we are 43 | # inside a list, it would be positioned after list marker) 44 | @line = 0 # line index in src 45 | @lineMax = 0 # lines count 46 | @tight = false # loose/tight mode for lists 47 | @parentType = 'root' # if `list`, block parser stops on two newlines 48 | @ddIndent = -1 # indent of the current dd block (-1 if there isn't any) 49 | @listIndent = -1 # indent of the current list block (-1 if there isn't any) 50 | 51 | # can be 'blockquote', 'list', 'root', 'paragraph' or 'reference' 52 | # used in lists to determine if they interrupt a paragraph 53 | @parentType = 'root' 54 | 55 | @level = 0 56 | 57 | # renderer 58 | @result = '' 59 | 60 | # Create caches 61 | # Generate markers. 62 | s = @src 63 | indent_found = false 64 | 65 | start = pos = indent = offset = 0 66 | len = s.length 67 | while pos < len 68 | ch = charCodeAt(s, pos) 69 | 70 | if !indent_found 71 | if isSpace(ch) 72 | indent += 1 73 | 74 | if ch == 0x09 75 | offset += 4 - offset % 4 76 | else 77 | offset += 1 78 | end 79 | (pos += 1) and next 80 | else 81 | indent_found = true 82 | end 83 | end 84 | 85 | if ch == 0x0A || pos == (len - 1) 86 | pos += 1 if ch != 0x0A 87 | @bMarks.push(start) 88 | @eMarks.push(pos) 89 | @tShift.push(indent) 90 | @sCount.push(offset) 91 | @bsCount.push(0) 92 | 93 | indent_found = false 94 | indent = 0 95 | offset = 0 96 | start = pos + 1 97 | end 98 | 99 | pos += 1 100 | end 101 | 102 | # Push fake entry to simplify cache bounds checks 103 | @bMarks.push(s.length) 104 | @eMarks.push(s.length) 105 | @tShift.push(0) 106 | @sCount.push(0) 107 | @bsCount.push(0) 108 | 109 | @lineMax = @bMarks.length - 1 # don't count last fake line 110 | end 111 | 112 | # Push new token to "stream". 113 | #------------------------------------------------------------------------------ 114 | def push(type, tag, nesting) 115 | token = Token.new(type, tag, nesting) 116 | token.block = true 117 | 118 | @level -= 1 if nesting < 0 # closing tag 119 | token.level = @level 120 | @level += 1 if nesting > 0 # opening tag 121 | 122 | @tokens.push(token) 123 | return token 124 | end 125 | 126 | #------------------------------------------------------------------------------ 127 | def isEmpty(line) 128 | return @bMarks[line] + @tShift[line] >= @eMarks[line] 129 | end 130 | 131 | #------------------------------------------------------------------------------ 132 | def skipEmptyLines(from) 133 | while from < @lineMax 134 | break if (@bMarks[from] + @tShift[from] < @eMarks[from]) 135 | from += 1 136 | end 137 | return from 138 | end 139 | 140 | # Skip spaces from given position. 141 | #------------------------------------------------------------------------------ 142 | def skipSpaces(pos) 143 | max = @src.length 144 | while pos < max 145 | ch = charCodeAt(@src, pos) 146 | break if !isSpace(ch) 147 | pos += 1 148 | end 149 | return pos 150 | end 151 | 152 | # Skip spaces from given position in reverse. 153 | #------------------------------------------------------------------------------ 154 | def skipSpacesBack(pos, min) 155 | return pos if pos <= min 156 | 157 | while (pos > min) 158 | return pos + 1 if !isSpace(charCodeAt(@src, pos -= 1)) 159 | end 160 | return pos 161 | end 162 | 163 | # Skip char codes from given position 164 | #------------------------------------------------------------------------------ 165 | def skipChars(pos, code) 166 | max = @src.length 167 | while pos < max 168 | break if (charCodeAt(@src, pos) != code) 169 | pos += 1 170 | end 171 | return pos 172 | end 173 | 174 | # Skip char codes reverse from given position - 1 175 | #------------------------------------------------------------------------------ 176 | def skipCharsBack(pos, code, min) 177 | return pos if pos <= min 178 | 179 | while (pos > min) 180 | return (pos + 1) if code != charCodeAt(@src, pos -= 1) 181 | end 182 | return pos 183 | end 184 | 185 | # cut lines range from source. 186 | #------------------------------------------------------------------------------ 187 | def getLines(line_begin, line_end, indent, keepLastLF) 188 | line = line_begin 189 | 190 | return '' if line_begin >= line_end 191 | 192 | queue = Array.new(line_end - line_begin) 193 | 194 | i = 0 195 | while line < line_end 196 | lineIndent = 0 197 | lineStart = first = @bMarks[line] 198 | 199 | if line + 1 < line_end || keepLastLF 200 | # No need for bounds check because we have fake entry on tail. 201 | last = @eMarks[line] + 1 202 | else 203 | last = @eMarks[line] 204 | end 205 | 206 | while first < last && lineIndent < indent 207 | ch = charCodeAt(@src, first) 208 | 209 | if isSpace(ch) 210 | if ch === 0x09 211 | lineIndent += 4 - (lineIndent + @bsCount[line]) % 4 212 | else 213 | lineIndent += 1 214 | end 215 | elsif first - lineStart < @tShift[line] 216 | # patched tShift masked characters to look like spaces (blockquotes, list markers) 217 | lineIndent += 1 218 | else 219 | break 220 | end 221 | 222 | first += 1 223 | end 224 | 225 | if lineIndent > indent 226 | # partially expanding tabs in code blocks, e.g '\t\tfoobar' 227 | # with indent=2 becomes ' \tfoobar' 228 | queue[i] = (' ' * (lineIndent - indent)) + @src.slice(first...last) 229 | else 230 | queue[i] = @src.slice(first...last) 231 | end 232 | line += 1 233 | i += 1 234 | end 235 | 236 | return queue.join('') 237 | end 238 | 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_block/table.rb: -------------------------------------------------------------------------------- 1 | # GFM table, https://github.github.com/gfm/#tables-extension- 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesBlock 5 | class Table 6 | extend Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def self.getLine(state, line) 10 | pos = state.bMarks[line] + state.tShift[line] 11 | max = state.eMarks[line] 12 | 13 | return state.src[pos...max] 14 | end 15 | 16 | #------------------------------------------------------------------------------ 17 | def self.escapedSplit(str) 18 | result = [] 19 | pos = 0 20 | max = str.length 21 | isEscaped = false 22 | lastPos = 0 23 | current = '' 24 | 25 | ch = charCodeAt(str, pos) 26 | 27 | while (pos < max) 28 | if ch == 0x7c # | 29 | if (!isEscaped) 30 | # pipe separating cells, '|' 31 | result.push(current + str[lastPos...pos]) 32 | current = '' 33 | lastPos = pos + 1 34 | else 35 | # escaped pipe, '\|' 36 | current += str[lastPos...(pos - 1)] 37 | lastPos = pos 38 | end 39 | end 40 | 41 | isEscaped = (ch == 0x5c) # '\' 42 | pos += 1 43 | 44 | ch = charCodeAt(str, pos) 45 | end 46 | 47 | result.push(current + str[lastPos..-1]) 48 | 49 | return result 50 | end 51 | 52 | #------------------------------------------------------------------------------ 53 | def self.table(state, startLine, endLine, silent) 54 | # should have at least two lines 55 | return false if (startLine + 2 > endLine) 56 | 57 | nextLine = startLine + 1 58 | 59 | return false if (state.sCount[nextLine] < state.blkIndent) 60 | 61 | # if it's indented more than 3 spaces, it should be a code block 62 | return false if state.sCount[nextLine] - state.blkIndent >= 4 63 | 64 | # first character of the second line should be '|', '-', ':', 65 | # and no other characters are allowed but spaces; 66 | # basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp 67 | 68 | pos = state.bMarks[nextLine] + state.tShift[nextLine] 69 | return false if (pos >= state.eMarks[nextLine]) 70 | 71 | firstCh = charCodeAt(state.src, pos) 72 | pos += 1 73 | return false if (firstCh != 0x7C && firstCh != 0x2D && firstCh != 0x3A) # | or - or : 74 | 75 | return false if (pos >= state.eMarks[nextLine]) 76 | 77 | secondCh = charCodeAt(state.src, pos) 78 | pos += 1 79 | return false if (secondCh != 0x7C && secondCh != 0x2D && secondCh != 0x3A && !isSpace(secondCh)) # | or - or : 80 | 81 | # if first character is '-', then second character must not be a space 82 | # (due to parsing ambiguity with list) 83 | return false if (firstCh === 0x2D && isSpace(secondCh)) # - 84 | 85 | while pos < state.eMarks[nextLine] 86 | ch = charCodeAt(state.src, pos) 87 | return false if (ch != 0x7C && ch != 0x2D && ch != 0x3A && !isSpace(ch)) # | or - or : 88 | 89 | pos += 1 90 | end 91 | 92 | lineText = getLine(state, startLine + 1) 93 | 94 | columns = lineText.split('|') 95 | aligns = [] 96 | (0...columns.length).each do |i| 97 | t = columns[i].strip 98 | if t.empty? 99 | # allow empty columns before and after table, but not in between columns 100 | # e.g. allow ` |---| `, disallow ` ---||--- ` 101 | if (i == 0 || i == columns.length - 1) 102 | next 103 | else 104 | return false 105 | end 106 | end 107 | 108 | return false if (/^:?-+:?$/ =~ t).nil? 109 | if (charCodeAt(t, t.length - 1) == 0x3A) # ':' 110 | aligns.push(charCodeAt(t, 0) == 0x3A ? 'center' : 'right') 111 | elsif (charCodeAt(t, 0) == 0x3A) 112 | aligns.push('left') 113 | else 114 | aligns.push('') 115 | end 116 | end 117 | 118 | lineText = getLine(state, startLine).strip 119 | return false if !lineText.include?('|') 120 | return false if state.sCount[startLine] - state.blkIndent >= 4 121 | columns = self.escapedSplit(lineText) 122 | 123 | columns.shift if (columns.length && columns[0] == '') 124 | columns.pop if (columns.length && columns[columns.length - 1] == '') 125 | 126 | # header row will define an amount of columns in the entire table, 127 | # and align row should be exactly the same (the rest of the rows can differ) 128 | columnCount = columns.length 129 | return false if columnCount == 0 || columnCount != aligns.length 130 | 131 | return true if silent 132 | 133 | oldParentType = state.parentType 134 | state.parentType = 'table' 135 | 136 | # use 'blockquote' lists for termination because it's 137 | # the most similar to tables 138 | terminatorRules = state.md.block.ruler.getRules('blockquote') 139 | 140 | token = state.push('table_open', 'table', 1) 141 | token.map = tableLines = [ startLine, 0 ] 142 | 143 | token = state.push('thead_open', 'thead', 1) 144 | token.map = [ startLine, startLine + 1 ] 145 | 146 | token = state.push('tr_open', 'tr', 1) 147 | token.map = [ startLine, startLine + 1 ] 148 | 149 | (0...columns.length).each do |i| 150 | token = state.push('th_open', 'th', 1) 151 | unless aligns[i].empty? 152 | token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ] 153 | end 154 | 155 | token = state.push('inline', '', 0) 156 | token.content = columns[i].strip 157 | token.children = [] 158 | 159 | token = state.push('th_close', 'th', -1) 160 | end 161 | 162 | token = state.push('tr_close', 'tr', -1) 163 | token = state.push('thead_close', 'thead', -1) 164 | 165 | nextLine = startLine + 2 166 | while nextLine < endLine 167 | break if (state.sCount[nextLine] < state.blkIndent) 168 | 169 | terminate = false 170 | (0...terminatorRules.length).each do |i| 171 | if (terminatorRules[i].call(state, nextLine, endLine, true)) 172 | terminate = true 173 | break 174 | end 175 | end 176 | 177 | break if (terminate) 178 | 179 | lineText = getLine(state, nextLine).strip 180 | break if lineText.empty? 181 | break if state.sCount[nextLine] - state.blkIndent >= 4 182 | columns = self.escapedSplit(lineText) 183 | columns.shift if (columns.length && columns[0] == '') 184 | columns.pop if (columns.length && columns[columns.length - 1] == '') 185 | 186 | if (nextLine == startLine + 2) 187 | token = state.push('tbody_open', 'tbody', 1) 188 | token.map = tbodyLines = [ startLine + 2, 0 ] 189 | end 190 | 191 | token = state.push('tr_open', 'tr', 1) 192 | token.map = [ nextLine, nextLine + 1 ] 193 | 194 | (0...columnCount).each do |i| 195 | token = state.push('td_open', 'td', 1) 196 | token.map = [ nextLine, nextLine + 1 ] 197 | unless aligns[i].empty? 198 | token.attrs = [ [ 'style', 'text-align:' + aligns[i] ] ] 199 | end 200 | 201 | token = state.push('inline', '', 0) 202 | token.content = columns[i] ? columns[i].strip : '' 203 | token.children = [] 204 | 205 | token = state.push('td_close', 'td', -1) 206 | end 207 | token = state.push('tr_close', 'tr', -1) 208 | nextLine += 1 209 | end 210 | 211 | if (tbodyLines) 212 | token = state.push('tbody_close', 'tbody', -1) 213 | tbodyLines[1] = nextLine 214 | end 215 | 216 | token = state.push('table_close', 'table', -1) 217 | tableLines[1] = nextLine 218 | 219 | state.parentType = oldParentType 220 | state.line = nextLine 221 | return true 222 | end 223 | 224 | end 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_core/block.rb: -------------------------------------------------------------------------------- 1 | module MarkdownIt 2 | module RulesCore 3 | class Block 4 | 5 | #------------------------------------------------------------------------------ 6 | def self.block(state) 7 | if state.inlineMode 8 | token = Token.new('inline', '', 0) 9 | token.content = state.src 10 | token.map = [ 0, 1 ] 11 | token.children = [] 12 | state.tokens.push(token) 13 | else 14 | state.md.block.parse(state.src, state.md, state.env, state.tokens) 15 | end 16 | end 17 | 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_core/inline.rb: -------------------------------------------------------------------------------- 1 | module MarkdownIt 2 | module RulesCore 3 | class Inline 4 | 5 | #------------------------------------------------------------------------------ 6 | def self.inline(state) 7 | tokens = state.tokens 8 | 9 | # Parse inlines 10 | 0.upto(tokens.length - 1) do |i| 11 | tok = tokens[i] 12 | if tok.type == 'inline' 13 | state.md.inline.parse(tok.content, state.md, state.env, tok.children) 14 | end 15 | end 16 | end 17 | 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_core/linkify.rb: -------------------------------------------------------------------------------- 1 | # Replace link-like texts with link nodes. 2 | # 3 | # Currently restricted by `md.validateLink()` to http/https/ftp 4 | #------------------------------------------------------------------------------ 5 | module MarkdownIt 6 | module RulesCore 7 | class Linkify 8 | MAILTO_RE = /^mailto\:/ 9 | 10 | #------------------------------------------------------------------------------ 11 | def self.isLinkOpen(str) 12 | return !(/^\s]/i =~ str).nil? 13 | end 14 | def self.isLinkClose(str) 15 | return !(/^<\/a\s*>/i =~ str).nil? 16 | end 17 | 18 | #------------------------------------------------------------------------------ 19 | def self.linkify(state) 20 | blockTokens = state.tokens 21 | 22 | return if (!state.md.options[:linkify]) 23 | 24 | (0...blockTokens.length).each do |j| 25 | if (blockTokens[j].type != 'inline' || !state.md.linkify.pretest(blockTokens[j].content)) 26 | next 27 | end 28 | 29 | tokens = blockTokens[j].children 30 | 31 | htmlLinkLevel = 0 32 | 33 | # We scan from the end, to keep position when new tags added. 34 | # Use reversed logic in links start/end match 35 | i = tokens.length - 1 36 | while i >= 0 37 | currentToken = tokens[i] 38 | 39 | # Skip content of markdown links 40 | if (currentToken.type == 'link_close') 41 | i -= 1 42 | while (tokens[i].level != currentToken.level && tokens[i].type != 'link_open') 43 | i -= 1 44 | end 45 | i -= 1 and next 46 | end 47 | 48 | # Skip content of html tag links 49 | if (currentToken.type == 'html_inline') 50 | if (isLinkOpen(currentToken.content) && htmlLinkLevel > 0) 51 | htmlLinkLevel -= 1 52 | end 53 | if (isLinkClose(currentToken.content)) 54 | htmlLinkLevel += 1 55 | end 56 | end 57 | i -= 1 and next if (htmlLinkLevel > 0) 58 | 59 | if (currentToken.type == 'text' && state.md.linkify.test(currentToken.content)) 60 | 61 | text = currentToken.content 62 | links = state.md.linkify.match(text) 63 | 64 | # Now split string to nodes 65 | nodes = [] 66 | level = currentToken.level 67 | lastPos = 0 68 | 69 | # forbid escape sequence at the start of the string, 70 | # this avoids http\://example.com/ from being linkified as 71 | # http://example.com/ 72 | if (links.length > 0 && 73 | links[0].index == 0 && 74 | i > 0 && 75 | tokens[i - 1].type == 'text_special') 76 | links = links.slice(1..-1) 77 | end 78 | 79 | (0...links.length).each do |ln| 80 | url = links[ln].url 81 | fullUrl = state.md.normalizeLink.call(url) 82 | next if (!state.md.validateLink.call(fullUrl)) 83 | 84 | urlText = links[ln].text 85 | 86 | # Linkifier might send raw hostnames like "example.com", where url 87 | # starts with domain name. So we prepend http:// in those cases, 88 | # and remove it afterwards. 89 | if links[ln].schema.empty? 90 | urlText = state.md.normalizeLinkText.call("http://#{urlText}").sub(/^http:\/\//, '') 91 | elsif (links[ln].schema == 'mailto:' && !(MAILTO_RE =~ urlText)) 92 | urlText = state.md.normalizeLinkText.call("mailto:#{urlText}").sub(MAILTO_RE, '') 93 | else 94 | urlText = state.md.normalizeLinkText.call(urlText) 95 | end 96 | 97 | pos = links[ln].index 98 | 99 | if (pos > lastPos) 100 | token = Token.new('text', '', 0) 101 | token.content = text.slice(lastPos...pos) 102 | token.level = level 103 | nodes.push(token) 104 | end 105 | 106 | token = Token.new('link_open', 'a', 1) 107 | token.attrs = [ [ 'href', fullUrl ] ] 108 | token.level = level 109 | level += 1 110 | token.markup = 'linkify' 111 | token.info = 'auto' 112 | nodes.push(token) 113 | 114 | token = Token.new('text', '', 0) 115 | token.content = urlText 116 | token.level = level 117 | nodes.push(token) 118 | 119 | token = Token.new('link_close', 'a', -1) 120 | level -= 1 121 | token.level = level 122 | token.markup = 'linkify' 123 | token.info = 'auto' 124 | nodes.push(token) 125 | 126 | lastPos = links[ln].lastIndex 127 | end 128 | if (lastPos < text.length) 129 | token = Token.new('text', '', 0) 130 | token.content = text[lastPos..-1] 131 | token.level = level 132 | nodes.push(token) 133 | end 134 | 135 | # replace current node 136 | tokens[i] = nodes 137 | blockTokens[j].children = (tokens.flatten!(1)) 138 | end 139 | i -= 1 140 | end 141 | end 142 | end 143 | 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_core/normalize.rb: -------------------------------------------------------------------------------- 1 | # Normalize input string 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesCore 5 | class Normalize 6 | 7 | # https://spec.commonmark.org/0.29/#line-ending 8 | NEWLINES_RE = /\r\n?|\n/ 9 | NULL_RE = /\0/ 10 | 11 | #------------------------------------------------------------------------------ 12 | def self.normalize(state) 13 | # Normalize newlines 14 | str = state.src.gsub(NEWLINES_RE, "\n") 15 | 16 | # Replace NULL characters 17 | str = str.gsub(NULL_RE, '\uFFFD') 18 | 19 | state.src = str 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_core/replacements.rb: -------------------------------------------------------------------------------- 1 | # Simple typographic replacements 2 | # 3 | # (c) (C) → © 4 | # (tm) (TM) → ™ 5 | # (r) (R) → ® 6 | # +- → ± 7 | # (p) (P) -> § 8 | # ... → … (also ?.... → ?.., !.... → !..) 9 | # ???????? → ???, !!!!! → !!!, `,,` → `,` 10 | # -- → –, --- → — 11 | #------------------------------------------------------------------------------ 12 | module MarkdownIt 13 | module RulesCore 14 | class Replacements 15 | 16 | # TODO (from original) 17 | # - fractionals 1/2, 1/4, 3/4 -> ½, ¼, ¾ 18 | # - multiplications 2 x 4 -> 2 × 4 19 | 20 | RARE_RE = /\+-|\.\.|\?\?\?\?|!!!!|,,|--/ 21 | 22 | SCOPED_ABBR_RE = /\((c|tm|r)\)/i 23 | SCOPED_ABBR = { 24 | 'c' => '©', 25 | 'r' => '®', 26 | 'tm' => '™' 27 | } 28 | 29 | #------------------------------------------------------------------------------ 30 | def self.replaceFn(match, name) 31 | return SCOPED_ABBR[name.downcase] 32 | end 33 | 34 | #------------------------------------------------------------------------------ 35 | def self.replace_scoped(inlineTokens) 36 | inside_autolink = 0 37 | 38 | (inlineTokens.length - 1).downto(0) do |i| 39 | token = inlineTokens[i] 40 | if token.type == 'text' && inside_autolink == 0 41 | token.content = token.content.gsub(SCOPED_ABBR_RE) {|match| self.replaceFn(match, $1)} 42 | end 43 | 44 | if token.type == 'link_open' && token.info == 'auto' 45 | inside_autolink -= 1 46 | end 47 | 48 | if token.type == 'link_close' && token.info == 'auto' 49 | inside_autolink += 1 50 | end 51 | end 52 | end 53 | 54 | #------------------------------------------------------------------------------ 55 | def self.replace_rare(inlineTokens) 56 | inside_autolink = 0 57 | 58 | (inlineTokens.length - 1).downto(0) do |i| 59 | token = inlineTokens[i] 60 | if token.type == 'text' && inside_autolink == 0 61 | if (RARE_RE =~ token.content) 62 | token.content = token.content. 63 | gsub(/\+-/, '±'). 64 | # .., ..., ....... -> … 65 | # but ?..... & !..... -> ?.. & !.. 66 | gsub(/\.{2,}/, '…').gsub(/([?!])…/, "\\1.."). 67 | gsub(/([?!]){4,}/, '\\1\\1\\1').gsub(/,{2,}/, ','). 68 | # em-dash 69 | gsub(/(^|[^-])---(?=[^-]|$)/m, "\\1\u2014"). 70 | # en-dash 71 | gsub(/(^|\s)--(?=\s|$)/m, "\\1\u2013"). 72 | gsub(/(^|[^-\s])--(?=[^-\s]|$)/m, "\\1\u2013") 73 | end 74 | end 75 | 76 | if token.type == 'link_open' && token.info == 'auto' 77 | inside_autolink -= 1 78 | end 79 | 80 | if token.type == 'link_close' && token.info == 'auto' 81 | inside_autolink += 1 82 | end 83 | end 84 | end 85 | 86 | 87 | #------------------------------------------------------------------------------ 88 | def self.replace(state) 89 | return if (!state.md.options[:typographer]) 90 | 91 | (state.tokens.length - 1).downto(0) do |blkIdx| 92 | next if (state.tokens[blkIdx].type != 'inline') 93 | 94 | if (SCOPED_ABBR_RE =~ state.tokens[blkIdx].content) 95 | replace_scoped(state.tokens[blkIdx].children) 96 | end 97 | 98 | if (RARE_RE =~ state.tokens[blkIdx].content) 99 | replace_rare(state.tokens[blkIdx].children) 100 | end 101 | 102 | end 103 | end 104 | 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_core/smartquotes.rb: -------------------------------------------------------------------------------- 1 | # Convert straight quotation marks to typographic ones 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesCore 5 | class Smartquotes 6 | extend Common::Utils 7 | 8 | QUOTE_TEST_RE = /['"]/ 9 | QUOTE_RE = /['"]/ 10 | APOSTROPHE = "\u2019" # ’ 11 | 12 | #------------------------------------------------------------------------------ 13 | def self.replaceAt(str, index, ch) 14 | return str[0, index] + ch + str[(index + 1)..-1] 15 | end 16 | 17 | #------------------------------------------------------------------------------ 18 | def self.process_inlines(tokens, state) 19 | stack = [] 20 | 21 | (0...tokens.length).each do |i| 22 | token = tokens[i] 23 | 24 | thisLevel = tokens[i].level 25 | 26 | j = stack.length - 1 27 | while j >= 0 28 | break if (stack[j][:level] <= thisLevel) 29 | j -= 1 30 | end 31 | 32 | # stack.length = j + 1 33 | stack = (j < stack.length ? stack.slice(0, j + 1) : stack.fill(nil, stack.length...(j+1))) 34 | 35 | next if (token.type != 'text') 36 | 37 | text = token.content 38 | pos = 0 39 | max = text.length 40 | 41 | # OUTER loop 42 | while pos < max 43 | continue_outer_loop = false 44 | t = QUOTE_RE.match(text, pos) 45 | break if t.nil? 46 | 47 | canOpen = true 48 | canClose = true 49 | pos = t.begin(0) + 1 50 | isSingle = (t[0] == "'") 51 | 52 | # Find previous character, 53 | # default to space if it's the beginning of the line 54 | # 55 | lastChar = 0x20 56 | 57 | if t.begin(0) - 1 >= 0 58 | lastChar = charCodeAt(text, t.begin(0) - 1) 59 | else 60 | (i - 1).downto(0) do |j| 61 | break if tokens[j].type == 'softbreak' || tokens[j].type == 'hardbreak' # lastChar defaults to 0x20 62 | next if tokens[j].content.empty? # should skip all tokens except 'text', 'html_inline' or 'code_inline' 63 | 64 | lastChar = charCodeAt(tokens[j].content, tokens[j].content.length - 1) 65 | break 66 | end 67 | end 68 | 69 | # Find next character, 70 | # default to space if it's the end of the line 71 | # 72 | nextChar = 0x20 73 | 74 | if pos < max 75 | nextChar = charCodeAt(text, pos) 76 | else 77 | (i + 1).upto(tokens.length - 1) do |j| 78 | break if tokens[j].type == 'softbreak' || tokens[j].type == 'hardbreak' # nextChar defaults to 0x20 79 | next if tokens[j].content.empty? # should skip all tokens except 'text', 'html_inline' or 'code_inline' 80 | 81 | nextChar = charCodeAt(tokens[j].content, 0) 82 | break 83 | end 84 | end 85 | 86 | isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(fromCodePoint(lastChar)) 87 | isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(fromCodePoint(nextChar)) 88 | 89 | isLastWhiteSpace = isWhiteSpace(lastChar) 90 | isNextWhiteSpace = isWhiteSpace(nextChar) 91 | 92 | if (isNextWhiteSpace) 93 | canOpen = false 94 | elsif (isNextPunctChar) 95 | if (!(isLastWhiteSpace || isLastPunctChar)) 96 | canOpen = false 97 | end 98 | end 99 | 100 | if (isLastWhiteSpace) 101 | canClose = false 102 | elsif (isLastPunctChar) 103 | if (!(isNextWhiteSpace || isNextPunctChar)) 104 | canClose = false 105 | end 106 | end 107 | 108 | if (nextChar == 0x22 && t[0] == '"') # " 109 | if (lastChar >= 0x30 && lastChar <= 0x39) # >= 0 && <= 9 110 | # special case: 1"" - count first quote as an inch 111 | canClose = canOpen = false 112 | end 113 | end 114 | 115 | if (canOpen && canClose) 116 | # Replace quotes in the middle of punctuation sequence, but not 117 | # in the middle of the words, i.e.: 118 | # 119 | # 1. foo " bar " baz - not replaced 120 | # 2. foo-"-bar-"-baz - replaced 121 | # 3. foo"bar"baz - not replaced 122 | # 123 | canOpen = isLastPunctChar 124 | canClose = isNextPunctChar 125 | end 126 | 127 | if (!canOpen && !canClose) 128 | # middle of word 129 | if (isSingle) 130 | token.content = replaceAt(token.content, t.begin(0), APOSTROPHE) 131 | end 132 | next 133 | end 134 | 135 | if (canClose) 136 | # this could be a closing quote, rewind the stack to get a match 137 | j = stack.length - 1 138 | while j >= 0 139 | item = stack[j] 140 | break if (stack[j][:level] < thisLevel) 141 | if (item[:single] == isSingle && stack[j][:level] == thisLevel) 142 | item = stack[j] 143 | if isSingle 144 | openQuote = state.md.options[:quotes][2] 145 | closeQuote = state.md.options[:quotes][3] 146 | else 147 | openQuote = state.md.options[:quotes][0] 148 | closeQuote = state.md.options[:quotes][1] 149 | end 150 | 151 | # replace token.content *before* tokens[item.token].content, 152 | # because, if they are pointing at the same token, replaceAt 153 | # could mess up indices when quote length != 1 154 | token.content = replaceAt(token.content, t.begin(0), closeQuote) 155 | tokens[item[:token]].content = replaceAt(tokens[item[:token]].content, item[:pos], openQuote) 156 | 157 | pos += closeQuote.length - 1 158 | pos += (openQuote.length - 1) if item[:token] == i 159 | 160 | text = token.content 161 | max = text.length 162 | 163 | stack = (j < stack.length ? stack.slice(0, j) : stack.fill(nil, stack.length...(j))) # stack.length = j 164 | continue_outer_loop = true # continue OUTER; 165 | break 166 | end 167 | j -= 1 168 | end 169 | end 170 | next if continue_outer_loop 171 | 172 | if (canOpen) 173 | stack.push({ 174 | token: i, 175 | pos: t.begin(0), 176 | single: isSingle, 177 | level: thisLevel 178 | }) 179 | elsif (canClose && isSingle) 180 | token.content = replaceAt(token.content, t.begin(0), APOSTROPHE) 181 | end 182 | end 183 | end 184 | end 185 | 186 | 187 | #------------------------------------------------------------------------------ 188 | def self.smartquotes(state) 189 | return if (!state.md.options[:typographer]) 190 | 191 | blkIdx = state.tokens.length - 1 192 | while blkIdx >= 0 193 | if (state.tokens[blkIdx].type != 'inline' || !(QUOTE_TEST_RE =~ state.tokens[blkIdx].content)) 194 | blkIdx -= 1 195 | next 196 | end 197 | 198 | process_inlines(state.tokens[blkIdx].children, state) 199 | blkIdx -= 1 200 | end 201 | end 202 | 203 | end 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_core/state_core.rb: -------------------------------------------------------------------------------- 1 | # Core state object 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesCore 5 | class StateCore 6 | 7 | attr_accessor :src, :env, :tokens, :inlineMode, :md 8 | 9 | #------------------------------------------------------------------------------ 10 | def initialize(src, md, env) 11 | @src = src 12 | @env = env 13 | @tokens = [] 14 | @inlineMode = false 15 | @md = md # link to parser instance 16 | end 17 | 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_core/text_join.rb: -------------------------------------------------------------------------------- 1 | # Join raw text tokens with the rest of the text 2 | # 3 | # This is set as a separate rule to provide an opportunity for plugins 4 | # to run text replacements after text join, but before escape join. 5 | # 6 | # For example, `\:)` shouldn't be replaced with an emoji. 7 | # 8 | module MarkdownIt 9 | module RulesCore 10 | class TextJoin 11 | def self.text_join(state) 12 | blockTokens = state.tokens 13 | 14 | (0...blockTokens.length).each do |j| 15 | next if (blockTokens[j].type != 'inline') 16 | 17 | tokens = blockTokens[j].children 18 | max = tokens.length 19 | 20 | (0...max).each do |curr| 21 | if (tokens[curr].type == 'text_special') 22 | tokens[curr].type = 'text' 23 | end 24 | end 25 | 26 | last = 0 27 | curr = 0 28 | while curr < max 29 | if (tokens[curr].type == 'text' && 30 | curr + 1 < max && 31 | tokens[curr + 1].type == 'text') 32 | 33 | # collapse two adjacent text nodes 34 | tokens[curr + 1].content = tokens[curr].content + tokens[curr + 1].content 35 | else 36 | tokens[last] = tokens[curr] if (curr != last) 37 | 38 | last += 1 39 | end 40 | 41 | curr += 1 42 | end 43 | 44 | if (curr != last) 45 | tokens.pop(tokens.length - last) 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/autolink.rb: -------------------------------------------------------------------------------- 1 | # Process autolinks '' 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class Autolink 6 | extend Common::Utils 7 | 8 | EMAIL_RE = /^([a-zA-Z0-9.!#$\%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)$/ 9 | AUTOLINK_RE = /^([a-zA-Z][a-zA-Z0-9+.\-]{1,31}):([^<>\x00-\x20]*)$/ 10 | 11 | #------------------------------------------------------------------------------ 12 | def self.autolink(state, silent) 13 | pos = state.pos 14 | 15 | return false if (charCodeAt(state.src, pos) != 0x3C) # < 16 | 17 | start = state.pos 18 | max = state.posMax 19 | 20 | loop do 21 | return false if ((pos += 1) >= max) 22 | 23 | ch = charCodeAt(state.src, pos) 24 | 25 | return false if (ch == 0x3C) # < 26 | break if (ch == 0x3E) # > 27 | end 28 | 29 | url = state.src.slice((start + 1)...pos) 30 | 31 | if (AUTOLINK_RE =~ url) 32 | fullUrl = state.md.normalizeLink.call(url) 33 | return false if (!state.md.validateLink.call(fullUrl)) 34 | 35 | if (!silent) 36 | token = state.push('link_open', 'a', 1) 37 | token.attrs = [ [ 'href', fullUrl ] ] 38 | token.markup = 'autolink' 39 | token.info = 'auto' 40 | 41 | token = state.push('text', '', 0) 42 | token.content = state.md.normalizeLinkText.call(url) 43 | 44 | token = state.push('link_close', 'a', -1) 45 | token.markup = 'autolink' 46 | token.info = 'auto' 47 | end 48 | 49 | state.pos += url.length + 2 50 | return true 51 | end 52 | 53 | if (EMAIL_RE =~ url) 54 | fullUrl = state.md.normalizeLink.call('mailto:' + url) 55 | return false if (!state.md.validateLink.call(fullUrl)) 56 | 57 | if (!silent) 58 | token = state.push('link_open', 'a', 1) 59 | token.attrs = [ [ 'href', fullUrl ] ] 60 | token.markup = 'autolink' 61 | token.info = 'auto' 62 | 63 | token = state.push('text', '', 0) 64 | token.content = state.md.normalizeLinkText.call(url) 65 | 66 | token = state.push('link_close', 'a', -1) 67 | token.markup = 'autolink' 68 | token.info = 'auto' 69 | end 70 | 71 | state.pos += url.length + 2 72 | return true 73 | end 74 | 75 | return false 76 | end 77 | 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/backticks.rb: -------------------------------------------------------------------------------- 1 | # Parse backticks 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class Backticks 6 | extend Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def self.backtick(state, silent) 10 | pos = state.pos 11 | ch = charCodeAt(state.src, pos) 12 | 13 | return false if (ch != 0x60) # ` 14 | 15 | start = pos 16 | pos += 1 17 | max = state.posMax 18 | 19 | # scan marker length 20 | while (pos < max && charCodeAt(state.src, pos) == 0x60) # ` 21 | pos += 1 22 | end 23 | 24 | marker = state.src.slice(start...pos) 25 | openerLength = marker.length 26 | 27 | if (state.backticksScanned && (state.backticks[openerLength] || 0) <= start) 28 | state.pending += marker if (!silent) 29 | state.pos += openerLength 30 | return true 31 | end 32 | 33 | matchStart = matchEnd = pos 34 | 35 | # Nothing found in the cache, scan until the end of the line (or until marker is found) 36 | while ((matchStart = state.src.index('`', matchEnd)) != nil) 37 | matchEnd = matchStart + 1 38 | 39 | # scan marker length 40 | while (matchEnd < max && charCodeAt(state.src, matchEnd) == 0x60) # ` 41 | matchEnd += 1 42 | end 43 | 44 | closerLength = matchEnd - matchStart 45 | 46 | if (closerLength == openerLength) 47 | # Found matching closer length. 48 | if (!silent) 49 | token = state.push('code_inline', 'code', 0) 50 | token.markup = marker 51 | token.content = state.src.slice(pos...matchStart) 52 | .gsub(/\n/, ' ') 53 | .gsub(/^ (.+) $/, '\1') 54 | end 55 | state.pos = matchEnd 56 | return true 57 | end 58 | 59 | # Some different length found, put it in cache as upper limit of where closer can be found 60 | state.backticks[closerLength] = matchStart 61 | end 62 | 63 | # Scanned through the end, didn't find anything 64 | state.backticksScanned = true 65 | 66 | state.pending += marker if (!silent) 67 | state.pos += openerLength 68 | return true 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/balance_pairs.rb: -------------------------------------------------------------------------------- 1 | # For each opening emphasis-like marker find a matching closing one 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class BalancePairs 6 | 7 | #------------------------------------------------------------------------------ 8 | def self.processDelimiters(state, delimiters) 9 | openersBottom = {} 10 | max = delimiters.length 11 | 12 | return if (!max) 13 | 14 | # headerIdx is the first delimiter of the current (where closer is) delimiter run 15 | headerIdx = 0 16 | lastTokenIdx = -2 # needs any value lower than -1 17 | jumps = [] 18 | 19 | 0.upto(max - 1) do |closerIdx| 20 | closer = delimiters[closerIdx] 21 | 22 | jumps.push(0) 23 | 24 | # markers belong to same delimiter run if: 25 | # - they have adjacent tokens 26 | # - AND markers are the same 27 | # 28 | if (delimiters[headerIdx][:marker] != closer[:marker] || lastTokenIdx != closer[:token] - 1) 29 | headerIdx = closerIdx 30 | end 31 | 32 | lastTokenIdx = closer[:token] 33 | 34 | # Length is only used for emphasis-specific "rule of 3", 35 | # if it's not defined (in strikethrough or 3rd party plugins), 36 | # we can default it to 0 to disable those checks. 37 | # 38 | closer[:length] = closer[:length] || 0 39 | 40 | next if (!closer[:close]) 41 | 42 | # Previously calculated lower bounds (previous fails) 43 | # for each marker, each delimiter length modulo 3, 44 | # and for whether this closer can be an opener; 45 | # https://github.com/commonmark/cmark/commit/34250e12ccebdc6372b8b49c44fab57c72443460 46 | unless openersBottom[closer[:marker]] 47 | openersBottom[closer[:marker]] = [ -1, -1, -1, -1, -1, -1 ] 48 | end 49 | 50 | minOpenerIdx = openersBottom[closer[:marker]][(closer[:open] ? 3 : 0) + (closer[:length] % 3)] 51 | 52 | openerIdx = headerIdx - jumps[headerIdx] - 1 53 | 54 | newMinOpenerIdx = openerIdx 55 | 56 | while openerIdx > minOpenerIdx 57 | opener = delimiters[openerIdx] 58 | 59 | (openerIdx -= jumps[openerIdx] + 1) && next if (opener[:marker] != closer[:marker]) 60 | 61 | if (opener[:open] && opener[:end] < 0) 62 | 63 | isOddMatch = false 64 | 65 | # from spec: 66 | # 67 | # If one of the delimiters can both open and close emphasis, then the 68 | # sum of the lengths of the delimiter runs containing the opening and 69 | # closing delimiters must not be a multiple of 3 unless both lengths 70 | # are multiples of 3. 71 | # 72 | if (opener[:close] || closer[:open]) 73 | if ((opener[:length] + closer[:length]) % 3 == 0) 74 | if (opener[:length] % 3 != 0 || closer[:length] % 3 != 0) 75 | isOddMatch = true 76 | end 77 | end 78 | end 79 | 80 | if (!isOddMatch) 81 | # If previous delimiter cannot be an opener, we can safely skip 82 | # the entire sequence in future checks. This is required to make 83 | # sure algorithm has linear complexity (see *_*_*_*_*_... case). 84 | # 85 | lastJump = openerIdx > 0 && !delimiters[openerIdx - 1][:open] ? 86 | jumps[openerIdx - 1] + 1 : 0 87 | 88 | jumps[closerIdx] = closerIdx - openerIdx + lastJump 89 | jumps[openerIdx] = lastJump 90 | 91 | closer[:open] = false 92 | opener[:end] = closerIdx 93 | opener[:close] = false 94 | newMinOpenerIdx = -1 95 | # treat next token as start of run, 96 | # it optimizes skips in **<...>**a**<...>** pathological case 97 | lastTokenIdx = -2 98 | break 99 | end 100 | end 101 | 102 | openerIdx -= jumps[openerIdx] + 1 103 | end 104 | 105 | if (newMinOpenerIdx != -1) 106 | # If match for this delimiter run failed, we want to set lower bound for 107 | # future lookups. This is required to make sure algorithm has linear 108 | # complexity. 109 | # 110 | # See details here: 111 | # https://github.com/commonmark/cmark/issues/178#issuecomment-270417442 112 | # 113 | openersBottom[closer[:marker]][(closer[:open] ? 3 : 0) + ((closer[:length] || 0) % 3)] = newMinOpenerIdx 114 | end 115 | end 116 | end 117 | 118 | #------------------------------------------------------------------------------ 119 | def self.link_pairs(state) 120 | tokens_meta = state.tokens_meta 121 | max = state.tokens_meta.length 122 | 123 | processDelimiters(state, state.delimiters) 124 | 125 | 0.upto(max - 1) do |curr| 126 | if (tokens_meta[curr] && tokens_meta[curr][:delimiters]) 127 | processDelimiters(state, tokens_meta[curr][:delimiters]) 128 | end 129 | end 130 | end 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/emphasis.rb: -------------------------------------------------------------------------------- 1 | # Process *this* and _that_ 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class Emphasis 6 | extend MarkdownIt::Common::Utils 7 | 8 | # Insert each marker as a separate text token, and add it to delimiter list 9 | # 10 | def self.tokenize(state, silent) 11 | start = state.pos 12 | marker = charCodeAt(state.src, start) 13 | 14 | return false if silent 15 | 16 | return false if (marker != 0x5F && marker != 0x2A) # _ and * 17 | 18 | scanned = state.scanDelims(state.pos, marker == 0x2A) 19 | 20 | 0.upto(scanned[:length] - 1) do |i| 21 | token = state.push('text', '', 0) 22 | token.content = fromCodePoint(marker) 23 | 24 | state.delimiters.push({ 25 | # Char code of the starting marker (number). 26 | # 27 | marker: marker, 28 | 29 | # Total length of these series of delimiters. 30 | # 31 | length: scanned[:length], 32 | 33 | # A position of the token this delimiter corresponds to. 34 | # 35 | token: state.tokens.length - 1, 36 | 37 | # If this delimiter is matched as a valid opener, `end` will be 38 | # equal to its position, otherwise it's `-1`. 39 | # 40 | end: -1, 41 | 42 | # Boolean flags that determine if this delimiter could open or close 43 | # an emphasis. 44 | # 45 | open: scanned[:can_open], 46 | close: scanned[:can_close] 47 | }) 48 | end 49 | 50 | state.pos += scanned[:length] 51 | 52 | return true 53 | end 54 | 55 | def self.private_postProcess(state, delimiters) 56 | max = delimiters.length 57 | 58 | i = max - 1 59 | while i >= 0 60 | startDelim = delimiters[i] 61 | 62 | (i -= 1) and next if startDelim[:marker] != 0x5F && startDelim[:marker] != 0x2A # _ and * 63 | 64 | # Process only opening markers 65 | (i -= 1) and next if startDelim[:end] == -1 66 | 67 | endDelim = delimiters[startDelim[:end]] 68 | 69 | # If the previous delimiter has the same marker and is adjacent to this one, 70 | # merge those into one strong delimiter. 71 | # 72 | # `whatever` -> `whatever` 73 | # 74 | isStrong = i > 0 && 75 | delimiters[i - 1][:end] == startDelim[:end] + 1 && 76 | # check that first two markers match and adjacent 77 | delimiters[i - 1][:marker] == startDelim[:marker] && 78 | delimiters[i - 1][:token] == startDelim[:token] - 1 && 79 | # check that last two markers are adjacent (we can safely assume they match) 80 | delimiters[startDelim[:end] + 1][:token] == endDelim[:token] + 1 81 | 82 | ch = fromCodePoint(startDelim[:marker]) 83 | 84 | token = state.tokens[startDelim[:token]] 85 | token.type = isStrong ? 'strong_open' : 'em_open' 86 | token.tag = isStrong ? 'strong' : 'em' 87 | token.nesting = 1 88 | token.markup = isStrong ? ch + ch : ch 89 | token.content = '' 90 | 91 | token = state.tokens[endDelim[:token]] 92 | token.type = isStrong ? 'strong_close' : 'em_close' 93 | token.tag = isStrong ? 'strong' : 'em' 94 | token.nesting = -1 95 | token.markup = isStrong ? ch + ch : ch 96 | token.content = '' 97 | 98 | if isStrong 99 | state.tokens[delimiters[i - 1][:token]].content = '' 100 | state.tokens[delimiters[startDelim[:end] + 1][:token]].content = '' 101 | i -= 1 102 | end 103 | 104 | i -= 1 105 | end 106 | end 107 | 108 | # Walk through delimiter list and replace text tokens with tags 109 | # 110 | def self.postProcess(state) 111 | tokens_meta = state.tokens_meta 112 | max = state.tokens_meta.length 113 | 114 | private_postProcess(state, state.delimiters) 115 | 116 | 0.upto(max - 1) do |curr| 117 | if (tokens_meta[curr] && tokens_meta[curr][:delimiters]) 118 | private_postProcess(state, tokens_meta[curr][:delimiters]) 119 | end 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/entity.rb: -------------------------------------------------------------------------------- 1 | # Process html entity - {, ¯, ", ... 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class Entity 6 | extend Common::Utils 7 | 8 | DIGITAL_RE = /^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i 9 | NAMED_RE = /^&([a-z][a-z0-9]{1,31});/i 10 | 11 | #------------------------------------------------------------------------------ 12 | def self.entity(state, silent) 13 | pos = state.pos 14 | max = state.posMax 15 | 16 | return false if charCodeAt(state.src, pos) != 0x26 # & 17 | 18 | return false if pos + 1 >= max 19 | 20 | ch = charCodeAt(state.src, pos + 1) 21 | 22 | if ch == 0x23 # '#' 23 | match = state.src[pos..-1].match(DIGITAL_RE) 24 | if match 25 | if !silent 26 | code = match[1][0].downcase == 'x' ? match[1][1..-1].to_i(16) : match[1].to_i 27 | 28 | token = state.push('text_special', '', 0) 29 | token.content = isValidEntityCode(code) ? fromCodePoint(code) : fromCodePoint(0xFFFD) 30 | token.markup = match[0] 31 | token.info = 'entity' 32 | end 33 | state.pos += match[0].length 34 | return true 35 | end 36 | else 37 | match = state.src[pos..-1].match(NAMED_RE) 38 | if match 39 | if MarkdownIt::HTMLEntities::MAPPINGS[match[1]] 40 | if !silent 41 | token = state.push('text_special', '', 0) 42 | token.content += fromCodePoint(MarkdownIt::HTMLEntities::MAPPINGS[match[1]]) 43 | token.markup = match[0] 44 | token.info = 'entity' 45 | end 46 | state.pos += match[0].length 47 | return true 48 | end 49 | end 50 | end 51 | 52 | return false 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/escape.rb: -------------------------------------------------------------------------------- 1 | # Process escaped chars and hardbreaks 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class Escape 6 | extend Common::Utils 7 | 8 | ESCAPED = [] 9 | 10 | 0.upto(255) { |i| ESCAPED.push(0) } 11 | 12 | '\\!"#$%&\'()*+,./:;<=>?@[]^_`{|}~-'.split('').each { |ch| ESCAPED[ch.ord] = 1 } 13 | 14 | #------------------------------------------------------------------------------ 15 | def self.escape(state, silent) 16 | pos = state.pos 17 | max = state.posMax 18 | 19 | return false if charCodeAt(state.src, pos) != 0x5C # \ 20 | 21 | pos += 1 22 | 23 | # '\' at the end of the inline block 24 | return false if pos >= max 25 | 26 | ch1 = charCodeAt(state.src, pos) 27 | 28 | if ch1 == 0x0A 29 | if !silent 30 | state.push('hardbreak', 'br', 0) 31 | end 32 | 33 | pos += 1 34 | # skip leading whitespaces from next line 35 | while pos < max 36 | ch1 = charCodeAt(state.src, pos) 37 | break if !isSpace(ch1) 38 | pos += 1 39 | end 40 | 41 | state.pos = pos 42 | return true 43 | end 44 | 45 | escapedStr = state.src[pos] 46 | 47 | if (ch1 >= 0xD800 && ch1 <= 0xDBFF && pos + 1 < max) 48 | ch2 = charCodeAt(state.src, pos + 1) 49 | 50 | if (ch2 >= 0xDC00 && ch2 <= 0xDFFF) 51 | escapedStr += state.src[pos + 1] 52 | pos += 1 53 | end 54 | end 55 | 56 | origStr = '\\' + escapedStr 57 | 58 | if (!silent) 59 | token = state.push('text_special', '', 0) 60 | 61 | if ch1 < 256 && ESCAPED[ch1] != 0 62 | token.content = escapedStr 63 | else 64 | token.content = origStr 65 | end 66 | 67 | token.markup = origStr 68 | token.info = 'escape' 69 | end 70 | 71 | state.pos = pos + 1 72 | return true 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/fragments_join.rb: -------------------------------------------------------------------------------- 1 | # Clean up tokens after emphasis and strikethrough postprocessing: 2 | # merge adjacent text nodes into one and re-calculate all token levels 3 | # 4 | # This is necessary because initially emphasis delimiter markers (*, _, ~) 5 | # are treated as their own separate text tokens. Then emphasis rule either 6 | # leaves them as text (needed to merge with adjacent text) or turns them 7 | # into opening/closing tags (which messes up levels inside). 8 | # 9 | module MarkdownIt 10 | module RulesInline 11 | class FragmentsJoin 12 | extend Common::Utils 13 | 14 | def self.fragments_join(state) 15 | level = 0 16 | tokens = state.tokens 17 | max = state.tokens.length 18 | 19 | last = 0 20 | curr = 0 21 | while curr < max 22 | # re-calculate levels after emphasis/strikethrough turns some text nodes 23 | # into opening/closing tags 24 | level -= 1 if (tokens[curr].nesting < 0) # closing tag 25 | tokens[curr].level = level 26 | level +=1 if (tokens[curr].nesting > 0) # opening tag 27 | 28 | if (tokens[curr].type == 'text' && 29 | curr + 1 < max && 30 | tokens[curr + 1].type == 'text') 31 | 32 | # collapse two adjacent text nodes 33 | tokens[curr + 1].content = tokens[curr].content + tokens[curr + 1].content 34 | else 35 | tokens[last] = tokens[curr] if (curr != last) 36 | 37 | last += 1 38 | end 39 | 40 | curr += 1 41 | end 42 | 43 | if (curr != last) 44 | tokens.pop(tokens.length - last) 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/html_inline.rb: -------------------------------------------------------------------------------- 1 | # Process html tags 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class HtmlInline 6 | extend Common::Utils 7 | include MarkdownIt::Common::HtmlRe 8 | 9 | #------------------------------------------------------------------------------ 10 | def self.isLinkOpen(str) 11 | return !(/^\s]/i =~ str).nil? 12 | end 13 | def self.isLinkClose(str) 14 | return !(/^<\/a\s*>/i =~ str).nil? 15 | end 16 | 17 | #------------------------------------------------------------------------------ 18 | def self.isLetter(ch) 19 | lc = ch | 0x20 # to lower case 20 | return (lc >= 0x61) && (lc <= 0x7a) # >= a && <= z 21 | end 22 | 23 | #------------------------------------------------------------------------------ 24 | def self.html_inline(state, silent) 25 | pos = state.pos 26 | 27 | return false if !state.md.options[:html] 28 | 29 | # Check start 30 | max = state.posMax 31 | if (charCodeAt(state.src, pos) != 0x3C || pos + 2 >= max) # < 32 | return false 33 | end 34 | 35 | # Quick fail on second char 36 | ch = charCodeAt(state.src, pos + 1) 37 | if (ch != 0x21 && # ! 38 | ch != 0x3F && # ? 39 | ch != 0x2F && # / 40 | !isLetter(ch)) 41 | return false 42 | end 43 | 44 | match = state.src[pos..-1].match(HTML_TAG_RE) 45 | return false if !match 46 | 47 | if !silent 48 | token = state.push('html_inline', '', 0) 49 | token.content = state.src.slice(pos...(pos + match[0].length)) 50 | 51 | state.linkLevel += 1 if (isLinkOpen(token.content)) 52 | state.linkLevel -= 1 if (isLinkClose(token.content)) 53 | end 54 | state.pos += match[0].length 55 | return true 56 | end 57 | 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/image.rb: -------------------------------------------------------------------------------- 1 | # Process ![image]( "title") 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class Image 6 | extend Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def self.image(state, silent) 10 | href = '' 11 | oldPos = state.pos 12 | max = state.posMax 13 | 14 | return false if (charCodeAt(state.src, state.pos) != 0x21) # ! 15 | return false if (charCodeAt(state.src, state.pos + 1) != 0x5B) # [ 16 | 17 | labelStart = state.pos + 2 18 | labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false) 19 | 20 | # parser failed to find ']', so it's not a valid link 21 | return false if (labelEnd < 0) 22 | 23 | pos = labelEnd + 1 24 | if (pos < max && charCodeAt(state.src, pos) == 0x28) # ( 25 | # 26 | # Inline link 27 | # 28 | 29 | # [link]( "title" ) 30 | # ^^ skipping these spaces 31 | pos += 1 32 | while pos < max 33 | code = charCodeAt(state.src, pos) 34 | break if (!isSpace(code) && code != 0x0A) 35 | pos += 1 36 | end 37 | return false if (pos >= max) 38 | 39 | # [link]( "title" ) 40 | # ^^^^^^ parsing link destination 41 | start = pos 42 | res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax) 43 | if (res[:ok]) 44 | href = state.md.normalizeLink.call(res[:str]) 45 | if (state.md.validateLink.call(href)) 46 | pos = res[:pos] 47 | else 48 | href = '' 49 | end 50 | end 51 | 52 | # [link]( "title" ) 53 | # ^^ skipping these spaces 54 | start = pos 55 | while pos < max 56 | code = charCodeAt(state.src, pos) 57 | break if (!isSpace(code) && code != 0x0A) 58 | pos += 1 59 | end 60 | 61 | # [link]( "title" ) 62 | # ^^^^^^^ parsing link title 63 | res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax) 64 | if (pos < max && start != pos && res[:ok]) 65 | title = res[:str] 66 | pos = res[:pos] 67 | 68 | # [link]( "title" ) 69 | # ^^ skipping these spaces 70 | while pos < max 71 | code = charCodeAt(state.src, pos); 72 | break if (!isSpace(code) && code != 0x0A) 73 | pos += 1 74 | end 75 | else 76 | title = '' 77 | end 78 | 79 | if (pos >= max || charCodeAt(state.src, pos) != 0x29) # ) 80 | state.pos = oldPos 81 | return false 82 | end 83 | pos += 1 84 | else 85 | # 86 | # Link reference 87 | # 88 | return false if state.env[:references].nil? 89 | 90 | if (pos < max && charCodeAt(state.src, pos) == 0x5B) # [ 91 | start = pos + 1 92 | pos = state.md.helpers.parseLinkLabel(state, pos) 93 | if (pos >= 0) 94 | label = state.src.slice(start...pos) 95 | pos += 1 96 | else 97 | pos = labelEnd + 1 98 | end 99 | else 100 | pos = labelEnd + 1 101 | end 102 | 103 | # covers label === '' and label === undefined 104 | # (collapsed reference link and shortcut reference link respectively) 105 | label = state.src.slice(labelStart...labelEnd) if label.nil? || label.empty? 106 | 107 | ref = state.env[:references][normalizeReference(label)] 108 | if (!ref) 109 | state.pos = oldPos 110 | return false 111 | end 112 | href = ref[:href] 113 | title = ref[:title] 114 | end 115 | 116 | # 117 | # We found the end of the link, and know for a fact it's a valid link; 118 | # so all that's left to do is to call tokenizer. 119 | # 120 | if (!silent) 121 | content = state.src.slice(labelStart...labelEnd) 122 | 123 | state.md.inline.parse( 124 | content, 125 | state.md, 126 | state.env, 127 | tokens = [] 128 | ) 129 | 130 | token = state.push('image', 'img', 0) 131 | token.attrs = attrs = [ [ 'src', href ], [ 'alt', '' ] ] 132 | token.children = tokens 133 | token.content = content; 134 | 135 | unless (title.nil? || title.empty?) 136 | attrs.push([ 'title', title ]) 137 | end 138 | end 139 | 140 | state.pos = pos 141 | state.posMax = max 142 | return true 143 | end 144 | 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/link.rb: -------------------------------------------------------------------------------- 1 | # Process [link]( "stuff") 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class Link 6 | extend Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def self.link(state, silent) 10 | href = '' 11 | title = '' 12 | oldPos = state.pos 13 | max = state.posMax 14 | start = state.pos 15 | parseReference = true 16 | 17 | return false if (charCodeAt(state.src, state.pos) != 0x5B) # [ 18 | 19 | labelStart = state.pos + 1 20 | labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, true) 21 | 22 | # parser failed to find ']', so it's not a valid link 23 | return false if (labelEnd < 0) 24 | 25 | pos = labelEnd + 1 26 | if (pos < max && charCodeAt(state.src, pos) == 0x28) # ( 27 | # 28 | # Inline link 29 | # 30 | 31 | # might have found a valid shortcut link, disable reference parsing 32 | parseReference = false 33 | 34 | # [link]( "title" ) 35 | # ^^ skipping these spaces 36 | pos += 1 37 | while pos < max 38 | code = charCodeAt(state.src, pos) 39 | break if (!isSpace(code) && code != 0x0A) 40 | pos += 1 41 | end 42 | return false if (pos >= max) 43 | 44 | # [link]( "title" ) 45 | # ^^^^^^ parsing link destination 46 | start = pos 47 | res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax) 48 | if (res[:ok]) 49 | href = state.md.normalizeLink.call(res[:str]) 50 | if (state.md.validateLink.call(href)) 51 | pos = res[:pos] 52 | else 53 | href = '' 54 | end 55 | end 56 | 57 | # [link]( "title" ) 58 | # ^^ skipping these spaces 59 | start = pos 60 | while pos < max 61 | code = charCodeAt(state.src, pos) 62 | break if (!isSpace(code) && code != 0x0A) 63 | pos += 1 64 | end 65 | 66 | # [link]( "title" ) 67 | # ^^^^^^^ parsing link title 68 | res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax) 69 | if (pos < max && start != pos && res[:ok]) 70 | title = res[:str] 71 | pos = res[:pos] 72 | 73 | # [link]( "title" ) 74 | # ^^ skipping these spaces 75 | while pos < max 76 | code = charCodeAt(state.src, pos) 77 | break if (!isSpace(code) && code != 0x0A) 78 | pos += 1 79 | end 80 | end 81 | 82 | if (pos >= max || charCodeAt(state.src, pos) != 0x29) # ) 83 | # parsing a valid shortcut link failed, fallback to reference 84 | parseReference = true 85 | end 86 | pos += 1 87 | end 88 | 89 | if parseReference 90 | # 91 | # Link reference 92 | # 93 | return false if state.env[:references].nil? 94 | 95 | if (pos < max && charCodeAt(state.src, pos) == 0x5B) # [ 96 | start = pos + 1 97 | pos = state.md.helpers.parseLinkLabel(state, pos) 98 | if (pos >= 0) 99 | label = state.src.slice(start...pos) 100 | pos += 1 101 | else 102 | pos = labelEnd + 1 103 | end 104 | else 105 | pos = labelEnd + 1 106 | end 107 | 108 | # covers label === '' and label === undefined 109 | # (collapsed reference link and shortcut reference link respectively) 110 | label = state.src.slice(labelStart...labelEnd) if label.nil? || label.empty? 111 | 112 | ref = state.env[:references][normalizeReference(label)] 113 | if (!ref) 114 | state.pos = oldPos 115 | return false 116 | end 117 | href = ref[:href] 118 | title = ref[:title] 119 | end 120 | 121 | # 122 | # We found the end of the link, and know for a fact it's a valid link; 123 | # so all that's left to do is to call tokenizer. 124 | # 125 | if (!silent) 126 | state.pos = labelStart 127 | state.posMax = labelEnd 128 | 129 | token = state.push('link_open', 'a', 1) 130 | token.attrs = attrs = [ [ 'href', href ] ] 131 | unless title.nil? || title.empty? 132 | attrs.push([ 'title', title ]) 133 | end 134 | 135 | state.linkLevel += 1 136 | state.md.inline.tokenize(state) 137 | state.linkLevel -= 1 138 | 139 | token = state.push('link_close', 'a', -1) 140 | end 141 | 142 | state.pos = pos 143 | state.posMax = max 144 | return true 145 | end 146 | 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/linkify.rb: -------------------------------------------------------------------------------- 1 | # Process links like https://example.org/ 2 | module MarkdownIt 3 | module RulesInline 4 | class Linkify 5 | extend Common::Utils 6 | 7 | # RFC3986: scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) 8 | SCHEME_RE = /(?:^|[^a-z0-9.+-])([a-z][a-z0-9.+-]*)$/i 9 | 10 | #------------------------------------------------------------------------------ 11 | def self.linkify(state, silent) 12 | return false if (!state.md.options[:linkify]) 13 | return false if (state.linkLevel > 0) 14 | 15 | pos = state.pos 16 | max = state.posMax 17 | 18 | return false if (pos + 3 > max) 19 | return false if (charCodeAt(state.src, pos) != 0x3A) # : 20 | return false if (charCodeAt(state.src, pos + 1) != 0x2F) # / 21 | return false if (charCodeAt(state.src, pos + 2) != 0x2F) # / 22 | 23 | match = state.pending.match(SCHEME_RE) 24 | return false if (!match) 25 | 26 | proto = match[1] 27 | 28 | link = state.md.linkify.matchAtStart(state.src.slice((pos - proto.length)..-1)) 29 | return false if (!link) 30 | 31 | url = link.url 32 | 33 | # disallow '*' at the end of the link (conflicts with emphasis) 34 | url = url.sub(/\*+$/, '') 35 | 36 | fullUrl = state.md.normalizeLink.call(url) 37 | return false if (!state.md.validateLink.call(fullUrl)) 38 | 39 | if (!silent) 40 | state.pending = state.pending[0...-proto.length] 41 | 42 | token = state.push('link_open', 'a', 1) 43 | token.attrs = [ [ 'href', fullUrl ] ] 44 | token.markup = 'linkify' 45 | token.info = 'auto' 46 | 47 | token = state.push('text', '', 0) 48 | token.content = state.md.normalizeLinkText.call(url) 49 | 50 | token = state.push('link_close', 'a', -1) 51 | token.markup = 'linkify' 52 | token.info = 'auto' 53 | end 54 | 55 | state.pos += url.length - proto.length 56 | return true 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/newline.rb: -------------------------------------------------------------------------------- 1 | # Proceess '\n' 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class Newline 6 | extend Common::Utils 7 | 8 | #------------------------------------------------------------------------------ 9 | def self.newline(state, silent) 10 | pos = state.pos 11 | return false if charCodeAt(state.src, pos) != 0x0A # \n 12 | 13 | pmax = state.pending.length - 1 14 | max = state.posMax 15 | 16 | # ' \n' -> hardbreak 17 | # Lookup in pending chars is bad practice! Don't copy to other rules! 18 | # Pending string is stored in concat mode, indexed lookups will cause 19 | # convertion to flat mode. 20 | if !silent 21 | if pmax >= 0 && charCodeAt(state.pending, pmax) == 0x20 22 | if pmax >= 1 && charCodeAt(state.pending, pmax - 1) == 0x20 23 | # Find whitespaces tail of pending chars. 24 | ws = pmax - 1 25 | while (ws >= 1 && charCodeAt(state.pending, ws - 1) == 0x20) 26 | ws -= 1 27 | end 28 | 29 | state.pending = state.pending.slice(0...ws) 30 | state.push('hardbreak', 'br', 0) 31 | else 32 | state.pending = state.pending.slice(0...-1) 33 | state.push('softbreak', 'br', 0) 34 | end 35 | 36 | else 37 | state.push('softbreak', 'br', 0) 38 | end 39 | end 40 | 41 | pos += 1 42 | 43 | # skip heading spaces for next line 44 | while pos < max && isSpace(charCodeAt(state.src, pos)) 45 | pos += 1 46 | end 47 | 48 | state.pos = pos 49 | return true 50 | end 51 | 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/state_inline.rb: -------------------------------------------------------------------------------- 1 | # Inline parser state 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class StateInline 6 | include MarkdownIt::Common::Utils 7 | 8 | attr_accessor :src, :env, :md, :tokens, :pos, :posMax, :level, :tokens_meta 9 | attr_accessor :pending, :pendingLevel, :cache, :delimiters 10 | attr_accessor :backticks, :backticksScanned, :linkLevel 11 | 12 | #------------------------------------------------------------------------------ 13 | def initialize(src, md, env, outTokens) 14 | @src = src 15 | @env = env 16 | @md = md 17 | @tokens = outTokens 18 | @tokens_meta = Array.new(outTokens.length) 19 | 20 | @pos = 0 21 | @posMax = @src.length 22 | @level = 0 23 | @pending = '' 24 | @pendingLevel = 0 25 | 26 | # Stores { start: end } pairs. Useful for backtrack 27 | # optimization of pairs parse (emphasis, strikes). 28 | @cache = {} 29 | 30 | # List of emphasis-like delimiters for current tag 31 | @delimiters = [] 32 | 33 | # Stack of delimiter lists for upper level tags 34 | @_prev_delimiters = []; 35 | 36 | # backtick length => last seen position 37 | @backticks = {} 38 | @backticksScanned = false 39 | 40 | # Counter used to disable inline linkify-it execution 41 | # inside and markdown links 42 | @linkLevel = 0 43 | end 44 | 45 | 46 | # Flush pending text 47 | #------------------------------------------------------------------------------ 48 | def pushPending 49 | token = Token.new('text', '', 0) 50 | token.content = @pending 51 | token.level = @pendingLevel 52 | @tokens.push(token) 53 | @pending = '' 54 | return token 55 | end 56 | 57 | # Push new token to "stream". 58 | # If pending text exists - flush it as text token 59 | #------------------------------------------------------------------------------ 60 | def push(type, tag, nesting) 61 | pushPending unless @pending.empty? 62 | 63 | token = Token.new(type, tag, nesting) 64 | token_meta = nil 65 | 66 | if nesting < 0 67 | # closing tag 68 | @level -= 1 69 | @delimiters = @_prev_delimiters.pop 70 | end 71 | 72 | token.level = @level 73 | 74 | if nesting > 0 75 | # opening tag 76 | @level += 1 77 | @_prev_delimiters.push(@delimiters) 78 | @delimiters = [] 79 | token_meta = { delimiters: @delimiters } 80 | end 81 | 82 | @pendingLevel = @level 83 | @tokens.push(token) 84 | @tokens_meta.push(token_meta) 85 | 86 | return token 87 | end 88 | 89 | # Scan a sequence of emphasis-like markers, and determine whether 90 | # it can start an emphasis sequence or end an emphasis sequence. 91 | # 92 | # - start - position to scan from (it should point at a valid marker); 93 | # - canSplitWord - determine if these markers can be found inside a word 94 | #------------------------------------------------------------------------------ 95 | def scanDelims(start, canSplitWord) 96 | pos = start 97 | left_flanking = true 98 | right_flanking = true 99 | max = @posMax 100 | marker = charCodeAt(@src, start) 101 | 102 | # treat beginning of the line as a whitespace 103 | lastChar = start > 0 ? charCodeAt(@src, start - 1) : 0x20 104 | 105 | while (pos < max && charCodeAt(@src, pos) == marker) 106 | pos += 1 107 | end 108 | 109 | count = pos - start 110 | 111 | # treat end of the line as a whitespace 112 | nextChar = pos < max ? charCodeAt(@src, pos) : 0x20 113 | 114 | isLastPunctChar = isMdAsciiPunct(lastChar) || isPunctChar(fromCodePoint(lastChar)) 115 | isNextPunctChar = isMdAsciiPunct(nextChar) || isPunctChar(fromCodePoint(nextChar)) 116 | 117 | isLastWhiteSpace = isWhiteSpace(lastChar) 118 | isNextWhiteSpace = isWhiteSpace(nextChar) 119 | 120 | if (isNextWhiteSpace) 121 | left_flanking = false 122 | elsif (isNextPunctChar) 123 | if (!(isLastWhiteSpace || isLastPunctChar)) 124 | left_flanking = false 125 | end 126 | end 127 | 128 | if isLastWhiteSpace 129 | right_flanking = false 130 | elsif isLastPunctChar 131 | if !(isNextWhiteSpace || isNextPunctChar) 132 | right_flanking = false 133 | end 134 | end 135 | 136 | if !canSplitWord 137 | can_open = left_flanking && (!right_flanking || isLastPunctChar) 138 | can_close = right_flanking && (!left_flanking || isNextPunctChar) 139 | else 140 | can_open = left_flanking 141 | can_close = right_flanking 142 | end 143 | 144 | return { can_open: can_open, can_close: can_close, length: count } 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/strikethrough.rb: -------------------------------------------------------------------------------- 1 | # ~~strike through~~ 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | module RulesInline 5 | class Strikethrough 6 | extend Common::Utils 7 | 8 | # Insert each marker as a separate text token, and add it to delimiter list 9 | #------------------------------------------------------------------------------ 10 | def self.tokenize(state, silent) 11 | start = state.pos 12 | marker = charCodeAt(state.src, start) 13 | 14 | return false if silent 15 | 16 | return false if marker != 0x7E # ~ 17 | 18 | scanned = state.scanDelims(state.pos, true) 19 | len = scanned[:length] 20 | ch = fromCodePoint(marker) 21 | 22 | return false if len < 2 23 | 24 | if len % 2 > 0 25 | token = state.push('text', '', 0) 26 | token.content = ch 27 | len -= 1 28 | end 29 | 30 | i = 0 31 | while i < len 32 | token = state.push('text', '', 0) 33 | token.content = ch + ch 34 | 35 | state.delimiters.push({ 36 | marker: marker, 37 | length: 0, # disable "rule of 3" length checks meant for emphasis 38 | token: state.tokens.length - 1, 39 | end: -1, 40 | open: scanned[:can_open], 41 | close: scanned[:can_close] 42 | }) 43 | i += 2 44 | end 45 | 46 | state.pos += scanned[:length] 47 | 48 | return true 49 | end 50 | 51 | def self.private_postProcess(state, delimiters) 52 | loneMarkers = [] 53 | max = delimiters.length 54 | 55 | 0.upto(max - 1) do |i| 56 | startDelim = delimiters[i] 57 | 58 | next if startDelim[:marker] != 0x7E # ~ 59 | 60 | next if startDelim[:end] == -1 61 | 62 | endDelim = delimiters[startDelim[:end]] 63 | 64 | token = state.tokens[startDelim[:token]] 65 | token.type = 's_open' 66 | token.tag = 's' 67 | token.nesting = 1 68 | token.markup = '~~' 69 | token.content = '' 70 | 71 | token = state.tokens[endDelim[:token]] 72 | token.type = 's_close' 73 | token.tag = 's' 74 | token.nesting = -1 75 | token.markup = '~~' 76 | token.content = '' 77 | 78 | if (state.tokens[endDelim[:token] - 1].type == 'text' && 79 | state.tokens[endDelim[:token] - 1].content == '~') 80 | loneMarkers.push(endDelim[:token] - 1) 81 | end 82 | end 83 | 84 | # If a marker sequence has an odd number of characters, it's splitted 85 | # like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the 86 | # start of the sequence. 87 | # 88 | # So, we have to move all those markers after subsequent s_close tags. 89 | # 90 | while loneMarkers.length > 0 91 | i = loneMarkers.pop 92 | j = i + 1 93 | 94 | while j < state.tokens.length && state.tokens[j].type == 's_close' 95 | j += 1 96 | end 97 | 98 | j -= 1 99 | 100 | if i != j 101 | token = state.tokens[j] 102 | state.tokens[j] = state.tokens[i] 103 | state.tokens[i] = token 104 | end 105 | end 106 | end 107 | 108 | # Walk through delimiter list and replace text tokens with tags 109 | # 110 | def self.postProcess(state) 111 | tokens_meta = state.tokens_meta 112 | max = state.tokens_meta.length 113 | 114 | private_postProcess(state, state.delimiters) 115 | 116 | 0.upto(max - 1) do |curr| 117 | if (tokens_meta[curr] && tokens_meta[curr][:delimiters]) 118 | private_postProcess(state, tokens_meta[curr][:delimiters]) 119 | end 120 | end 121 | end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/rules_inline/text.rb: -------------------------------------------------------------------------------- 1 | # Skip text characters for text token, place those to pending buffer 2 | # and increment current pos 3 | #------------------------------------------------------------------------------ 4 | module MarkdownIt 5 | module RulesInline 6 | class Text 7 | extend Common::Utils 8 | 9 | # Rule to skip pure text 10 | # '{}$%@~+=:' reserved for extentions 11 | 12 | # !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, _, `, {, |, }, or ~ 13 | 14 | # !!!! Don't confuse with "Markdown ASCII Punctuation" chars 15 | # http://spec.commonmark.org/0.15/#ascii-punctuation-character 16 | #------------------------------------------------------------------------------ 17 | def self.isTerminatorChar(ch) 18 | case ch 19 | when 0x0A, # \n 20 | 0x21, # ! 21 | 0x23, # # 22 | 0x24, # $ 23 | 0x25, # % 24 | 0x26, # & 25 | 0x2A, # * 26 | 0x2B, # + 27 | 0x2D, # - 28 | 0x3A, # : 29 | 0x3C, # < 30 | 0x3D, # = 31 | 0x3E, # > 32 | 0x40, # @ 33 | 0x5B, # [ 34 | 0x5C, # \ 35 | 0x5D, # ] 36 | 0x5E, # ^ 37 | 0x5F, # _ 38 | 0x60, # ` 39 | 0x7B, # { 40 | 0x7D, # } 41 | 0x7E # ~ 42 | return true 43 | else 44 | return false 45 | end 46 | end 47 | 48 | #------------------------------------------------------------------------------ 49 | def self.text(state, silent) 50 | pos = state.pos 51 | 52 | while pos < state.posMax && !self.isTerminatorChar(charCodeAt(state.src, pos)) 53 | pos += 1 54 | end 55 | 56 | return false if pos == state.pos 57 | 58 | state.pending += state.src.slice(state.pos...pos) if !silent 59 | state.pos = pos 60 | return true 61 | end 62 | 63 | # // Alternative implementation, for memory. 64 | # // 65 | # // It costs 10% of performance, but allows extend terminators list, if place it 66 | # // to `ParcerInline` property. Probably, will switch to it sometime, such 67 | # // flexibility required. 68 | # 69 | # /* 70 | # var TERMINATOR_RE = /[\n!#$%&*+\-:<=>@[\\\]^_`{}~]/; 71 | # 72 | # module.exports = function text(state, silent) { 73 | # var pos = state.pos, 74 | # idx = state.src.slice(pos).search(TERMINATOR_RE); 75 | # 76 | # // first char is terminator -> empty text 77 | # if (idx === 0) { return false; } 78 | # 79 | # // no terminator -> text till end of string 80 | # if (idx < 0) { 81 | # if (!silent) { state.pending += state.src.slice(pos); } 82 | # state.pos = state.src.length; 83 | # return true; 84 | # } 85 | # 86 | # if (!silent) { state.pending += state.src.slice(pos, pos + idx); } 87 | # 88 | # state.pos += idx; 89 | # 90 | # return true; 91 | # };*/ 92 | 93 | end 94 | end 95 | end -------------------------------------------------------------------------------- /lib/motion-markdown-it/token.rb: -------------------------------------------------------------------------------- 1 | # Token class 2 | #------------------------------------------------------------------------------ 3 | module MarkdownIt 4 | class Token 5 | 6 | attr_accessor :type, :tag, :attrs, :map, :nesting, :level, :children 7 | attr_accessor :content, :markup, :info, :meta, :block, :hidden 8 | 9 | # new Token(type, tag, nesting) 10 | # 11 | # Create new token and fill passed properties. 12 | #------------------------------------------------------------------------------ 13 | def initialize(type, tag, nesting) 14 | # * Token#type -> String 15 | # * 16 | # * Type of the token (string, e.g. "paragraph_open") 17 | @type = type 18 | 19 | # * Token#tag -> String 20 | # * 21 | # * html tag name, e.g. "p" 22 | @tag = tag 23 | 24 | # * Token#attrs -> Array 25 | # * 26 | # * Html attributes. Format: `[ [ name1, value1 ], [ name2, value2 ] ]` 27 | @attrs = nil 28 | 29 | # * Token#map -> Array 30 | # * 31 | # * Source map info. Format: `[ line_begin, line_end ]` 32 | @map = nil 33 | 34 | # * Token#nesting -> Number 35 | # * 36 | # * Level change (number in {-1, 0, 1} set), where: 37 | # * 38 | # * - `1` means the tag is opening 39 | # * - `0` means the tag is self-closing 40 | # * - `-1` means the tag is closing 41 | @nesting = nesting 42 | 43 | # * Token#level -> Number 44 | # * 45 | # * nesting level, the same as `state.level` 46 | @level = 0 47 | 48 | # * Token#children -> Array 49 | # * 50 | # * An array of child nodes (inline and img tokens) 51 | @children = nil 52 | 53 | # * Token#content -> String 54 | # * 55 | # * In a case of self-closing tag (code, html, fence, etc.), 56 | # * it has contents of this tag. 57 | @content = '' 58 | 59 | # * Token#markup -> String 60 | # * 61 | # * '*' or '_' for emphasis, fence string for fence, etc. 62 | @markup = '' 63 | 64 | # * Token#info -> String 65 | # * 66 | # * Additional information: 67 | # * 68 | # * - Info string for "fence" tokens 69 | # * - The value "auto" for autolink "link_open" and "link_close" tokens 70 | # * - The string value of the item marker for ordered-list "list_item_open" tokens 71 | @info = '' 72 | 73 | # * Token#meta -> Object 74 | # * 75 | # * A place for plugins to store an arbitrary data 76 | @meta = nil 77 | 78 | # * Token#block -> Boolean 79 | # * 80 | # * True for block-level tokens, false for inline tokens. 81 | # * Used in renderer to calculate line breaks 82 | @block = false 83 | 84 | # * Token#hidden -> Boolean 85 | # * 86 | # * If it's true, ignore this element when rendering. Used for tight lists 87 | # * to hide paragraphs. 88 | @hidden = false 89 | end 90 | 91 | 92 | # * Token.attrIndex(name) -> Number 93 | # * 94 | # * Search attribute index by name. 95 | #------------------------------------------------------------------------------ 96 | def attrIndex(name) 97 | return -1 if !@attrs 98 | 99 | attrs = @attrs 100 | 101 | attrs.each_with_index do |attr_, index| 102 | return index if attr_[0] == name 103 | end 104 | return -1 105 | end 106 | 107 | # * Token.attrPush(attrData) 108 | # * 109 | # * Add `[ name, value ]` attribute to list. Init attrs if necessary 110 | #------------------------------------------------------------------------------ 111 | def attrPush(attrData) 112 | if @attrs 113 | @attrs.push(attrData) 114 | else 115 | @attrs = [ attrData ] 116 | end 117 | end 118 | 119 | # Token.attrSet(name, value) 120 | # 121 | # Set `name` attribute to `value`. Override old value if exists. 122 | #------------------------------------------------------------------------------ 123 | def attrSet(name, value) 124 | idx = attrIndex(name) 125 | attrData = [ name, value ] 126 | 127 | if idx < 0 128 | attrPush(attrData) 129 | else 130 | @attrs[idx] = attrData 131 | end 132 | end 133 | 134 | # Token.attrGet(name) 135 | # 136 | # Get the value of attribute `name`, or null if it does not exist. 137 | #------------------------------------------------------------------------------ 138 | def attrGet(name) 139 | idx = attrIndex(name) 140 | value = nil 141 | 142 | if idx >= 0 143 | value = @attrs[idx][1] 144 | end 145 | 146 | return value 147 | end 148 | 149 | # Token.attrJoin(name, value) 150 | # 151 | # Join value to existing attribute via space. Or create new attribute if not 152 | # exists. Useful to operate with token classes. 153 | #------------------------------------------------------------------------------ 154 | def attrJoin(name, value) 155 | idx = attrIndex(name) 156 | 157 | if idx < 0 158 | attrPush([ name, value ]) 159 | else 160 | @attrs[idx][1] = @attrs[idx][1] + ' ' + value 161 | end 162 | end 163 | 164 | #------------------------------------------------------------------------------ 165 | def to_json 166 | { 167 | type: @type, 168 | tag: @tag, 169 | attrs: @attrs, 170 | map: @map, 171 | nesting: @nesting, 172 | level: @level, 173 | children: @children.nil? ? nil : @children.each {|t| t.to_json}, 174 | content: @content, 175 | markup: @markup, 176 | info: @info, 177 | meta: @meta, 178 | block: @block, 179 | hidden: @hidden 180 | } 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /lib/motion-markdown-it/version.rb: -------------------------------------------------------------------------------- 1 | module MotionMarkdownIt 2 | VERSION = '13.0.1' 3 | end 4 | -------------------------------------------------------------------------------- /motion-markdown-it.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path('../lib/motion-markdown-it/version.rb', __FILE__) 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = 'motion-markdown-it' 5 | gem.version = MotionMarkdownIt::VERSION 6 | gem.authors = ["Brett Walker", "Vitaly Puzrin", "Alex Kocharin"] 7 | gem.email = 'github@digitalmoksha.com' 8 | gem.summary = "Ruby version markdown-it" 9 | gem.description = "Ruby/RubyMotion version of markdown-it" 10 | gem.homepage = 'https://github.com/digitalmoksha/motion-markdown-it' 11 | gem.licenses = ['MIT'] 12 | 13 | gem.files = Dir.glob('lib/**/*.rb') 14 | gem.files << 'README.md' 15 | gem.test_files = Dir["spec/**/*.rb"] 16 | 17 | gem.require_paths = ["lib"] 18 | 19 | gem.add_dependency 'mdurl-rb', '~> 1.0' 20 | gem.add_dependency 'uc.micro-rb', '~> 1.0' 21 | gem.add_dependency 'linkify-it-rb', '~> 4.0' 22 | 23 | gem.add_development_dependency 'motion-expect', '~> 2.0' # required for Travis build to work 24 | end 25 | -------------------------------------------------------------------------------- /rubymotion/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | gemspec path: '..' 5 | 6 | gem 'rake' 7 | gem 'pry-byebug' 8 | 9 | group :spec do 10 | gem 'motion-expect' 11 | end 12 | -------------------------------------------------------------------------------- /rubymotion/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift('/Library/RubyMotion/lib') 4 | $LOAD_PATH.unshift('~/.rubymotion/rubymotion-templates') 5 | require 'pry-byebug' 6 | 7 | testing = true if ARGV.join(' ') =~ /spec/ 8 | platform = ENV.fetch('platform', 'osx') 9 | SDK_VERSION = '13.0' 10 | DEPLOYMENT_TARGET = '13.0' 11 | 12 | require "motion/project/template/#{platform}" 13 | 14 | # set these upfront, otherwise other gems using `setup` could cause 15 | # internal validations to fail on incorrect deployment_target/sdk_version 16 | Motion::Project::App.setup do |app| 17 | app.name = 'motion-markdown-it' 18 | app.identifier = 'com.motion-gemtest.motion-markdown-it' 19 | 20 | if platform == 'ios' 21 | # must set to the maximum SDK that the open source license supports, 22 | # which is the latest non-beta 23 | app.sdk_version = '16.1' 24 | app.deployment_target = '16.1' 25 | else 26 | app.sdk_version = ENV.fetch('SDK_VERSION', SDK_VERSION) 27 | app.deployment_target = ENV.fetch('DEPLOYMENT_TARGET', DEPLOYMENT_TARGET) 28 | end 29 | end 30 | 31 | require 'rubygems' 32 | 33 | begin 34 | require 'bundler' 35 | testing ? Bundler.require(:default, :spec) : Bundler.require 36 | rescue LoadError 37 | end 38 | 39 | require 'motion-expect' if testing 40 | -------------------------------------------------------------------------------- /rubymotion/app/app_delegate.rb: -------------------------------------------------------------------------------- 1 | class AppDelegate 2 | 3 | # OS X entry point 4 | #------------------------------------------------------------------------------ 5 | def applicationDidFinishLaunching(notification) 6 | end 7 | 8 | # iOS entry point 9 | #------------------------------------------------------------------------------ 10 | def application(application, didFinishLaunchingWithOptions:launchOptions) 11 | true 12 | end 13 | 14 | end -------------------------------------------------------------------------------- /rubymotion/spec/motion-markdown-it/_helpers/_testgen_helper.rb: -------------------------------------------------------------------------------- 1 | # Markdown-It ignores CM where an empty blockquote tag gets rendered with a 2 | # newline. This changes the output so that the tests pass [2058, 2381, and 2388] 3 | #------------------------------------------------------------------------------ 4 | def normalize(text) 5 | return text.gsub(/
\n<\/blockquote>/, '
') 6 | end 7 | 8 | #------------------------------------------------------------------------------ 9 | def get_tests(specfile) 10 | line_number = 0 11 | start_line = 0 12 | end_line = 0 13 | example_number = 0 14 | markdown_lines = [] 15 | html_lines = [] 16 | state = 0 # 0 regular text, 1 markdown example, 2 html output 17 | headertext = '' 18 | tests = [] 19 | header_re = /#+ / 20 | 21 | File.open(specfile) do |specf| 22 | specf.each_line do |line| 23 | line_number += 1 24 | if state == 0 && header_re =~ line 25 | headertext = line.gsub(header_re, '').strip 26 | end 27 | if line.strip == "." 28 | state = (state + 1) % 3 29 | if state == 0 30 | example_number += 1 31 | end_line = line_number 32 | tests << { 33 | markdown: markdown_lines.join.gsub('→',"\t"), 34 | html: html_lines.join, 35 | example: example_number, 36 | start_line: start_line, 37 | end_line: end_line, 38 | section: headertext} 39 | start_line = 0 40 | markdown_lines = [] 41 | html_lines = [] 42 | end 43 | elsif state == 1 44 | if start_line == 0 45 | start_line = line_number - 1 46 | end 47 | markdown_lines << line 48 | elsif state == 2 49 | html_lines << line 50 | end 51 | end 52 | end 53 | return tests 54 | end 55 | 56 | #------------------------------------------------------------------------------ 57 | def define_test(testcase, parser, debug_tokens = false) 58 | 59 | it "#{testcase[:section]} (#{testcase[:example].to_s}/#{testcase[:start_line]}-#{testcase[:end_line]}) with markdown:\n#{testcase[:markdown]}" do 60 | if debug_tokens 61 | parser.parse(testcase[:markdown], { references: {} }).each {|token| pp token.to_json} 62 | end 63 | expect(parser.render(testcase[:markdown])).to eq normalize(testcase[:html]) 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /rubymotion/spec/motion-markdown-it/bench_mark_spec.rb: -------------------------------------------------------------------------------- 1 | # runs = 50 2 | # files = ['mdsyntax.text', 'mdbasics.text'] 3 | # benchmark_dir = File.join(File.dirname(__FILE__), '../../benchmark') 4 | # 5 | # puts 6 | # puts "Running tests on #{Time.now.strftime("%Y-%m-%d")} under #{RUBY_DESCRIPTION}" 7 | # 8 | # files.each do |file| 9 | # data = File.read(File.join(benchmark_dir, file)) 10 | # puts 11 | # puts "==> Test using file #{file} and #{runs} runs" 12 | # 13 | # # results = Benchmark.bmbm do |b| 14 | # results = Benchmark.bm do |b| 15 | # b.report("motion-markdown-it 0.1.0") do 16 | # parser = MarkdownIt::Parser.new(:commonmark, { html: false }) 17 | # runs.times { parser.render(data) } 18 | # end 19 | # # b.report("kramdown #{Kramdown::VERSION}") { runs.times { Kramdown::Document.new(data).to_html } } 20 | # # b.report("markdown-it 4.0.1 JS") { runs.times { NSApplication.sharedApplication.delegate.markdown_it(data) } } 21 | # # b.report(" hoedown 3.0.1") do 22 | # # runs.times do 23 | # # document = HoedownDocument.new 24 | # # document.initWithHtmlRendererWithFlags(FLAGS) 25 | # # html = document.renderMarkdownString(data) 26 | # # end 27 | # # end 28 | # end 29 | # 30 | # # puts 31 | # # puts "Real time of X divided by real time of kramdown" 32 | # # kd = results.shift.real 33 | # # %w[hoedown].each do |name| 34 | # # puts name.ljust(19) << (results.shift.real/kd).round(4).to_s 35 | # # end 36 | # end 37 | # 38 | # describe "Benchmark Test" do 39 | # it "benchmarks with mdsyntax.text and mdbasics.text" do 40 | # expect(true).to eq true 41 | # end 42 | # end 43 | -------------------------------------------------------------------------------- /rubymotion/spec/motion-markdown-it/commonmark_spec.rb: -------------------------------------------------------------------------------- 1 | fixture_dir = File.join(File.dirname(__FILE__), '../../../spec/motion-markdown-it/fixtures') 2 | 3 | #------------------------------------------------------------------------------ 4 | describe "CommonMark Specs" do 5 | parser = MarkdownIt::Parser.new(:commonmark) 6 | specfile = File.join(fixture_dir, 'commonmark', 'good.txt') 7 | tests = get_tests(specfile) 8 | 9 | if ENV['example'] 10 | define_test(tests[ENV['example'].to_i - 1], parser, true) 11 | else 12 | tests.each do |t| 13 | define_test(t, parser) 14 | end 15 | end 16 | 17 | it "another" do 18 | expect(true).to eq true 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /rubymotion/spec/motion-markdown-it/markdown_it_spec.rb: -------------------------------------------------------------------------------- 1 | fixture_dir = File.join(File.dirname(__FILE__), '../../../spec/motion-markdown-it/fixtures') 2 | 3 | #------------------------------------------------------------------------------ 4 | describe "markdown-it" do 5 | 6 | parser = MarkdownIt::Parser.new({ html: true, langPrefix: '', typographer: true, linkify: true }) 7 | datafiles = File.join(fixture_dir, 'markdown-it', '**/*') 8 | 9 | Dir[datafiles].each do |data_file| 10 | tests = get_tests(data_file) 11 | if ENV['example'] 12 | define_test(tests[ENV['example'].to_i - 1], parser, true) 13 | else 14 | tests.each do |t| 15 | define_test(t, parser) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /rubymotion/spec/motion-markdown-it/ruler_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Ruler' do 2 | 3 | #------------------------------------------------------------------------------ 4 | it 'should replace rule (.at)' do 5 | ruler = MarkdownIt::Ruler.new 6 | res = 0 7 | 8 | ruler.push('test', lambda { res = 1 }) 9 | ruler.at('test', lambda { res = 2 }) 10 | 11 | rules = ruler.getRules('') 12 | 13 | expect(rules.length).to eq 1 14 | rules[0].call 15 | expect(res).to eq 2 16 | end 17 | 18 | 19 | #------------------------------------------------------------------------------ 20 | it 'should inject before/after rule' do 21 | ruler = MarkdownIt::Ruler.new 22 | @res = 0 23 | 24 | ruler.push('test', lambda { @res = 1 }) 25 | ruler.before('test', 'before_test', lambda { @res = -10; }) 26 | ruler.after('test', 'after_test', lambda { @res = 10; }) 27 | 28 | rules = ruler.getRules(''); 29 | 30 | expect(rules.length).to eq 3 31 | rules[0].call 32 | expect(@res).to eq -10 33 | rules[1].call 34 | expect(@res).to eq 1 35 | rules[2].call 36 | expect(@res).to eq 10 37 | end 38 | 39 | 40 | #------------------------------------------------------------------------------ 41 | it 'should enable/disable rule' do 42 | ruler = MarkdownIt::Ruler.new 43 | 44 | ruler.push('test', lambda {}) 45 | ruler.push('test2', lambda {}) 46 | 47 | rules = ruler.getRules('') 48 | expect(rules.length).to eq 2 49 | 50 | ruler.disable('test') 51 | rules = ruler.getRules('') 52 | expect(rules.length).to eq 1 53 | ruler.disable('test2') 54 | rules = ruler.getRules('') 55 | expect(rules.length).to eq 0 56 | 57 | ruler.enable('test') 58 | rules = ruler.getRules('') 59 | expect(rules.length).to eq 1 60 | ruler.enable('test2') 61 | rules = ruler.getRules('') 62 | expect(rules.length).to eq 2 63 | end 64 | 65 | 66 | #------------------------------------------------------------------------------ 67 | it 'should enable/disable multiple rule' do 68 | ruler = MarkdownIt::Ruler.new 69 | 70 | ruler.push('test', lambda {}) 71 | ruler.push('test2', lambda {}) 72 | 73 | ruler.disable([ 'test', 'test2' ]) 74 | rules = ruler.getRules('') 75 | expect(rules.length).to eq 0 76 | ruler.enable([ 'test', 'test2' ]) 77 | rules = ruler.getRules('') 78 | expect(rules.length).to eq 2 79 | end 80 | 81 | 82 | #------------------------------------------------------------------------------ 83 | it 'should enable rules by whitelist' do 84 | ruler = MarkdownIt::Ruler.new 85 | 86 | ruler.push('test', lambda {}) 87 | ruler.push('test2', lambda {}) 88 | 89 | ruler.enableOnly('test') 90 | rules = ruler.getRules('') 91 | expect(rules.length).to eq 1 92 | end 93 | 94 | 95 | #------------------------------------------------------------------------------ 96 | it 'should support multiple chains' do 97 | ruler = MarkdownIt::Ruler.new 98 | 99 | ruler.push('test', lambda {}) 100 | ruler.push('test2', lambda {}, { alt: [ 'alt1' ] }) 101 | ruler.push('test2', lambda {}, { alt: [ 'alt1', 'alt2' ] }) 102 | 103 | rules = ruler.getRules('') 104 | expect(rules.length).to eq 3 105 | rules = ruler.getRules('alt1') 106 | expect(rules.length).to eq 2 107 | rules = ruler.getRules('alt2'); 108 | expect(rules.length).to eq 1 109 | end 110 | 111 | 112 | #------------------------------------------------------------------------------ 113 | it 'should fail on invalid rule name' do 114 | ruler = MarkdownIt::Ruler.new 115 | 116 | ruler.push('test', lambda {}) 117 | 118 | expect { 119 | ruler.at('invalid name', lambda {}) 120 | }.to raise_error(StandardError) 121 | expect { 122 | ruler.before('invalid name', lambda {}) 123 | }.to raise_error(StandardError) 124 | expect { 125 | ruler.after('invalid name', lambda {}) 126 | }.to raise_error(StandardError) 127 | expect { 128 | ruler.enable('invalid name') 129 | }.to raise_error(StandardError) 130 | expect { 131 | ruler.disable('invalid name') 132 | }.to raise_error(StandardError) 133 | end 134 | 135 | 136 | #------------------------------------------------------------------------------ 137 | it 'should not fail on invalid rule name in silent mode' do 138 | ruler = MarkdownIt::Ruler.new 139 | 140 | ruler.push('test', lambda {}) 141 | 142 | expect { 143 | ruler.enable('invalid name', true) 144 | }.not_to raise_error 145 | expect { 146 | ruler.enableOnly('invalid name', true) 147 | }.not_to raise_error 148 | expect { 149 | ruler.disable('invalid name', true) 150 | }.not_to raise_error 151 | end 152 | 153 | end 154 | -------------------------------------------------------------------------------- /rubymotion/spec/motion-markdown-it/token_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Token" do 2 | 3 | it 'attr' do 4 | t = MarkdownIt::Token.new('test_token', 'tok', 1) 5 | 6 | expect(t.attrs).to eq nil 7 | expect(t.attrIndex('foo')).to eq -1 8 | 9 | t.attrPush([ 'foo', 'bar' ]) 10 | t.attrPush([ 'baz', 'bad' ]) 11 | 12 | expect(t.attrIndex('foo')).to eq 0 13 | expect(t.attrIndex('baz')).to eq 1 14 | expect(t.attrIndex('none')).to eq -1 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /rubymotion/spec/motion-markdown-it/utils_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Utils' do 2 | extend MarkdownIt::Common::Utils 3 | 4 | #------------------------------------------------------------------------------ 5 | it 'fromCodePoint' do 6 | expect(fromCodePoint(0x20)).to eq ' ' 7 | expect(fromCodePoint(0x1F601)).to eq '😁' 8 | end 9 | 10 | #------------------------------------------------------------------------------ 11 | it 'isValidEntityCode' do 12 | expect(isValidEntityCode(0x20)).to eq true 13 | expect(isValidEntityCode(0xD800)).to eq false 14 | expect(isValidEntityCode(0xFDD0)).to eq false 15 | expect(isValidEntityCode(0x1FFFF)).to eq false 16 | expect(isValidEntityCode(0x1FFFE)).to eq false 17 | expect(isValidEntityCode(0x00)).to eq false 18 | expect(isValidEntityCode(0x0B)).to eq false 19 | expect(isValidEntityCode(0x0E)).to eq false 20 | expect(isValidEntityCode(0x7F)).to eq false 21 | end 22 | 23 | #------------------------------------------------------------------------------ 24 | it 'assign' do 25 | expect(assign({ a: 1 }, nil, { b: 2 })).to eq ({ a: 1, b: 2 }) 26 | expect { 27 | assign({}, 123) 28 | }.to raise_error(StandardError) 29 | end 30 | 31 | #------------------------------------------------------------------------------ 32 | it 'escapeRE' do 33 | expect(escapeRE(' .?*+^$[]\\(){}|-')).to eq ' \\.\\?\\*\\+\\^\\$\\[\\]\\\\\\(\\)\\{\\}\\|\\-' 34 | end 35 | 36 | #------------------------------------------------------------------------------ 37 | it 'isWhiteSpace' do 38 | expect(isWhiteSpace(0x2000)).to eq true 39 | expect(isWhiteSpace(0x09)).to eq true 40 | 41 | expect(isWhiteSpace(0x30)).to eq false 42 | end 43 | 44 | #------------------------------------------------------------------------------ 45 | it 'isMdAsciiPunct' do 46 | expect(isMdAsciiPunct(0x30)).to eq false 47 | 48 | '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split('').each do |ch| 49 | expect(isMdAsciiPunct(ch.ord)).to eq true 50 | end 51 | end 52 | 53 | #------------------------------------------------------------------------------ 54 | it 'unescapeMd' do 55 | expect(unescapeMd('\\foo')).to eq '\\foo' 56 | expect(unescapeMd('foo')).to eq 'foo' 57 | 58 | '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split('').each do |ch| 59 | expect(unescapeMd('\\' + ch)).to eq ch 60 | end 61 | end 62 | 63 | #------------------------------------------------------------------------------ 64 | it "escapeHtml" do 65 | str = '
x & "y"' 66 | expect(escapeHtml(str)).to eq '<hr>x & "y"' 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /rubymotion/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Add following abilities 2 | # 3 | # rake spec filter="name of spec" # To filter by spec name 4 | # rake spec files=foo_spec filter="name of spec" 5 | # rake spec filter_context="this doesn't work yet" # To filter by context name (doesn't work until MacBacon implements it) 6 | # rake spec hide_backtraces=yes # Hide backtraces 7 | 8 | 9 | # From http://chen.do/blog/2013/06/03/running-individual-specs-with-rubymotion/ 10 | #------------------------------------------------------------------------------ 11 | def silence_warnings(&block) 12 | warn_level = $VERBOSE 13 | $VERBOSE = nil 14 | begin 15 | result = block.call 16 | ensure 17 | $VERBOSE = warn_level 18 | end 19 | result 20 | end 21 | 22 | silence_warnings do 23 | module Bacon 24 | if ENV['filter'] 25 | $stderr.puts "Filtering specs that match: #{ENV['filter']}" 26 | RestrictName = Regexp.new(ENV['filter']) 27 | end 28 | 29 | if ENV['filter_context'] 30 | $stderr.puts "Filtering contexts that match: #{ENV['filter_context']}" 31 | RestrictContext = Regexp.new(ENV['filter_context']) 32 | end 33 | 34 | Backtraces = false if ENV['hide_backtraces'] 35 | end 36 | end 37 | 38 | #------------------------------------------------------------------------------ 39 | # TODO total hack - without this, the `bacon-expect` gem doesn't get used, and 40 | # On iOS I get errors like 41 | # *** Terminating app due to uncaught exception 'NameError', reason: '.../bacon-expect/lib/bacon-expect/matchers/eql.rb:2:in `
: uninitialized constant BaconExpect::Matcher::SingleMethod (NameError) 42 | # and on macOS it only show up as `*nil description*` 43 | module BaconExpect 44 | module Matcher 45 | class SingleMethod 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/commonmark_spec.rb: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | describe "CommonMark Specs" do 3 | 4 | parser = MarkdownIt::Parser.new(:commonmark) 5 | specfile = File.join(File.dirname(__FILE__), 'fixtures', 'commonmark', 'good.txt') 6 | tests = get_tests(specfile) 7 | 8 | if ENV['example'] 9 | define_test(tests[ENV['example'].to_i - 1], parser, true) 10 | else 11 | tests.each do |t| 12 | define_test(t, parser) 13 | end 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /spec/motion-markdown-it/fixtures/commonmark/bad.txt: -------------------------------------------------------------------------------- 1 | These examples have been pulled out because they fail for 2 | currently unknown reasons. 3 | 4 | Pulled from `good.txt` 5 | 6 | This fails only in RubyMotion 7 | 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | src line: 2984 10 | 11 | . 12 | [ΑΓΩ]: /φου 13 | 14 | [αγω] 15 | . 16 |

αγω

17 | . 18 | 19 | This fails only in RubyMotion 20 | 21 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 22 | src line: 8117 23 | 24 | . 25 | [ẞ] 26 | 27 | [SS]: /url 28 | . 29 |

30 | . 31 | 32 | This one fails in both Ruby and RubyMotion 33 | 34 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | src line: 8770 36 | 37 | . 38 | 39 | . 40 |

http://../

41 | . 42 | 43 | -------- 44 | 45 | Pulled from `commonmark_extras.txt` 46 | 47 | This fails only in RubyMotion 48 | 49 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 50 | Reference labels: 'i̇θωkå'.toUpperCase() is 'İΘΩKÅ', but these should still be equivalent 51 | . 52 | [İϴΩKÅ] 53 | 54 | [i̇θωkå]: /url 55 | . 56 |

İϴΩKÅ

57 | . 58 | 59 | 60 | This fails only in RubyMotion 61 | 62 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | Reference labels: support ligatures (equivalent according to unicode case folding) 64 | . 65 | [fffifl] 66 | 67 | [fffifl]: /url 68 | . 69 |

fffifl

70 | . 71 | 72 | 73 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/fixtures/markdown-it/fatal.txt: -------------------------------------------------------------------------------- 1 | Should not throw exception on invalid chars in URL (`*` not allowed in path) [mailformed URI] 2 | . 3 | [foo](<%test>) 4 | . 5 |

foo

6 | . 7 | 8 | 9 | Should not throw exception on broken utf-8 sequence in URL [mailformed URI] 10 | . 11 | [foo](%C3) 12 | . 13 |

foo

14 | . 15 | 16 | 17 | Should not throw exception on broken utf-16 surrogates sequence in URL [mailformed URI] 18 | . 19 | [foo](�) 20 | . 21 |

foo

22 | . 23 | 24 | 25 | Should not hang comments regexp 26 | . 27 | foo 30 | . 31 |

foo <!— xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ->

32 |

foo <!------------------------------------------------------------------->

33 | . 34 | 35 | 36 | Should not hang cdata regexp 37 | . 38 | foo 39 | . 40 |

foo <![CDATA[ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ]>

41 | . 42 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/fixtures/markdown-it/linkify.txt: -------------------------------------------------------------------------------- 1 | linkify 2 | . 3 | url http://www.youtube.com/watch?v=5Jt5GEr4AYg. 4 | . 5 |

url http://www.youtube.com/watch?v=5Jt5GEr4AYg.

6 | . 7 | 8 | 9 | don't touch text in links 10 | . 11 | [https://example.com](https://example.com) 12 | . 13 |

https://example.com

14 | . 15 | 16 | 17 | don't touch text in autolinks 18 | . 19 | 20 | . 21 |

https://example.com

22 | . 23 | 24 | 25 | don't touch text in html tags 26 | . 27 | https://example.com 28 | . 29 |

https://example.com

30 | . 31 | 32 | 33 | entities inside raw links 34 | . 35 | https://example.com/foo&bar 36 | . 37 |

https://example.com/foo&amp;bar

38 | . 39 | 40 | 41 | emphasis inside raw links (asterisk, can happen in links with params) 42 | . 43 | https://example.com/foo*bar*baz 44 | . 45 |

https://example.com/foo*bar*baz

46 | . 47 | 48 | 49 | emphasis inside raw links (underscore) 50 | . 51 | http://example.org/foo._bar_-_baz 52 | . 53 |

http://example.org/foo._bar_-_baz

54 | . 55 | 56 | 57 | backticks inside raw links 58 | . 59 | https://example.com/foo`bar`baz 60 | . 61 |

https://example.com/foo`bar`baz

62 | . 63 | 64 | 65 | links inside raw links 66 | . 67 | https://example.com/foo[123](456)bar 68 | . 69 |

https://example.com/foo[123](456)bar

70 | . 71 | 72 | 73 | escapes not allowed at the start 74 | . 75 | \https://example.com 76 | . 77 |

\https://example.com

78 | . 79 | 80 | 81 | escapes not allowed at comma 82 | . 83 | https\://example.com 84 | . 85 |

https://example.com

86 | . 87 | 88 | 89 | escapes not allowed at slashes 90 | . 91 | https:\//aa.org https://bb.org 92 | . 93 |

https://aa.org https://bb.org

94 | . 95 | 96 | 97 | fuzzy link shouldn't match cc.org 98 | . 99 | https:/\/cc.org 100 | . 101 |

https://cc.org

102 | . 103 | 104 | 105 | bold links (exclude markup of pairs from link tail) 106 | . 107 | **http://example.com/foobar** 108 | . 109 |

http://example.com/foobar

110 | . 111 | 112 | 113 | match links without protocol 114 | . 115 | www.example.org 116 | . 117 |

www.example.org

118 | . 119 | 120 | 121 | emails 122 | . 123 | test@example.com 124 | 125 | mailto:test@example.com 126 | . 127 |

test@example.com

128 |

mailto:test@example.com

129 | . 130 | 131 | 132 | typorgapher should not break href 133 | . 134 | http://example.com/(c) 135 | . 136 |

http://example.com/(c)

137 | . 138 | 139 | 140 | coverage, prefix not valid 141 | . 142 | http:/example.com/ 143 | . 144 |

http:/example.com/

145 | . 146 | 147 | 148 | coverage, negative link level 149 | . 150 | [https://example.com](https://example.com) 151 | . 152 |

https://example.com

153 | . 154 | 155 | 156 | emphasis with '*', real link: 157 | . 158 | http://cdecl.ridiculousfish.com/?q=int+%28*f%29+%28float+*%29%3B 159 | . 160 |

http://cdecl.ridiculousfish.com/?q=int+(*f)+(float+*)%3B

161 | . 162 | 163 | 164 | emphasis with '_', real link: 165 | . 166 | https://www.sell.fi/sites/default/files/elainlaakarilehti/tieteelliset_artikkelit/kahkonen_t._et_al.canine_pancreatitis-_review.pdf 167 | . 168 |

https://www.sell.fi/sites/default/files/elainlaakarilehti/tieteelliset_artikkelit/kahkonen_t._et_al.canine_pancreatitis-_review.pdf

169 | . 170 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/fixtures/markdown-it/normalize.txt: -------------------------------------------------------------------------------- 1 | 2 | Encode link destination, decode text inside it: 3 | 4 | . 5 | 6 | . 7 |

http://example.com/αβγδ

8 | . 9 | 10 | . 11 | [foo](http://example.com/α%CE%B2γ%CE%B4) 12 | . 13 |

foo

14 | . 15 | 16 | 17 | Keep %25 as is because decoding it may break urls, #720 18 | . 19 | 20 | . 21 |

https://www.google.com/search?q=hello.%252Ehello

22 | . 23 | 24 | 25 | Should decode punycode: 26 | 27 | . 28 | 29 | . 30 |

http://☃.net/

31 | . 32 | 33 | . 34 | 35 | . 36 |

http://☃.net/

37 | . 38 | 39 | Invalid punycode: 40 | 41 | . 42 | 43 | . 44 |

http://xn--xn.com/

45 | . 46 | 47 | Invalid punycode (non-ascii): 48 | 49 | . 50 | 51 | . 52 |

http://xn--γ.com/

53 | . 54 | 55 | Two slashes should start a domain: 56 | 57 | . 58 | [](//☃.net/) 59 | . 60 |

61 | . 62 | 63 | Don't encode domains in unknown schemas: 64 | 65 | . 66 | [](skype:γγγ) 67 | . 68 |

69 | . 70 | 71 | Should auto-add protocol to autolinks: 72 | 73 | . 74 | test google.com foo 75 | . 76 |

test google.com foo

77 | . 78 | 79 | Should support IDN in autolinks: 80 | 81 | . 82 | test http://xn--n3h.net/ foo 83 | . 84 |

test http://☃.net/ foo

85 | . 86 | 87 | . 88 | test http://☃.net/ foo 89 | . 90 |

test http://☃.net/ foo

91 | . 92 | 93 | . 94 | test //xn--n3h.net/ foo 95 | . 96 |

test //☃.net/ foo

97 | . 98 | 99 | . 100 | test xn--n3h.net foo 101 | . 102 |

test ☃.net foo

103 | . 104 | 105 | . 106 | test xn--n3h@xn--n3h.net foo 107 | . 108 |

test xn--n3h@☃.net foo

109 | . 110 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/fixtures/markdown-it/proto.txt: -------------------------------------------------------------------------------- 1 | . 2 | [__proto__] 3 | 4 | [__proto__]: blah 5 | . 6 |

proto

7 | . 8 | 9 | 10 | . 11 | [hasOwnProperty] 12 | 13 | [hasOwnProperty]: blah 14 | . 15 |

hasOwnProperty

16 | . 17 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/fixtures/markdown-it/smartquotes.txt: -------------------------------------------------------------------------------- 1 | Should parse nested quotes: 2 | . 3 | "foo 'bar' baz" 4 | 5 | 'foo 'bar' baz' 6 | . 7 |

“foo ‘bar’ baz”

8 |

‘foo ‘bar’ baz’

9 | . 10 | 11 | 12 | Should not overlap quotes: 13 | . 14 | 'foo "bar' baz" 15 | . 16 |

‘foo "bar’ baz"

17 | . 18 | 19 | 20 | Should match quotes on the same level: 21 | . 22 | "foo *bar* baz" 23 | . 24 |

“foo bar baz”

25 | . 26 | 27 | 28 | Should handle adjacent nested quotes: 29 | . 30 | '"double in single"' 31 | 32 | "'single in double'" 33 | . 34 |

‘“double in single”’

35 |

“‘single in double’”

36 | . 37 | 38 | 39 | 40 | Should not match quotes on different levels: 41 | . 42 | *"foo* bar" 43 | 44 | "foo *bar"* 45 | 46 | *"foo* bar *baz"* 47 | . 48 |

"foo bar"

49 |

"foo bar"

50 |

"foo bar baz"

51 | . 52 | 53 | Smartquotes should not overlap with other tags: 54 | . 55 | *foo "bar* *baz" quux* 56 | . 57 |

foo "bar baz" quux

58 | . 59 | 60 | 61 | Should try and find matching quote in this case: 62 | . 63 | "foo "bar 'baz" 64 | . 65 |

"foo “bar 'baz”

66 | . 67 | 68 | 69 | Should not touch 'inches' in quotes: 70 | . 71 | "Monitor 21"" and "Monitor"" 72 | . 73 |

“Monitor 21"” and “Monitor”"

74 | . 75 | 76 | 77 | Should render an apostrophe as a rsquo: 78 | . 79 | This isn't and can't be the best approach to implement this... 80 | . 81 |

This isn’t and can’t be the best approach to implement this…

82 | . 83 | 84 | 85 | Apostrophe could end the word, that's why original smartypants replaces all of them as rsquo: 86 | . 87 | users' stuff 88 | . 89 |

users’ stuff

90 | . 91 | 92 | Quotes between punctuation chars: 93 | 94 | . 95 | "(hai)". 96 | . 97 |

“(hai)”.

98 | . 99 | 100 | Quotes at the start/end of the tokens: 101 | . 102 | "*foo* bar" 103 | 104 | "foo *bar*" 105 | 106 | "*foo bar*" 107 | . 108 |

foo bar”

109 |

“foo bar

110 |

foo bar

111 | . 112 | 113 | Should treat softbreak as a space: 114 | . 115 | "this" 116 | and "that". 117 | 118 | "this" and 119 | "that". 120 | . 121 |

“this” 122 | and “that”.

123 |

“this” and 124 | “that”.

125 | . 126 | 127 | Should treat hardbreak as a space: 128 | . 129 | "this"\ 130 | and "that". 131 | 132 | "this" and\ 133 | "that". 134 | . 135 |

“this”
136 | and “that”.

137 |

“this” and
138 | “that”.

139 | . 140 | 141 | Should allow quotes adjacent to other punctuation characters, #643: 142 | . 143 | The dog---"'man's' best friend" 144 | . 145 |

The dog—“‘man’s’ best friend”

146 | . 147 | 148 | Should parse quotes adjacent to code block, #677: 149 | . 150 | "test `code`" 151 | 152 | "`code` test" 153 | . 154 |

“test code

155 |

code test”

156 | . 157 | 158 | Should parse quotes adjacent to inline html, #677: 159 | . 160 | "test
" 161 | 162 | "
test" 163 | . 164 |

“test

165 |


test”

166 | . 167 | 168 | Should be escapable: 169 | . 170 | "foo" 171 | 172 | \"foo" 173 | 174 | "foo\" 175 | . 176 |

“foo”

177 |

"foo"

178 |

"foo"

179 | . 180 | 181 | Should not replace entities: 182 | . 183 | "foo" 184 | 185 | "foo" 186 | 187 | "foo" 188 | . 189 |

"foo"

190 |

"foo"

191 |

"foo"

192 | . 193 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/fixtures/markdown-it/strikethrough.txt: -------------------------------------------------------------------------------- 1 | . 2 | ~~Strikeout~~ 3 | . 4 |

Strikeout

5 | . 6 | 7 | . 8 | x ~~~~foo~~ bar~~ 9 | . 10 |

x foo bar

11 | . 12 | 13 | . 14 | x ~~foo ~~bar~~~~ 15 | . 16 |

x foo bar

17 | . 18 | 19 | . 20 | x ~~~~foo~~~~ 21 | . 22 |

x foo

23 | . 24 | 25 | . 26 | x ~~a ~~foo~~~~~~~~~~~bar~~ b~~ 27 | 28 | x ~~a ~~foo~~~~~~~~~~~~bar~~ b~~ 29 | . 30 |

x a foo~~~bar b

31 |

x a foo~~~~bar b

32 | . 33 | 34 | 35 | Strikeouts have the same priority as emphases: 36 | . 37 | **~~test**~~ 38 | 39 | ~~**test~~** 40 | . 41 |

~~test~~

42 |

**test**

43 | . 44 | 45 | 46 | Strikeouts have the same priority as emphases with respect to links: 47 | . 48 | [~~link]()~~ 49 | 50 | ~~[link~~]() 51 | . 52 |

~~link~~

53 |

~~link~~

54 | . 55 | 56 | 57 | Strikeouts have the same priority as emphases with respect to backticks: 58 | . 59 | ~~`code~~` 60 | 61 | `~~code`~~ 62 | . 63 |

~~code~~

64 |

~~code~~

65 | . 66 | 67 | 68 | Nested strikeouts: 69 | . 70 | ~~foo ~~bar~~ baz~~ 71 | 72 | ~~f **o ~~o b~~ a** r~~ 73 | . 74 |

foo bar baz

75 |

f o o b a r

76 | . 77 | 78 | 79 | Should not have a whitespace between text and "~~": 80 | . 81 | foo ~~ bar ~~ baz 82 | . 83 |

foo ~~ bar ~~ baz

84 | . 85 | 86 | 87 | Should parse strikethrough within link tags: 88 | . 89 | [~~foo~~]() 90 | . 91 |

foo

92 | . 93 | 94 | 95 | Newline should be considered a whitespace: 96 | . 97 | ~~test 98 | ~~ 99 | 100 | ~~ 101 | test~~ 102 | 103 | ~~ 104 | test 105 | ~~ 106 | . 107 |

~~test 108 | ~~

109 |

~~ 110 | test~~

111 |

~~ 112 | test 113 | ~~

114 | . 115 | 116 | From CommonMark test suite, replacing `**` with our marker: 117 | 118 | . 119 | a~~"foo"~~ 120 | . 121 |

a~~“foo”~~

122 | . 123 | 124 | Regression test for #742: 125 | . 126 | -~~~~;~~~~~~ 127 | . 128 |

-;~~

129 | . 130 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/fixtures/markdown-it/typographer.txt: -------------------------------------------------------------------------------- 1 | . 2 | (bad) 3 | . 4 |

(bad)

5 | . 6 | 7 | 8 | copyright 9 | . 10 | (c) (C) 11 | . 12 |

© ©

13 | . 14 | 15 | 16 | reserved 17 | . 18 | (r) (R) 19 | . 20 |

® ®

21 | . 22 | 23 | 24 | trademark 25 | . 26 | (tm) (TM) 27 | . 28 |

™ ™

29 | . 30 | 31 | 32 | plus-minus 33 | . 34 | +-5 35 | . 36 |

±5

37 | . 38 | 39 | 40 | ellipsis 41 | . 42 | test.. test... test..... test?..... test!.... 43 | . 44 |

test… test… test… test?.. test!..

45 | . 46 | 47 | 48 | dupes 49 | . 50 | !!!!!! ???? ,, 51 | . 52 |

!!! ??? ,

53 | . 54 | 55 | copyright should be escapable 56 | . 57 | \(c) 58 | . 59 |

(c)

60 | . 61 | 62 | shouldn't replace entities 63 | . 64 | (c) (c) (c) 65 | . 66 |

(c) (c) ©

67 | . 68 | 69 | 70 | dashes 71 | . 72 | ---markdownit --- super--- 73 | 74 | markdownit---awesome 75 | 76 | abc ---- 77 | 78 | --markdownit -- super-- 79 | 80 | markdownit--awesome 81 | . 82 |

—markdownit — super—

83 |

markdownit—awesome

84 |

abc ----

85 |

–markdownit – super–

86 |

markdownit–awesome

87 | . 88 | 89 | dashes should be escapable 90 | . 91 | foo \-- bar 92 | 93 | foo -\- bar 94 | . 95 |

foo -- bar

96 |

foo -- bar

97 | . 98 | 99 | regression tests for #624 100 | . 101 | 1---2---3 102 | 103 | 1--2--3 104 | 105 | 1 -- -- 3 106 | . 107 |

1—2—3

108 |

1–2–3

109 |

1 – – 3

110 | . 111 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/fixtures/markdown-it/xss.txt: -------------------------------------------------------------------------------- 1 | . 2 | [normal link](javascript) 3 | . 4 |

normal link

5 | . 6 | 7 | 8 | Should not allow some protocols in links and images 9 | . 10 | [xss link](javascript:alert(1)) 11 | 12 | [xss link](JAVASCRIPT:alert(1)) 13 | 14 | [xss link](vbscript:alert(1)) 15 | 16 | [xss link](VBSCRIPT:alert(1)) 17 | 18 | [xss link](file:///123) 19 | . 20 |

[xss link](javascript:alert(1))

21 |

[xss link](JAVASCRIPT:alert(1))

22 |

[xss link](vbscript:alert(1))

23 |

[xss link](VBSCRIPT:alert(1))

24 |

[xss link](file:///123)

25 | . 26 | 27 | 28 | . 29 | [xss link]("><script>alert("xss")</script>) 30 | 31 | [xss link](Javascript:alert(1)) 32 | 33 | [xss link](&#74;avascript:alert(1)) 34 | 35 | [xss link](\Javascript:alert(1)) 36 | . 37 |

xss link

38 |

[xss link](Javascript:alert(1))

39 |

xss link

40 |

xss link

41 | . 42 | 43 | . 44 | [xss link]() 45 | . 46 |

[xss link](<javascript:alert(1)>)

47 | . 48 | 49 | . 50 | [xss link](javascript:alert(1)) 51 | . 52 |

[xss link](javascript:alert(1))

53 | . 54 | 55 | 56 | Should not allow data-uri except some whitelisted mimes 57 | . 58 | ![](data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7) 59 | . 60 |

61 | . 62 | 63 | . 64 | [xss link](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K) 65 | . 66 |

[xss link](data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K)

67 | . 68 | 69 | . 70 | [normal link](/javascript:link) 71 | . 72 |

normal link

73 | . 74 | 75 | 76 | Image parser use the same code base as link. 77 | . 78 | ![xss link](javascript:alert(1)) 79 | . 80 |

![xss link](javascript:alert(1))

81 | . 82 | 83 | 84 | Autolinks 85 | . 86 | 87 | 88 | 89 | . 90 |

<javascript:alert(1)>

91 |

<javascript:alert(1)>

92 | . 93 | 94 | 95 | Linkifier 96 | . 97 | javascript:alert(1) 98 | 99 | javascript:alert(1) 100 | . 101 |

javascript:alert(1)

102 |

javascript:alert(1)

103 | . 104 | 105 | 106 | References 107 | . 108 | [test]: javascript:alert(1) 109 | . 110 |

[test]: javascript:alert(1)

111 | . 112 | 113 | 114 | Make sure we decode entities before split: 115 | . 116 | ```js custom-class 117 | test1 118 | ``` 119 | 120 | ```js custom-class 121 | test2 122 | ``` 123 | . 124 |
test1
125 | 
126 |
test2
127 | 
128 | . 129 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/markdown_it_spec.rb: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | describe "markdown-it" do 3 | 4 | parser = MarkdownIt::Parser.new({ html: true, langPrefix: '', typographer: true, linkify: true }) 5 | datadir = File.join(File.dirname(__FILE__), 'fixtures', 'markdown-it') 6 | datafiles = (ENV['datafile'] ? [ File.join(datadir, ENV['datafile']) ] : Dir[File.join(datadir, '**/*')]) 7 | 8 | datafiles.each do |data_file| 9 | tests = get_tests(data_file) 10 | if ENV['example'] && !tests[ENV['example'].to_i - 1].nil? 11 | define_test(tests[ENV['example'].to_i - 1], parser, true) 12 | else 13 | tests.each do |t| 14 | define_test(t, parser) 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /spec/motion-markdown-it/ruler_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Ruler' do 2 | 3 | #------------------------------------------------------------------------------ 4 | it 'should replace rule (.at)' do 5 | ruler = MarkdownIt::Ruler.new 6 | res = 0 7 | 8 | ruler.push('test', lambda { res = 1 }) 9 | ruler.at('test', lambda { res = 2 }) 10 | 11 | rules = ruler.getRules('') 12 | 13 | expect(rules.length).to eq 1 14 | rules[0].call 15 | expect(res).to eq 2 16 | end 17 | 18 | 19 | #------------------------------------------------------------------------------ 20 | it 'should inject before/after rule' do 21 | ruler = MarkdownIt::Ruler.new 22 | res = 0 23 | 24 | ruler.push('test', lambda { res = 1 }) 25 | ruler.before('test', 'before_test', lambda { res = -10; }) 26 | ruler.after('test', 'after_test', lambda { res = 10; }) 27 | 28 | rules = ruler.getRules(''); 29 | 30 | expect(rules.length).to eq 3 31 | rules[0].call 32 | expect(res).to eq -10 33 | rules[1].call 34 | expect(res).to eq 1 35 | rules[2].call 36 | expect(res).to eq 10 37 | end 38 | 39 | 40 | #------------------------------------------------------------------------------ 41 | it 'should enable/disable rule' do 42 | ruler = MarkdownIt::Ruler.new 43 | 44 | ruler.push('test', lambda {}) 45 | ruler.push('test2', lambda {}) 46 | 47 | rules = ruler.getRules('') 48 | expect(rules.length).to eq 2 49 | 50 | ruler.disable('test') 51 | rules = ruler.getRules('') 52 | expect(rules.length).to eq 1 53 | ruler.disable('test2') 54 | rules = ruler.getRules('') 55 | expect(rules.length).to eq 0 56 | 57 | ruler.enable('test') 58 | rules = ruler.getRules('') 59 | expect(rules.length).to eq 1 60 | ruler.enable('test2') 61 | rules = ruler.getRules('') 62 | expect(rules.length).to eq 2 63 | end 64 | 65 | 66 | #------------------------------------------------------------------------------ 67 | it 'should enable/disable multiple rule' do 68 | ruler = MarkdownIt::Ruler.new 69 | 70 | ruler.push('test', lambda {}) 71 | ruler.push('test2', lambda {}) 72 | 73 | ruler.disable([ 'test', 'test2' ]) 74 | rules = ruler.getRules('') 75 | expect(rules.length).to eq 0 76 | ruler.enable([ 'test', 'test2' ]) 77 | rules = ruler.getRules('') 78 | expect(rules.length).to eq 2 79 | end 80 | 81 | 82 | #------------------------------------------------------------------------------ 83 | it 'should enable rules by whitelist' do 84 | ruler = MarkdownIt::Ruler.new 85 | 86 | ruler.push('test', lambda {}) 87 | ruler.push('test2', lambda {}) 88 | 89 | ruler.enableOnly('test') 90 | rules = ruler.getRules('') 91 | expect(rules.length).to eq 1 92 | end 93 | 94 | 95 | #------------------------------------------------------------------------------ 96 | it 'should support multiple chains' do 97 | ruler = MarkdownIt::Ruler.new 98 | 99 | ruler.push('test', lambda {}) 100 | ruler.push('test2', lambda {}, { alt: [ 'alt1' ] }) 101 | ruler.push('test2', lambda {}, { alt: [ 'alt1', 'alt2' ] }) 102 | 103 | rules = ruler.getRules('') 104 | expect(rules.length).to eq 3 105 | rules = ruler.getRules('alt1') 106 | expect(rules.length).to eq 2 107 | rules = ruler.getRules('alt2'); 108 | expect(rules.length).to eq 1 109 | end 110 | 111 | 112 | #------------------------------------------------------------------------------ 113 | it 'should fail on invalid rule name' do 114 | ruler = MarkdownIt::Ruler.new 115 | 116 | ruler.push('test', lambda {}) 117 | 118 | expect { 119 | ruler.at('invalid name', lambda {}) 120 | }.to raise_error(StandardError) 121 | expect { 122 | ruler.before('invalid name', lambda {}) 123 | }.to raise_error(StandardError) 124 | expect { 125 | ruler.after('invalid name', lambda {}) 126 | }.to raise_error(StandardError) 127 | expect { 128 | ruler.enable('invalid name') 129 | }.to raise_error(StandardError) 130 | expect { 131 | ruler.disable('invalid name') 132 | }.to raise_error(StandardError) 133 | end 134 | 135 | 136 | #------------------------------------------------------------------------------ 137 | it 'should not fail on invalid rule name in silent mode' do 138 | ruler = MarkdownIt::Ruler.new 139 | 140 | ruler.push('test', lambda {}) 141 | 142 | expect { 143 | ruler.enable('invalid name', true) 144 | }.not_to raise_error 145 | expect { 146 | ruler.enableOnly('invalid name', true) 147 | }.not_to raise_error 148 | expect { 149 | ruler.disable('invalid name', true) 150 | }.not_to raise_error 151 | end 152 | 153 | end 154 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/testgen_helper.rb: -------------------------------------------------------------------------------- 1 | # Markdown-It ignores CM where an empty blockquote tag gets rendered with a 2 | # newline. This changes the output so that the tests pass [2058, 2381, and 2388] 3 | #------------------------------------------------------------------------------ 4 | def normalize(text) 5 | return text.gsub(/
\n<\/blockquote>/, '
') 6 | end 7 | 8 | #------------------------------------------------------------------------------ 9 | def get_tests(specfile) 10 | line_number = 0 11 | start_line = 0 12 | end_line = 0 13 | example_number = 0 14 | markdown_lines = [] 15 | html_lines = [] 16 | state = 0 # 0 regular text, 1 markdown example, 2 html output 17 | headertext = '' 18 | tests = [] 19 | header_re = /#+ / 20 | filename = File.basename(specfile) 21 | 22 | File.open(specfile) do |specf| 23 | specf.each_line do |line| 24 | line_number += 1 25 | if state == 0 && header_re =~ line 26 | headertext = line.gsub(header_re, '').strip 27 | end 28 | if line.strip == "." 29 | state = (state + 1) % 3 30 | if state == 0 31 | example_number += 1 32 | end_line = line_number 33 | tests << { 34 | markdown: markdown_lines.join.gsub('→',"\t"), 35 | html: html_lines.join, 36 | example: example_number, 37 | start_line: start_line, 38 | end_line: end_line, 39 | section: headertext, 40 | filename: filename} 41 | start_line = 0 42 | markdown_lines = [] 43 | html_lines = [] 44 | end 45 | elsif state == 1 46 | if start_line == 0 47 | start_line = line_number - 1 48 | end 49 | markdown_lines << line 50 | elsif state == 2 51 | html_lines << line 52 | end 53 | end 54 | end 55 | return tests 56 | end 57 | 58 | #------------------------------------------------------------------------------ 59 | def define_test(testcase, parser, debug_tokens = false) 60 | 61 | it "#{testcase[:filename]} #{testcase[:section]} (#{testcase[:example].to_s}/#{testcase[:start_line]}-#{testcase[:end_line]}) with markdown:\n#{testcase[:markdown]}" do 62 | if debug_tokens 63 | parser.parse(testcase[:markdown], { references: {} }).each {|token| pp token.to_json} 64 | end 65 | expect(parser.render(testcase[:markdown])).to eq normalize(testcase[:html]) 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/token_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Token" do 2 | 3 | it 'attr' do 4 | t = MarkdownIt::Token.new('test_token', 'tok', 1) 5 | 6 | expect(t.attrs).to eq nil 7 | expect(t.attrIndex('foo')).to eq -1 8 | 9 | t.attrPush([ 'foo', 'bar' ]) 10 | t.attrPush([ 'baz', 'bad' ]) 11 | 12 | expect(t.attrIndex('foo')).to eq 0 13 | expect(t.attrIndex('baz')).to eq 1 14 | expect(t.attrIndex('none')).to eq -1 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /spec/motion-markdown-it/utils_spec.rb: -------------------------------------------------------------------------------- 1 | include MarkdownIt::Common::Utils 2 | 3 | describe 'Utils' do 4 | 5 | #------------------------------------------------------------------------------ 6 | it 'fromCodePoint' do 7 | expect(fromCodePoint(0x20)).to eq ' ' 8 | expect(fromCodePoint(0x1F601)).to eq '😁' 9 | end 10 | 11 | #------------------------------------------------------------------------------ 12 | it 'isValidEntityCode' do 13 | expect(isValidEntityCode(0x20)).to eq true 14 | expect(isValidEntityCode(0xD800)).to eq false 15 | expect(isValidEntityCode(0xFDD0)).to eq false 16 | expect(isValidEntityCode(0x1FFFF)).to eq false 17 | expect(isValidEntityCode(0x1FFFE)).to eq false 18 | expect(isValidEntityCode(0x00)).to eq false 19 | expect(isValidEntityCode(0x0B)).to eq false 20 | expect(isValidEntityCode(0x0E)).to eq false 21 | expect(isValidEntityCode(0x7F)).to eq false 22 | end 23 | 24 | #------------------------------------------------------------------------------ 25 | it 'assign' do 26 | expect(assign({ a: 1 }, nil, { b: 2 })).to eq ({ a: 1, b: 2 }) 27 | expect { 28 | assign({}, 123) 29 | }.to raise_error(StandardError) 30 | end 31 | 32 | #------------------------------------------------------------------------------ 33 | it 'escapeRE' do 34 | expect(escapeRE(' .?*+^$[]\\(){}|-')).to eq ' \\.\\?\\*\\+\\^\\$\\[\\]\\\\\\(\\)\\{\\}\\|\\-' 35 | end 36 | 37 | #------------------------------------------------------------------------------ 38 | it 'isWhiteSpace' do 39 | expect(isWhiteSpace(0x2000)).to eq true 40 | expect(isWhiteSpace(0x09)).to eq true 41 | 42 | expect(isWhiteSpace(0x30)).to eq false 43 | end 44 | 45 | #------------------------------------------------------------------------------ 46 | it 'isMdAsciiPunct' do 47 | expect(isMdAsciiPunct(0x30)).to eq false 48 | 49 | '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split('').each do |ch| 50 | expect(isMdAsciiPunct(ch.ord)).to eq true 51 | end 52 | end 53 | 54 | #------------------------------------------------------------------------------ 55 | it 'unescapeMd' do 56 | expect(unescapeMd('\\foo')).to eq '\\foo' 57 | expect(unescapeMd('foo')).to eq 'foo' 58 | 59 | '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'.split('').each do |ch| 60 | expect(unescapeMd('\\' + ch)).to eq ch 61 | end 62 | end 63 | 64 | #------------------------------------------------------------------------------ 65 | it "escapeHtml" do 66 | str = '
x & "y"' 67 | expect(escapeHtml(str)).to eq '<hr>x & "y"' 68 | end 69 | 70 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # make sure to have `--require spec_helper` in `.rspec` to have the 2 | # spec_helper.rb included automatically in spec files 3 | require 'motion-markdown-it/testgen_helper' 4 | require 'motion-markdown-it' 5 | 6 | require 'pry' 7 | --------------------------------------------------------------------------------