├── .gitignore ├── .gitmodules ├── .rubocop.yml ├── .yardopts ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── benchmark └── .gitignore ├── bin └── optdown ├── lib ├── optdown.rb └── optdown │ ├── always_frozen.rb │ ├── atx_heading.rb │ ├── autolink.rb │ ├── blockhtml.rb │ ├── blocklevel.rb │ ├── blockquote.rb │ ├── code_span.rb │ ├── deeply_frozen.rb │ ├── emphasis.rb │ ├── entity.rb │ ├── escape.rb │ ├── expr.rb │ ├── fenced_code_block.rb │ ├── flanker.rb │ ├── html5entity.erb │ ├── html5entity.rb │ ├── indented_code_block.rb │ ├── inline.rb │ ├── link.rb │ ├── link_def.rb │ ├── link_title.rb │ ├── list.rb │ ├── list_item.rb │ ├── matcher.rb │ ├── newline.rb │ ├── paragraph.rb │ ├── parser.rb │ ├── plugins │ ├── houdini_compat.rb │ ├── html_renderer.rb │ └── plaintext_renderer.rb │ ├── raw_html.rb │ ├── renderer.rb │ ├── setext_heading.rb │ ├── strikethrough.rb │ ├── table.rb │ ├── thematic_break.rb │ ├── token.rb │ └── xprintf.rb └── test ├── .gitignore ├── 000_compile.rb ├── always_frozen.rb ├── blocklevel.rb ├── deeply_frozen.rb ├── expr.rb ├── inline.rb ├── integrated.rb ├── matcher.rb ├── pathological.rb ├── renderer.rb ├── test_helper.rb └── xprintf.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017 Urabe, Shyouhei 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be 11 | # included in all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | .DS_Store 22 | .bundle 23 | .byebug_history 24 | .ruby-version 25 | .yardoc 26 | Gemfile.lock 27 | coverage 28 | doc 29 | vendor 30 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/progit"] 2 | path = submodules/progit 3 | url = git@github.com:progit/progit.git 4 | [submodule "submodules/CommonMark"] 5 | path = submodules/CommonMark 6 | url = git@github.com:commonmark/CommonMark.git 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/rubocop 2 | # -*- mode: yaml; coding: utf-8 -*- 3 | 4 | # Copyright (c) 2017 Urabe, Shyouhei 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | AllCops: 25 | DisabledByDefault: false 26 | DisplayCopNames: true 27 | Exclude: 28 | - "submodules/**/*" 29 | - "vendor/**/*" 30 | - 'test/**/*' 31 | - '*.gemspec' 32 | TargetRubyVersion: 2.4 33 | 34 | Layout: 35 | Enabled: false 36 | 37 | Lint/AmbiguousBlockAssociation: 38 | Enabled: false 39 | 40 | Lint/AssignmentInCondition: 41 | Enabled: false 42 | 43 | Lint/EmptyWhen: 44 | Enabled: false 45 | 46 | Lint/LiteralInCondition: 47 | Enabled: false 48 | 49 | Lint/ScriptPermission: 50 | Enabled: false 51 | 52 | Lint/UselessAccessModifier: 53 | # I think this cop is buggy as of 0.49 54 | Enabled: false 55 | 56 | Lint/UnusedMethodArgument: 57 | Enabled: false 58 | 59 | Metrics/AbcSize: 60 | Enabled: false 61 | 62 | Metrics/BlockLength: 63 | Enabled: false 64 | 65 | Metrics/ClassLength: 66 | Enabled: false 67 | 68 | Metrics/CyclomaticComplexity: 69 | Enabled: false 70 | 71 | Metrics/MethodLength: 72 | Enabled: false 73 | 74 | Metrics/PerceivedComplexity: 75 | # What the f* is this thing? 76 | Enabled: false 77 | 78 | Naming: 79 | Enabled: false 80 | 81 | Performance: 82 | Enabled: false 83 | 84 | Rails: 85 | Enabled: false 86 | 87 | Security/Eval: 88 | Enabled: false 89 | 90 | Security/MarshalLoad: 91 | Enabled: false 92 | 93 | Style: 94 | Enabled: false 95 | 96 | Metrics/LineLength: 97 | AllowURI: true 98 | Max: 80 99 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup-provider=redcarpet 2 | --markup=markdown 3 | --charset utf-8 4 | --readme README.md 5 | lib/**/*.rb 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/bundler 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | source 'https://rubygems.org' 27 | ruby '>= 2.4.1' # for Onigmo 6 28 | 29 | group :development do 30 | gem 'bundler' 31 | gem 'json' 32 | gem 'pry-byebug' 33 | gem 'rake' 34 | gem 'redcarpet' 35 | gem 'yard' 36 | end 37 | 38 | group :test do 39 | gem 'benchmark-ips' 40 | gem 'rubocop' 41 | gem 'simplecov' 42 | gem 'stackprof' 43 | gem 'test-unit', '>= 3' 44 | end 45 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Urabe, Shyouhei 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be 11 | included in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `optdown`: Pure-ruby GFM[[1]] parser. 2 | 3 | This simple script converts a GFM into an HTML. 4 | 5 | ## Background stories 6 | 7 | In Japan, especially when it comes to write a physical book in computer science or programming, Markdown is widely used by programmers / tech writers. 8 | 9 | This sounds to be a good thing but in reality, not an easy job to do. Japanese writing system tends to be complex. HTML-rendered composition of Japanese language has not (yet) reached to certain level of quality. Editors are forced to use other layout engines such as InDesign. So, there are needs to convert a Markdown document into other formats than HTML. The situation is severe because there is no such thing like a "standardized markup of Japanese books". Each book editors have distinct home-grown text markup so that then they can cut & paste the marked-up text file into InDesign (by hand). 10 | 11 | The authors hope the status quo to become relaxed someday somehow. For the meantime we developed a tiny program to technically tackle _some_ part of the problem; that there is no such thing like a Markdown parser that renders arbitrary 3rd-party markup. This former program was not made public. 12 | 13 | In developing such non-HTML Markdown parser the authors found a fact that (1) Markdown, while it seems to be, is not a trivial language; and (2) however, they can be parsed using pure-ruby only. Based on this finding we thought that this Markdown-parsing problem could be a good benchmark for the Ruby interpreter itself. 14 | 15 | We added an HTML rendering mode to the library, deleted 3rd-party markup modes, and this program was born. 16 | 17 | ## What it is 18 | 19 | This parser understands GFM. 20 | 21 | What is generated by parsing a Markdown document is a DOM object. At the very least you can traverse the DOM tree. Along with the DOM object we provide several "visitor classes" that understands the given DOM to render whatever transformed output. You can create one by subclassing some pre-existing visitor class. 22 | 23 | ## Why the yet another Markdown parser? 24 | 25 | ### General 26 | 27 | - It is standards compliant. As of writing we support `0.28-gfm (2017-08-01)`. 28 | - It is fully multilingualized; proper handling of complex Japanese texts implemented through Ruby's multilingualization features (note: emojis are Japan rooted). 29 | - It is extensible; though not bundled, any 3rd party target format can be made. 30 | - It is pure-ruby; this property sacrifices runtime speed for maximum portability. 31 | 32 | ### Why not redcarpet[[2]] 33 | 34 | - First off, the author would like to say that redcarpet IS great. We started by extending the library before ended up writing this repo. 35 | - Yet, it is not CommonMark compatible. 36 | - Also, it does not expose its DOM. All you can do is to customize HTML rendering. 37 | - One of the authors experienced SEGV using it before[[3]]. 38 | 39 | ### Why not cmark[[4]] / commonmarker[[5]] 40 | 41 | - Cmark is pretty well written. Extraordinary fast to run, and error-prone. 42 | - That being said, cmark supports only fixed kinds of output formats like MAN, HTML, XML, COMMONMARK, and TEX. It seems to me that github/cmark[[6]] ended up forking the library to support FORMAT_PLAINTEXT. This is not a good property. 43 | - Commommarker is a good gem. People should consider using it if possible. However it is a C extension that needs a compiler. Not for our needs. 44 | 45 | ## Limitations 46 | 47 | - Slower than any other known Markdown library as of writing. We are dead serious. This program is order of magnitude slower than other practical Markdown parsers which claim to be GFM compatible. The authors think this is due to Ruby's interpreter being not optimized enough. 48 | - While it is relatively straight-forward to write a new format, it is not known to be easy to override existing Markdown syntax; e.g. to introduce elastic tabstop is a challenge. 49 | 50 | ## Legal 51 | 52 | The authors believe they have not employed any 3rd-party intellectual properties except ones listed below. Consult LICENSE.txt[[7]] for detailed usage of this software. TL;DR: it is MIT-licensed. 53 | 54 | ### 3rd-party intellectual properties 55 | 56 | - Lawful quotations of the CommonMark spec is included; which is under CC-BY-SA 4.0. 57 | - File named `html5entity.rb` is a delivation of a source code provided by W3C[[8]]; which is licensed under W3C document license[[9]]. Here is the copyright notice that the license requests us to show: 58 | 59 | Copyright © 2015 W3C® (MIT, ERCIM, Keio, Beihang). This software or document includes material copied from or derived from https://www.w3.org/TR/html5/entities.json 60 | 61 | ### HTML5 related legal twist 62 | 63 | At one point the GFM spec refers to the HTML Living Standard[[10]]. However, the authors cannot find any license terms of it. Circumstantial evidence shows that the standard is not seriously licensed in any way. We are afraid of such thing to pollute my codes -- at least, we have no idea if the WHATWG HTML is MIT compatible. 64 | 65 | So instead, we hereby refer to the W3C definition of HTML5[[11]]. It is at least explicitly licensed and the license tells us that it is safe to use. 66 | 67 | ## Acknowledgment 68 | 69 | The authors would like to show our gratitude to Tanaka Akira for his works on extending regular expression grammars. Two key features which made this entire project possible (namely the absent operator and the subexp call) were both invented by him. 70 | 71 | [1]: https://github.github.com/gfm/ 72 | [2]: https://github.com/vmg/redcarpet 73 | [3]: https://twitter.com/shyouhei/status/833513136946679814 74 | [4]: https://github.com/CommonMark/cmark 75 | [5]: https://github.com/gjtorikian/commonmarker 76 | [6]: https://github.com/github/cmark 77 | [7]: https://github.com/shyouhei/markdown_parser/blob/master/LICENSE.txt 78 | [8]: https://www.w3.org/TR/html5/entities.json 79 | [9]: https://www.w3.org/Consortium/Legal/2015/doc-license 80 | [10]: https://html.spec.whatwg.org/multipage/ 81 | [11]: https://www.w3.org/TR/html5/ 82 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/rake 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require 'rubygems' 27 | require 'bundler/setup' 28 | Bundler.setup :development, :test 29 | require 'rake' 30 | require 'yard' 31 | require 'rake/testtask' 32 | require 'rubocop/rake_task' 33 | 34 | YARD::Rake::YardocTask.new 35 | RuboCop::RakeTask.new 36 | 37 | task default: :test 38 | task spec: :test 39 | desc "run tests" 40 | Rake::TestTask.new do |t| 41 | t.test_files = FileList['test/**/*.rb'] - ['test/test_helper.rb'] 42 | t.warning = true 43 | end 44 | 45 | desc "pry console" 46 | task :pry do 47 | require_relative 'lib/optdown' 48 | require 'pry' 49 | Pry.start 50 | end 51 | task c: :pry 52 | task console: :pry 53 | 54 | desc "run script under project" 55 | task :runner do 56 | require_relative 'lib/optdown' 57 | ARGV.shift while ARGV.first != 'runner' 58 | ARGV.shift 59 | eval ARGF.read, TOPLEVEL_BINDING, '(ARGF)' 60 | end 61 | 62 | file 'lib/optdown/html5entity.rb' => 'lib/optdown/html5entity.erb' do |t| 63 | require 'open-uri' 64 | require 'erb' 65 | require 'json' 66 | URI('https://www.w3.org/TR/html5/entities.json').open do |fp| 67 | # For this use of create_additions option: 68 | # @see https://www.ruby-lang.org/en/news/2013/02/22/json-dos-cve-2013-0269/ 69 | entities = JSON.parse fp.read, create_additions: false 70 | path = t.prerequisites.first 71 | src = File.read path 72 | erb = ERB.new src, nil, '%-' 73 | erb.filename = path 74 | b = TOPLEVEL_BINDING.dup 75 | b.local_variable_set 'entities', entities 76 | dst = erb.result b 77 | File.write t.name, dst 78 | end 79 | end 80 | 81 | task :submodule do 82 | sh 'git submodule update --init --recursive' 83 | end 84 | 85 | file 'test/spec.json' => 'submodules/CommonMark/spec.txt' do |f| 86 | sh 'make -C submodules/CommonMark spec.json' 87 | rm_r 'submodules/CommonMark/test/__pycache__' 88 | mv 'submodules/CommonMark/spec.json', f.name 89 | end 90 | 91 | file 'benchmark/input.md' => 'submodules/progit/README.md' do |f| 92 | require 'pathname' 93 | dir = Pathname.new __dir__ 94 | dest = dir + f.name 95 | src = dir + 'submodules/progit' 96 | dest.open 'a' do |fp| 97 | src \ 98 | . find \ 99 | . lazy \ 100 | . select {|i| i.fnmatch '*.{markdown,md}', File::FNM_EXTGLOB } \ 101 | . each {|i| IO.copy_stream i.to_path, fp } 102 | end 103 | end 104 | 105 | task :benchmark => 'benchmark/input.md' do 106 | # usage: rake benchmark -- prog prog prog ... 107 | require 'benchmark/ips' 108 | Benchmark.ips do |x| 109 | progs = ARGV.drop_while {|i| i != '--' } 110 | progs.shift 111 | progs.each do |prog| 112 | x.report prog do 113 | sh "#{prog} benchmark/input.md > /dev/null", verbose: false 114 | end 115 | end 116 | end 117 | end 118 | 119 | task test: 'test/spec.json' 120 | task prepare: :submodule 121 | task prepare: 'lib/optdown/html5entity.rb' 122 | task prepare: 'test/spec.json' 123 | task prepare: 'benchmark/input.md' 124 | -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | input.md 2 | -------------------------------------------------------------------------------- /bin/optdown: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative '../lib/optdown' 27 | 28 | parser = Optdown::Parser.new 29 | renderer = Optdown::HTMLRenderer.new 30 | dom = parser.parse ARGF.read 31 | html = renderer.render dom 32 | 33 | puts html 34 | -------------------------------------------------------------------------------- /lib/optdown.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | ; 26 | 27 | module Optdown 28 | VERSION = 1 29 | 30 | require_relative 'optdown/html5entity' 31 | require_relative 'optdown/deeply_frozen' 32 | require_relative 'optdown/always_frozen' 33 | require_relative 'optdown/expr' 34 | require_relative 'optdown/xprintf' 35 | require_relative 'optdown/matcher' 36 | require_relative 'optdown/token' 37 | require_relative 'optdown/flanker' 38 | require_relative 'optdown/emphasis' 39 | require_relative 'optdown/link' 40 | require_relative 'optdown/strikethrough' 41 | require_relative 'optdown/autolink' 42 | require_relative 'optdown/raw_html' 43 | require_relative 'optdown/code_span' 44 | require_relative 'optdown/entity' 45 | require_relative 'optdown/escape' 46 | require_relative 'optdown/newline' 47 | require_relative 'optdown/inline' 48 | require_relative 'optdown/paragraph' 49 | require_relative 'optdown/table' 50 | require_relative 'optdown/setext_heading' 51 | require_relative 'optdown/atx_heading' 52 | require_relative 'optdown/indented_code_block' 53 | require_relative 'optdown/fenced_code_block' 54 | require_relative 'optdown/blockhtml' 55 | require_relative 'optdown/list_item' 56 | require_relative 'optdown/list' 57 | require_relative 'optdown/blockquote' 58 | require_relative 'optdown/link_def' 59 | require_relative 'optdown/thematic_break' 60 | require_relative 'optdown/blocklevel' 61 | require_relative 'optdown/parser' 62 | require_relative 'optdown/renderer' 63 | require_relative 'optdown/plugins/html_renderer.rb' 64 | end 65 | -------------------------------------------------------------------------------- /lib/optdown/always_frozen.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # By prepending this module your class gets frozen. Instances of your class 27 | # can no longer be modifiable. You cannot but create a new instance to modify 28 | # something. 29 | # 30 | # Due to ruby's language restriction this module has no public methods. 31 | module Optdown::AlwaysFrozen 32 | private 33 | 34 | def self.included *; 35 | raise "#{self} must be prepended, not included" 36 | end 37 | 38 | private_class_method :included 39 | 40 | # Following two method definitions are lexically identical. However you 41 | # cannot merge them into one, because what `super` resolves to differs. 42 | 43 | def initialize_copy *; 44 | super 45 | freeze 46 | end 47 | 48 | def initialize *; 49 | super 50 | freeze 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/optdown/atx_heading.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'inline' 27 | 28 | # @see http://spec.commonmark.org/0.28/#atx-headings 29 | class Optdown::ATXHeading 30 | 31 | attr_reader :level # return [Integer] heading level 32 | 33 | # (see Optdown::Blocklevel#initialize) 34 | def initialize str, ctx 35 | @level = str['atx:open'].length 36 | # > The raw contents of the heading are stripped of leading and trailing 37 | # > spaces before being parsed as inline content. 38 | @children = Optdown::Inline.from_stripped str['atx:body'], ctx 39 | end 40 | 41 | # (see Optdown::Blocklevel#accept) 42 | def accept visitor, tightp: false 43 | inner = visitor.visit @children 44 | return visitor.visit_heading self, inner 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/optdown/autolink.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | 27 | # http://spec.commonmark.org/0.28/#autolinks 28 | class Optdown::Autolink 29 | attr_reader :href # @return [String] the link. 30 | attr_reader :display # @return [String] the content. 31 | 32 | # @param tok [Token] terminal token. 33 | def initialize tok 34 | md = tok.yylval 35 | case 36 | when md['auto:URI'] then @href = md['auto:URI'] 37 | when md['auto:mail'] then @href = 'mailto:' + md['auto:mail'] 38 | when md['auto:GH:www'] then @href = 'http://' + tok.to_s 39 | when md['auto:GH:url'] then @href = tok.to_s 40 | when md['auto:GH:email'] then @href = 'mailto:' + tok.to_s 41 | end 42 | @display = md['auto:URI'] || md['auto:mail'] || tok.to_s 43 | end 44 | 45 | # (see Optdown::Inline#accept) 46 | def accept visitor 47 | return visitor.visit_auto_link self 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/optdown/blockhtml.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'xprintf' 28 | 29 | # @see http://spec.commonmark.org/0.28/#html-blocks 30 | class Optdown::BlockHTML 31 | using Optdown::XPrintf 32 | 33 | attr_reader :html # @return [Matcher] the HTML content. 34 | 35 | # (see Optdown::Blocklevel#initialize) 36 | def initialize str, ctx 37 | md = str.last_match 38 | open = str[0] 39 | re = Optdown::EXPR # easy typing. 40 | case 41 | when md['tag:start1'] then term = /#{re}\g/o 42 | when md['tag:start2'] then term = /#{re}\g/o 43 | when md['tag:start3'] then term = /#{re}\g/o 44 | when md['tag:start4'] then term = /#{re}\g/o 45 | when md['tag:start5'] then term = /#{re}\g/o 46 | when md['tag:start6'] then term = nil 47 | when md['tag:start7'] then term = nil 48 | else rprintf RuntimeError, "logical bug: unknown match %p", md 49 | end 50 | 51 | # > If the first line meets both the start condition and the end condition, 52 | # > the block will contain just that line. 53 | # 54 | # @see http://spec.commonmark.org/0.28/#html-blocks 55 | list = [] 56 | line = Optdown::Matcher.join [ open, str.gets ] # the first line. 57 | loop do 58 | list << line 59 | break if str.eos? 60 | break if term and line.match? term 61 | break if (! term) and str.match? %r/#{re}\G\g/o 62 | line = str.gets 63 | end 64 | @html = Optdown::Matcher.join list 65 | end 66 | 67 | # (see Optdown::Blocklevel#accept) 68 | def accept visitor, tightp: false 69 | return visitor.visit_blockhtml self 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/optdown/blocklevel.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'matcher' 28 | 29 | # DOM top level. It seems the only thing that the spec says about such thing is 30 | # the following: 31 | # 32 | # > # Blocks and inlines 33 | # > 34 | # > We can think of a document as a sequence of blocks -- (snip). 35 | # 36 | # @see http://spec.commonmark.org/0.28/#blocks-and-inlines 37 | class Optdown::Blocklevel 38 | using Optdown::Matcher::Refinements 39 | 40 | attr_reader :children # @return [Array] description TBW. 41 | 42 | # Utility constructor to join the lines before calling new. 43 | # @param lines [Array] target lines to join. 44 | # @param ctx [Parser] parser context. 45 | def self.from_lines lines, ctx 46 | str = Optdown::Matcher.join lines 47 | return new str, ctx 48 | end 49 | 50 | # Parse the argument to construct AST. 51 | # 52 | # @note In contrast to inline elements who need clear separation of tokenize 53 | # and parse, blocklevel elements can be parsed in sequence. 54 | # @param str [Matcher] target to scan. 55 | # @param ctx [Parser] parser context. 56 | def initialize str, ctx 57 | @children = [] 58 | @blank_seen = false 59 | re = Optdown::EXPR # easy typing. 60 | until str.eos? do 61 | case str # ORDER MATTERS HERE 62 | when /#{re} \G \g /xo then 63 | # FAST PATH 64 | case 65 | when str['hr:chr'] then k = Optdown::ThematicBreak 66 | when str['link:def'] then k = Optdown::LinkDef 67 | when str['blockquote'] then k = Optdown::Blockquote 68 | when str['li'] then k = Optdown::List 69 | when str['tag:block'] then k = Optdown::BlockHTML 70 | when str['LINE:blank'] then k = nil # need check after tags 71 | when str['pre:fenced'] then k = Optdown::FencedCodeBlock 72 | when str['atx'] then k = Optdown::ATXHeading 73 | end 74 | when /#{re} \G \g /xo then k = Optdown::IndentedCodeBlock 75 | when /#{re} \G (?= \g ) /xo then k = Optdown::SetextHeading 76 | when /#{re} \G (?= \g ) /xo then k = Optdown::Table 77 | else k = Optdown::Paragraph 78 | end 79 | 80 | if k then 81 | node = k.new str, ctx 82 | @children << node 83 | else 84 | @blank_seen = !@children.empty? && !str.eos? 85 | end 86 | end 87 | end 88 | 89 | # Makes sense when this blocklevel is inside of a list item. 90 | # 91 | # @return [true] it is. 92 | # @return [false] it isn't. 93 | def tight? 94 | return ! @blank_seen 95 | end 96 | 97 | # Traverse the tree 98 | # 99 | # @param visitor [Renderer] rendering visitor. 100 | # @param tightp [true,false] tightness. 101 | # @return [Object] visitor visiting result. 102 | def accept visitor, tightp: false 103 | inner = @children.map {|i| visitor.visit i, tightp: tightp } 104 | return visitor.visit_blocklevel self, inner 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/optdown/blockquote.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'matcher' 28 | 29 | # @see http://spec.commonmark.org/0.28/#block-quotes 30 | class Optdown::Blockquote 31 | using Optdown::Matcher::Refinements 32 | 33 | attr_reader :children # (see Optdown::Blocklevel#children) 34 | 35 | # (see Optdown::Blocklevel#initialize) 36 | def initialize str, ctx 37 | lines = [ ] 38 | until str.eos? do 39 | line = str.gets 40 | lines << line 41 | unless line.match? %r/#{Optdown::EXPR}\G 42 | # :FIXME: this is mostly okay but inaccurate. 43 | ( \g | \g
| \g | 44 | \g | \g | \g ) 45 | /xo then 46 | until str.match? %r/#{Optdown::EXPR}\G\g/o do 47 | lines << Optdown::Paragraph::PAD 48 | lines << str.gets 49 | end 50 | end 51 | break unless str.match %r/#{Optdown::EXPR}\G\g\g
/o 52 | end 53 | @children = Optdown::Blocklevel.from_lines lines, ctx 54 | end 55 | 56 | # (see Optdown::Blocklevel#accept) 57 | def accept visitor, tightp: false 58 | inner = visitor.visit @children 59 | return visitor.visit_blockquote self, inner 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/optdown/code_span.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | 27 | # @see http://spec.commonmark.org/0.28/#code-spans 28 | class Optdown::CodeSpan 29 | attr_reader :level # @return [Integer] number of backticks. 30 | attr_reader :entity # @return [String] the content. 31 | 32 | # @param tok [Token] terminal token. 33 | def initialize tok 34 | @level = tok.yylval['code:start'].length 35 | @entity = squash tok.yylval['code:body'] 36 | end 37 | 38 | private 39 | 40 | # > The contents of the code span are the characters between the two 41 | # > backtick strings, with leading and trailing spaces and line endings 42 | # > removed, and whitespace collapsed to single spaces. 43 | # 44 | # @see http://spec.commonmark.org/0.28/#code-span 45 | def squash str 46 | ret = str.to_s.dup 47 | ret.gsub! %r/#{Optdown::EXPR}\A\g/o, '' 48 | ret.gsub! %r/#{Optdown::EXPR}\g\z/o, '' 49 | ret.gsub! %r/#{Optdown::EXPR}\g\z/o, '' 50 | ret.gsub! %r/#{Optdown::EXPR}\g/o, ' ' 51 | return ret 52 | end 53 | 54 | public 55 | 56 | # (see Optdown::Inline#accept) 57 | def accept visitor 58 | return visitor.visit_code_span self 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/optdown/deeply_frozen.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: false -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # This module is a Refinements. It introduces Object#deeply_frozen_copy. 27 | module Optdown::DeeplyFrozen 28 | refine Object do 29 | 30 | private 31 | 32 | # Recursively freeze everything inside, no matter what the given object is. 33 | # 34 | # @param x [Object] anything. 35 | # @return [Object] deeply frozen copy of x. 36 | # @note @shyouhei recommends you to read the implementation. 37 | # This is fascinating. 38 | def deeply_frozen_copy_of x 39 | str = Marshal.dump x 40 | ary = Array.new 41 | ret = Marshal.load str, ->(y) { ary.push y; y } 42 | ary.each(&:freeze) 43 | return ret 44 | end 45 | 46 | public 47 | 48 | # Recursively freeze everything inside, no matter what it is. 49 | # 50 | # @return [Object] deeply frozen copy of self. 51 | def deeply_frozen_copy 52 | return deeply_frozen_copy_of self 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/optdown/emphasis.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: false -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'flanker' 27 | require_relative 'token' 28 | 29 | # @see http://spec.commonmark.org/0.28/#emphasis-and-strong-emphasis 30 | module Optdown::Emphasis 31 | 32 | # Emphasis of `*`, `**`, `***`, ... 33 | class Aster < Optdown::Flanker 34 | 35 | # > A single `*` character can open emphasis iff (if and only if) it is 36 | # > part of a left-flanking delimiter run. 37 | # 38 | # @see http://spec.commonmark.org/0.27/#emphasis-and-strong-emphasis 39 | # @param tok [Optdown::Token] token in question 40 | # @return [true] it is. 41 | # @return [false] it isn't. 42 | def self.opener? tok 43 | return super && tok.yylval['flanker:run:*'] 44 | end 45 | 46 | # > A single `*` character can close emphasis iff it is part of a right- 47 | # > flanking delimiter run. 48 | # 49 | # @see http://spec.commonmark.org/0.27/#emphasis-and-strong-emphasis 50 | # @param tok [Optdown::Token] token in question 51 | # @return [true] it is. 52 | # @return [false] it isn't. 53 | def self.closer? tok 54 | return super && tok.yylval['flanker:run:*'] 55 | end 56 | 57 | attr_reader :level # @return [Integer] nesting. 58 | 59 | # (see Optdown::Inline#accept) 60 | def accept visitor 61 | return visitor.visit_emphasis self, @children.map {|i| visitor.visit i } 62 | end 63 | 64 | def initialize open, body, close 65 | @level = open.to_s.length 66 | @children = body 67 | end 68 | end 69 | 70 | # Emphasis of `_`, `__`, `___`, ... 71 | # 72 | # @note this is complicated than Aster. 73 | class Under < Optdown::Flanker 74 | 75 | # > A single `_` character can open emphasis iff it is part of a left- 76 | # > flanking delimiter run and either (a) not part of a right-flanking 77 | # > delimiter run or (b) part of a right-flanking delimiter run preceded by 78 | # > punctuation. 79 | # 80 | # @see http://spec.commonmark.org/0.28/#emphasis-and-strong-emphasis 81 | # @param tok [Optdown::Flanker::Token] token in question 82 | # @return [true] it is. 83 | # @return [false] it isn't. 84 | def self.opener? tok 85 | return false unless super 86 | md = tok.yylval 87 | return false unless md['flanker:run:_'] 88 | return false unless md['flanker:left'] 89 | return true unless md['flanker:right'] # (a) 90 | return %r/#{Optdown::EXPR}\g\z/o.match? md.pre_match # (b) 91 | end 92 | 93 | # > A single `_` character can close emphasis iff it is part of a right- 94 | # > flanking delimiter run and either (a) not part of a left-flanking 95 | # > delimiter run or (b) part of a left-flanking delimiter run followed by 96 | # > punctuation. 97 | # 98 | # @see http://spec.commonmark.org/0.28/#emphasis-and-strong-emphasis 99 | # @param tok [Optdown::Flanker::Run] token in question 100 | # @return [true] it is. 101 | # @return [false] it isn't. 102 | def self.closer? tok 103 | return false unless super 104 | md = tok.yylval 105 | return false unless md['flanker:run:_'] 106 | return false unless md['flanker:right'] 107 | return true unless md['flanker:left'] # (a) 108 | return %r/#{Optdown::EXPR}\A\g/o.match? md.post_match # (b) 109 | end 110 | 111 | attr_reader :level # @return [Integer] nesting. 112 | 113 | # (see Optdown::Inline#accept) 114 | def accept visitor 115 | return visitor.visit_emphasis self, @children.map {|i| visitor.visit i } 116 | end 117 | 118 | def initialize open, body, close 119 | @level = open.to_s.length 120 | @children = body 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/optdown/entity.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'html5entity' 27 | 28 | # @see http://spec.commonmark.org/0.28/#entity-references 29 | class Optdown::Entity 30 | attr_reader :token # @return [String] original representation. 31 | attr_reader :entity # @return [String] escaped entity. 32 | 33 | # @param tok [Token] terminal token. 34 | def initialize tok 35 | md = tok.yylval 36 | @token = md['entity'] 37 | @entity = case 38 | when e = md['entity:hex'] then encode e.to_i(16) 39 | when e = md['entity:dec'] then encode e.to_i(10) 40 | when e = md['entity:named'] then 41 | f = Optdown::HTML5ENTITY.fetch e 42 | f['characters'] 43 | # else 44 | # what to do...? 45 | end 46 | end 47 | 48 | private 49 | 50 | # > Invalid Unicode code points will be replaced by the REPLACEMENT CHARACTER 51 | # > (U+FFFD). For security reasons, the code point U+0000 will also be 52 | # > replaced by U+FFFD. 53 | def encode c 54 | return "\uFFFD" if c == 0 55 | return c.chr Encoding::UTF_8 56 | rescue RangeError 57 | return "\uFFFD" 58 | end 59 | 60 | public 61 | 62 | # (see Optdown::Inline#accept) 63 | def accept visitor 64 | return visitor.visit_entity self 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/optdown/escape.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # @see http://spec.commonmark.org/0.28/#backslash-escapes 27 | class Optdown::Escape 28 | attr_reader :entity # @return [String] escaped entity. 29 | 30 | # @param tok [Token] terminal token. 31 | def initialize tok 32 | str = tok.yylval['escape+'] # or raise 'undefined escape seq' 33 | @entity = str \ 34 | .each_char \ 35 | .each_slice(2) \ 36 | .map {|_, i| i } \ 37 | .join('') 38 | end 39 | 40 | # (see Optdown::Inline#accept) 41 | def accept visitor 42 | return visitor.visit_escape self 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/optdown/fenced_code_block.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'matcher' 28 | 29 | # @see http://spec.commonmark.org/0.28/#fenced-code-blocks 30 | class Optdown::FencedCodeBlock 31 | using Optdown::Matcher::Refinements 32 | 33 | attr_reader :info # @return [String] the info string. 34 | attr_reader :pre # @return [String] verbatim contents. 35 | 36 | # (see Optdown::Blocklevel#initialize) 37 | def initialize str, ctx 38 | md = str.last_match 39 | width = md['indent'].length 40 | fence = Regexp.quote md['pre:fence'] 41 | pre = [] 42 | cutter = /#{Optdown::EXPR}\G\g#{fence}+\g*\g/ 43 | indenter = /#{Optdown::EXPR}\G\g{,#{width}}/ 44 | until str.eos? do 45 | case str 46 | when cutter then break 47 | when indenter then pre << str.gets 48 | end 49 | end 50 | @info = str[md, 'pre:info'] 51 | @info = nil if @info.empty? 52 | @pre = Optdown::Matcher.join pre 53 | end 54 | 55 | # (see Optdown::Blocklevel#accept) 56 | def accept visitor, tightp: false 57 | return visitor.visit_code_block self 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/optdown/flanker.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: false -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # Flanker is the mother of all inlines who nest. Those inlines include 27 | # emphasis, links, strike-throughs (, and possibly "smarty pants"). On the 28 | # other hand for instance HTML tags are not; they would never include other 29 | # inlines. 30 | class Optdown::Flanker 31 | 32 | attr_reader :children # @return [Inline] child nodes. 33 | 34 | class << self 35 | 36 | # @see http://spec.commonmark.org/0.27/#phase-2-inline-structure 37 | def parse ary, ctx 38 | # 39 | # " ... foo ... **bar* baz*** ... " 40 | # ^ ^ ^ 41 | # i k j 42 | # 43 | n = ary.length 44 | i = 0 45 | while i < n do 46 | j, x = find_closer ary, i 47 | if j then 48 | k = find_opener ary, j, x 49 | if k then 50 | x.reduce ary, k, j, ctx 51 | i = k + 1 52 | else 53 | i = j + 1 54 | end 55 | else 56 | i += 1 57 | end 58 | end 59 | end 60 | 61 | def find_closer ary, i 62 | i.upto ary.length do |j| 63 | next unless t = ary[j] 64 | next unless Optdown::Token === t 65 | next unless t.yylex == :flanker 66 | next unless t.yylval['flanker:right'] 67 | [ 68 | Optdown::Emphasis::Under, 69 | Optdown::Emphasis::Aster, 70 | Optdown::Strikethrough, 71 | # other flankers to come, maybe smartypants? 72 | ].each do |k| 73 | return j, k if k.closer? t 74 | end 75 | end 76 | return nil 77 | end 78 | 79 | def find_opener ary, i, k 80 | t = ary[i] 81 | i.downto 0 do |j| 82 | next unless tt = ary[j] 83 | next unless Optdown::Token === tt 84 | next unless tt.yylex == :flanker 85 | next unless tt.yylval['flanker:left'] 86 | next unless k.opener? tt 87 | next unless k.matching? tt, t 88 | return j 89 | end 90 | return nil 91 | end 92 | 93 | # > Emphasis begins with a delimiter that can open emphasis and ends with a 94 | # > delimiter that can close emphasis, and that uses the same character 95 | # > (`_` or `*`) as the opening delimiter. The opening and closing 96 | # > delimiters must belong to separate delimiter runs. If one of the 97 | # > delimiters can both open and close emphasis, then the sum of the 98 | # > lengths of the delimiter runs containing the opening and closing 99 | # > delimiters must not be a multiple of 3. 100 | # 101 | # @see http://spec.commonmark.org/0.27/#emphasis-and-strong-emphasis 102 | def matching? opener, closer 103 | return false unless opener.yylex == closer.yylex 104 | # "must belong to separate run" constraint 105 | return false if opener == closer 106 | 107 | # intuitive conditions 108 | return false unless opener?(opener) 109 | return false unless closer?(closer) 110 | 111 | o = opener.to_s 112 | c = closer.to_s 113 | 114 | # same character constraint 115 | return false unless o[0] == c[0] 116 | 117 | # "If one of the delimiters can both open and close emphasis..." part 118 | if opener?(closer) || closer?(opener) then 119 | return ((o.length + c.length) % 3) != 0 120 | end 121 | 122 | # reaching here indicates the arguments match. 123 | return true 124 | end 125 | 126 | def reduce tokens, iopen, iclose, ctx 127 | # Either t1 or t2 (or maybe both) would completely be consumed here, but 128 | # there might be at most one flanker that would be left-over. 129 | range = iopen..iclose 130 | body = tokens[range].compact 131 | t1 = body.shift 132 | t2 = body.pop 133 | eat = [t1, t2].map{|t| t.to_s.length }.min 134 | t3, t4 = leftover t1, eat 135 | t5, t6 = leftover t2, eat 136 | recur body, ctx 137 | node = new t3, body, t5 138 | tokens.fill nil, range 139 | if node then 140 | tokens[iopen, 3] = [t4, node, t6] 141 | else 142 | tokens[iopen, 3] = [t4, t3, body, t5, t6] # includes nil 143 | end 144 | tokens.compact! 145 | end 146 | 147 | def recur ary, ctx 148 | ary.compact! 149 | return Optdown::Inline.new ary, ctx 150 | end 151 | 152 | def leftover tok, eat 153 | str = tok.to_s[0...eat] 154 | run = tok.to_s[eat..-1] 155 | eaten = Optdown::Token.new :cdata, str 156 | if run.nil? or run.empty? 157 | return eaten, nil 158 | else 159 | left = Optdown::Token.new tok.yylex, tok.yylval, run 160 | return eaten, left 161 | end 162 | end 163 | 164 | # routines shared among children 165 | 166 | def opener? tok 167 | return false unless Optdown::Token === tok 168 | return false unless tok.yylex == :flanker 169 | return false unless tok.yylval['flanker:left'] 170 | return true 171 | end 172 | 173 | def closer? tok 174 | return false unless Optdown::Token === tok 175 | return false unless tok.yylex == :flanker 176 | return false unless tok.yylval['flanker:right'] 177 | return true 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lib/optdown/html5entity.erb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | %# Copyright (c) 2017 Urabe, Shyouhei 7 | %# 8 | %# Permission is hereby granted, free of charge, to any person obtaining a copy 9 | %# of this software and associated documentation files (the "Software"), to 10 | %# deal in the Software without restriction, including without limitation the 11 | %# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | %# sell copies of the Software, and to permit persons to whom the Software is 13 | %# furnished to do so, subject to the following conditions: 14 | %# 15 | %# The above copyright notice and this permission notice shall be 16 | %# included in all copies or substantial portions of the Software. 17 | %# 18 | %# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | %# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | %# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | %# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | %# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | %# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24 | %# IN THE SOFTWARE. 25 | %# 26 | %# ---------------------------------------------------------------------------- 27 | %# 28 | %# Above copyright notice applies for this generator script; while the copy- 29 | %# right notice below applies to the generated ruby script. 30 | %# 31 | %# ---------------------------------------------------------------------------- 32 | %# 33 | # Copyright © 2014 W3C® (MIT, ERCIM, Keio, Beihang). All Rights Reserved. 34 | # 35 | # This work is being provided by the copyright holders under the following 36 | # license. 37 | # 38 | # License 39 | # 40 | # By obtaining and/or copying this work, you (the licensee) agree that you have 41 | # read, understood, and will comply with the following terms and conditions. 42 | # 43 | # Permission to copy, modify, and distribute this work, with or without 44 | # modification, for any purpose and without fee or royalty is hereby granted, 45 | # provided that you include the following on ALL copies of the work or portions 46 | # thereof, including modifications: 47 | # 48 | # * The full text of this NOTICE in a location viewable to users of the 49 | # redistributed or derivative work. 50 | # 51 | # * Any pre-existing intellectual property disclaimers, notices, or terms and 52 | # conditions. If none exist, the W3C Software and Document Short Notice 53 | # should be included. 54 | # 55 | # * Notice of any changes or modifications, through a copyright statement on 56 | # the new code or document such as "This software or document includes 57 | # material copied from or derived from [title and URI of the W3C 58 | # document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." 59 | # 60 | # Disclaimers 61 | # 62 | # THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS 63 | # OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES 64 | # OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF 65 | # THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, 66 | # COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. 67 | # 68 | # COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR 69 | # CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. 70 | # 71 | # The name and trademarks of copyright holders may NOT be used in advertising 72 | # or publicity pertaining to the work without specific, written prior 73 | # permission. Title to copyright in this work will at all times remain with 74 | # copyright holders. 75 | 76 | # This is a verbatim translation of https://www.w3.org/TR/html5/entities.json, 77 | # into ruby. Because "this software or document includes material copied from 78 | # or derived from" above URL, the W3C® Software License shown at the top of 79 | # this file must apply. The translation is a fully automatic process. The 80 | # person who did that (@shyouhei) do not claim any copyright or intellectual 81 | # properties over this particular generated output. Attribution shall belong 82 | # to the original authors. 83 | 84 | require_relative 'deeply_frozen' 85 | using Optdown::DeeplyFrozen 86 | 87 | # @see http://spec.commonmark.org/0.28/#entity-and-numeric-character-references 88 | # @see https://html.spec.whatwg.org/multipage/named-characters.html 89 | # @see https://html.spec.whatwg.org/multipage/entities.json 90 | # @note As written in the {file:README.md}, we use W3C's entities.json instead 91 | # of WHATWG's because of its unclear licensing. They shall be very 92 | # similar if not identical. 93 | Optdown::HTML5ENTITY = deeply_frozen_copy_of({ 94 | % entities.each_pair do |e, h| 95 | <%= e.dump %> => { 96 | % h.each_pair do |k, v| 97 | <%= k.dump %> => <%= 98 | case v 99 | when Array then "[ #{v.join(", ")} ]" 100 | when String then v.dump 101 | end 102 | %>, 103 | % end 104 | }, 105 | % end 106 | }) 107 | -------------------------------------------------------------------------------- /lib/optdown/indented_code_block.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'matcher' 28 | 29 | # @see http://spec.commonmark.org/0.28/#indented-code-block 30 | class Optdown::IndentedCodeBlock 31 | using Optdown::Matcher::Refinements 32 | 33 | attr_reader :pre # @return [Matcher] verbatim contents. 34 | 35 | # (see Optdown::Blocklevel#initialize) 36 | def initialize str, ctx 37 | pre = [ str.gets ] 38 | 39 | until str.eos? do 40 | case str 41 | when /#{Optdown::EXPR}\G\g/o then 42 | pre << str.gets 43 | when /#{Optdown::EXPR}\G(?=\g+\g)/o then 44 | str.match %r/#{Optdown::EXPR}\G\g/o # cut space 45 | pre << str.gets 46 | else 47 | break 48 | end 49 | end 50 | 51 | # > Blank lines preceding or following an indented code block are not 52 | # > included in it 53 | # @see http://spec.commonmark.org/0.28/#indented-code-block 54 | @pre = Optdown::Matcher.join pre \ 55 | .reverse \ 56 | .drop_while(&:blank?) \ 57 | .reverse \ 58 | .drop_while(&:blank?) 59 | 60 | end 61 | 62 | # @return [nil] makes sense for fenced one, not here. 63 | def info 64 | return nil 65 | end 66 | 67 | # (see Optdown::Blocklevel#accept) 68 | def accept visitor, tightp: false 69 | return visitor.visit_code_block self 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/optdown/inline.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'xprintf' 28 | require_relative 'token' 29 | 30 | # Parses inline elements 31 | class Optdown::Inline 32 | using Optdown::XPrintf 33 | 34 | attr_reader :children # @return [Array] description TBW. 35 | 36 | # Inline elements cannot be parsed linearly because of link resoltuion. We 37 | # need to first tokenize the input as a series of tokens, then after all 38 | # inlines are tokenized, start understanding them as trees. 39 | # 40 | # @see http://spec.commonmark.org/0.28/#example-540 41 | # @see http://spec.commonmark.org/0.28/#example-541 42 | # @see http://spec.commonmark.org/0.28/#example-542 43 | # @param str [Matcher] target to scan. 44 | # @return [Array] str split into tokens. 45 | def self.tokenize str 46 | a = [] 47 | until str.eos? do 48 | b4, md = str.advance %r/#{Optdown::EXPR} \g /xo 49 | 50 | if b4 and not b4.empty? then 51 | tok = Optdown::Token.new :cdata, b4 52 | a << tok 53 | end 54 | 55 | next unless md 56 | text = md[0].dup 57 | # :FIXME: We need a more appropriate place than here to exercise the 58 | # "extended autolink path validation" maneuver. 59 | # 60 | # @see https://github.github.com/gfm/#extended-autolink-path-validation 61 | if md['auto:GH:path'] and 62 | /\)\z/ =~ text then 63 | while text.count('(') != text.count(')') and 64 | text.chomp!(')') do 65 | str.ungetc 66 | end 67 | end 68 | tok = Optdown::Token.new nil, md, text 69 | a << tok 70 | end 71 | return a 72 | end 73 | 74 | # Understand the tokenized series. 75 | def parse 76 | @children.map! do |t| 77 | next t unless Optdown::Token === t 78 | case t.yylex 79 | when :'break' then next Optdown::Newline.new t 80 | when :'escape' then next Optdown::Escape.new t 81 | when :'entity' then next Optdown::Entity.new t 82 | when :'code' then next Optdown::CodeSpan.new t 83 | when :'tag' then next Optdown::RawHTML.new t 84 | when :'autolink' then next Optdown::Autolink.new t 85 | else next t 86 | end 87 | end 88 | # This order of flanker reduction is very important. 89 | # 90 | # > - The brackets in link text bind more tightly than markers for emphasis 91 | # > and strong emphasis. 92 | # 93 | # @see http://spec.commonmark.org/0.28/#links 94 | Optdown::Link.parse @children, @parser 95 | Optdown::Flanker.parse @children, @parser 96 | @children.compact! 97 | end 98 | 99 | # Inline elements tends to be consist of multiple lines. Instead of parse 100 | # them evey lines we would like to first merge them, then parse. 101 | # 102 | # @param lines [Array] lines of inlines 103 | # @param ctx [Parser] parsing context. 104 | # @return [Inline] generated node. 105 | def self.from_lines lines, ctx 106 | str = Optdown::Matcher.join lines 107 | ary = tokenize str 108 | return new ary, ctx 109 | end 110 | 111 | # Also, there are cases when inline elements are whitespace-stripped. This 112 | # utility handle such situations. 113 | # 114 | # @param line [Matcher] a line of inlines 115 | # @param ctx [Parser] parsing context. 116 | # @return [Inline] generated node. 117 | def self.from_stripped line, ctx 118 | str = Optdown::Matcher.new line.to_s.strip 119 | ary = tokenize str 120 | return new ary, ctx 121 | end 122 | 123 | def initialize ary, ctx 124 | @children = ary 125 | @parser = ctx 126 | @parser.define_inline self 127 | end 128 | 129 | # Traverse the tree 130 | # 131 | # @param visitor [Renderer] rendering visitor. 132 | # @return [Object] visitor visiting result. 133 | def accept visitor 134 | return visitor.visit_inline self, @children.map {|i| visitor.visit i } 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/optdown/link.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'flanker' 27 | require_relative 'link_title' 28 | 29 | # @see http://spec.commonmark.org/0.28/#links 30 | module Optdown::Link 31 | # Links are different from other flankers in one point. Its opening and 32 | # closing characters are different. 33 | 34 | attr_reader :attr # return [Hash] description TBW. 35 | 36 | # (see Optdown::Flanker.parse) 37 | def self.parse ary, ctx 38 | ary.each_with_index do |t, i| 39 | next unless Optdown::Token === t 40 | next unless t.yylex == :link 41 | next unless t.yylval['a:right'] 42 | i.downto 0 do |j| 43 | tt = ary[j] 44 | next unless Optdown::Token === tt 45 | next unless tt.yylex == :link 46 | if tt.yylval['a:left'] then 47 | A.reduce ary, j, i, ctx 48 | break 49 | elsif tt.yylval['img:left'] then 50 | Img.reduce ary, j, i, ctx 51 | break 52 | end 53 | end 54 | end 55 | end 56 | 57 | private 58 | 59 | def initialize argh 60 | @attr = argh 61 | if link = argh[:link] then 62 | @attr[:dest] = link.dest 63 | @attr[:title] = link.title 64 | elsif tok = argh[:inline] then 65 | @attr[:dest] = unparen_dest tok 66 | @attr[:title] = unparen_title tok 67 | end 68 | @children = argh[:label] 69 | end 70 | 71 | def unparen_title tok 72 | md = tok.yylval 73 | title = md['link:title:2j'] || md['link:title:1j'] || md['link:title:0j'] 74 | return nil unless title 75 | return Optdown::LinkTitle.new title 76 | end 77 | 78 | def unparen_dest tok 79 | md = tok.yylval 80 | dest = md['link:dest:a'] || md['link:dest:b'] 81 | return nil unless dest 82 | obj = Optdown::LinkTitle.new dest 83 | return obj.plain 84 | end 85 | 86 | def reduce_nothing tokens, iopen, iclose 87 | tokens[iopen] = tokens[iopen].cdataify 88 | tokens[iclose] = tokens[iclose].cdataify 89 | end 90 | 91 | def reduce_ref tokens, iopen, iclose, ctx 92 | md = tokens[iclose].yylval 93 | cand = md['link:label'] 94 | cand ||= /#{Optdown::EXPR}\g\z/o.match(md.pre_match + ']') 95 | link = ctx.find_link_by cand 96 | return reduce_nothing tokens, iopen, iclose unless link 97 | ifill = iclose 98 | if md['link:label'] then 99 | # eat the follwing label here 100 | ifill += 1 101 | ifill += 1 while tokens[ifill]&.to_s&.!= ']' 102 | end 103 | range = iopen..iclose 104 | body = tokens[range].compact 105 | body.shift 106 | body.pop 107 | tokens.fill nil, iopen..ifill 108 | child = recur body, ctx 109 | tokens[iopen] = new label: child, link: link 110 | end 111 | 112 | def reduce_inline tokens, iopen, iclose, ctx 113 | range = iopen..iclose 114 | body = tokens[range].compact 115 | body.shift 116 | t = body.pop 117 | tokens.fill nil, range 118 | child = recur body, ctx 119 | tokens[iopen] = new label: child, inline: t 120 | end 121 | 122 | public 123 | 124 | # (see Optdown::Flanker.reduce) 125 | def reduce tokens, iopen, iclose, ctx 126 | if tokens[iclose].yylval['a:inline'] then 127 | reduce_inline tokens, iopen, iclose, ctx 128 | else 129 | reduce_ref tokens, iopen, iclose, ctx 130 | end 131 | end 132 | 133 | # @see http://spec.commonmark.org/0.28/#images 134 | class Img < Optdown::Flanker 135 | include Optdown::Link 136 | extend Optdown::Link 137 | 138 | # (see Optdown::Inline#accept) 139 | def accept visitor 140 | label = visitor.visit @attr[:label] if @attr[:label] 141 | title = visitor.visit @attr[:title] if @attr[:title] 142 | return visitor.visit_image self, label, title 143 | end 144 | end 145 | 146 | # @see http://spec.commonmark.org/0.28/#links 147 | class A < Optdown::Flanker 148 | include Optdown::Link 149 | extend Optdown::Link 150 | 151 | class << self 152 | 153 | # > links may not contain other links, at any level of nesting. 154 | def reduce tokens, iopen, iclose, ctx 155 | if (iopen..iclose).any? {|i| has_link? tokens[i] } then 156 | reduce_nothing tokens, iopen, iclose 157 | else 158 | super 159 | end 160 | end 161 | 162 | private 163 | 164 | def has_link? tok 165 | case tok 166 | when self then 167 | return true 168 | when Optdown::Flanker then 169 | return has_link? tok.children 170 | when Optdown::Inline then 171 | return tok.children.any? {|i| has_link? i } 172 | else 173 | return false 174 | end 175 | end 176 | end 177 | 178 | # (see Optdown::Inline#accept) 179 | def accept visitor 180 | label = visitor.visit @attr[:label] if @attr[:label] 181 | title = visitor.visit @attr[:title] if @attr[:title] 182 | return visitor.visit_link self, label, title 183 | end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /lib/optdown/link_def.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | 28 | # @see http://spec.commonmark.org/0.28/#link-reference-definitions 29 | class Optdown::LinkDef 30 | attr_reader :dest # @return [String] URL 31 | attr_reader :title # @return [String] title 32 | attr_reader :label # @return [String] label 33 | 34 | # @see http://spec.commonmark.org/0.28/#matches 35 | # @param str [String] label candidate. 36 | # @return [String] normalized label string. 37 | def self.labelize str 38 | return str \ 39 | . to_s \ 40 | . downcase(:fold) \ 41 | . gsub %r/#{Optdown::EXPR}\g/o, ' ' 42 | end 43 | 44 | # (see Optdown::Blocklevel#initialize) 45 | def initialize str, ctx 46 | @label = self.class.labelize str['link:label'] 47 | dest = str['link:dest:a'] || 48 | str['link:dest:b'] 49 | title = str['link:title:2j'] || 50 | str['link:title:1j'] || 51 | str['link:title:0j'] 52 | @dest = dest && Optdown::LinkTitle.new(dest.to_s).plain 53 | @title = title && Optdown::LinkTitle.new(title.to_s) 54 | ctx.define_link self 55 | end 56 | 57 | # (see Optdown::Blocklevel#accept) 58 | def accept visitor, tightp: false 59 | t = visitor.visit @title if @title 60 | return visitor.visit_link_definition self, t 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/optdown/link_title.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'matcher' 27 | require_relative 'expr' 28 | require_relative 'inline' 29 | 30 | # The spec does not list this kind of elements, but includes the following 31 | # line: 32 | # 33 | # > Backslash escapes and entity and numeric character references may be used 34 | # > in titles 35 | # 36 | # So it should mean the titles are _structured_, not flat simple set of 37 | # characters. 38 | class Optdown::LinkTitle 39 | 40 | # @param str [String] the title to parse. 41 | def initialize str 42 | s = Optdown::Matcher === str ? str : Optdown::Matcher.new(str) 43 | @children = Optdown::Inline.tokenize s 44 | @children.map! do |t| 45 | case t.yylex 46 | when :'escape' then next Optdown::Escape.new t 47 | when :'entity' then next Optdown::Entity.new t 48 | else next t 49 | end 50 | end 51 | end 52 | 53 | # custom renderer does not make sense for link destination, which is a URL. 54 | def plain 55 | return @children.map {|t| 56 | case t 57 | when Optdown::Token then next t.yytext 58 | when Optdown::Escape then next t.entity 59 | when Optdown::Entity then next t.entity 60 | end 61 | }.join 62 | end 63 | 64 | # (see Optdown::Inline#accept) 65 | def accept visitor 66 | return visitor.visit_link_title self, @children.map{|i| visitor.visit i } 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/optdown/list.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'matcher' 28 | require_relative 'list_item' 29 | 30 | # @see http://spec.commonmark.org/0.28/#lists 31 | class Optdown::List 32 | using Optdown::Matcher::Refinements 33 | 34 | # (see Optdown::Blocklevel#initialize) 35 | def initialize str, ctx 36 | first = Optdown::ListItem.new str, ctx 37 | continue = first.same_type_expr 38 | @children = [ first ] 39 | @blank_seen = false 40 | until str.eos? do 41 | break unless str.match? continue 42 | while str.match? %r/#{Optdown::EXPR}\G\g/o do 43 | @blank_seen = true 44 | str.gets 45 | end 46 | item = Optdown::ListItem.new str, ctx 47 | @children << item 48 | end 49 | end 50 | 51 | # @see http://spec.commonmark.org/0.28/#loose 52 | # @return [true] it is. 53 | # @return [false] it isn't. 54 | def tight? 55 | return (! @blank_seen) && @children.all?(&:tight?) 56 | end 57 | 58 | # @return [:ordered, :bullet, :task] type of the list. 59 | def type 60 | @children.first.type 61 | end 62 | 63 | # @return [String] start number (makes sense for ordered list) 64 | def start 65 | @children.first.order.sub %r/\A0+(?=\d)/, '' 66 | end 67 | 68 | # (see Optdown::Blocklevel#accept) 69 | def accept visitor, tightp: false 70 | li = @children.map {|i| visitor.visit i, tightp: self.tight? } 71 | return visitor.visit_list self, li 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/optdown/list_item.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'matcher' 28 | 29 | # @see http://spec.commonmark.org/0.28/#list-items 30 | class Optdown::ListItem 31 | using Optdown::Matcher::Refinements 32 | 33 | attr_reader :type # @return [:bullet, :ordered] type of it. 34 | attr_reader :marker # @return [String] leading list marker. 35 | attr_reader :order # @return [String] item order, if any. 36 | 37 | private 38 | 39 | def re # easy typing 40 | Optdown::EXPR 41 | end 42 | 43 | def calc_width md 44 | str = md['li2'] rescue md['li'] 45 | pad = (md['indent'] || '').length 46 | ret = pad + str.length 47 | if md['li:normal'] then return ret 48 | elsif md['li:pre'] then return ret 49 | elsif md['li:eol'] then return ret + 1 50 | elsif md['li:task'] then 51 | # `- [ ] foo` 52 | # ^^^^^^^^ : str 53 | # ^^^^^ : what we need this case 54 | return ret - 3 55 | end 56 | end 57 | 58 | def handle_types width, md 59 | # > When both a thematic break and a list item are possible interpretations 60 | # > of a line, the thematic break takes precedence 61 | # @see http://spec.commonmark.org/0.28/#thematic-breaks 62 | # 63 | # > If there is any ambiguity between an interpretation of indentation as a 64 | # > code block and as indicating that material belongs to a list item, the 65 | # > list item interpretation takes precedence 66 | # @see https://github.github.com/gfm/#indented-code-blocks 67 | filler = /#{re}\G\g*\g*(?!\g
)/o 68 | if b = md['li:bullet'] then 69 | @type = md['li:task'] ? :task : :bullet 70 | @checked = /x/i =~ md['li:task'] if @type == :task 71 | @marker = b 72 | e = Regexp.escape b 73 | @same_type = /(?=#{filler}#{e}\g)/ 74 | else 75 | @type = :ordered 76 | @marker = md['li:mark'] 77 | @order = md['li:num'] 78 | e = Regexp.escape @marker 79 | @same_type = /(?=#{filler}\g#{e}\g)/ 80 | end 81 | end 82 | 83 | def eat_following_lines width, md, str 84 | re = /#{Optdown::EXPR}\G/o 85 | indent = /#{re}\g{#{width}}(?!\g)/ 86 | lazy = /#{re}(?=\g{,#{width-1}}(?!\g|\g))/ 87 | cutter = /#{re}(?=\g{,#{width-1}}\g)/ 88 | dedent = /#{re}(?=\g*\g{,#{width-1}}(?!\g|\g))/ 89 | 90 | if md['li:eol'] and str.match?(%r/#{re}\G\g{2,}/o) then 91 | # > A list item can begin with at most one blank line. 92 | # @see http://spec.commonmark.org/0.28/#list-items 93 | return [] 94 | else 95 | lines = [ str.gets ] 96 | end 97 | 98 | until str.eos? do 99 | case str 100 | # An empty list item cannot interrupt a paragraph while an empty list 101 | # item can follow a paragraph and a paragraph can be lazy. There seems 102 | # to be a conflict in the spec's language. I'd like to follow the two 103 | # examples. They are thought to represent intentions. 104 | # 105 | # @see http://spec.commonmark.org/0.28/#example-248 106 | # @see http://spec.commonmark.org/0.28/#example-276 107 | when indent then 108 | lines << str.gets 109 | when lazy then 110 | break if str.match? @same_type 111 | break if str.match? %r/#{re}\G\g
  • /o # ...? 112 | break if str.match? cutter 113 | lines << Optdown::Paragraph::PAD 114 | lines << str.gets 115 | when dedent then 116 | break 117 | else 118 | lines << str.gets 119 | end 120 | end 121 | return lines 122 | end 123 | 124 | # (see Optdown::Blocklevel#initialize) 125 | def initialize str, ctx 126 | @blank_seen = false 127 | @children = nil 128 | md = str.last_match 129 | md = str.match %r/#{re}\G(?\g*\g
  • )/o unless md['li'] 130 | width = calc_width md 131 | 132 | handle_types width, md 133 | return if str.eos? 134 | 135 | lines = eat_following_lines width, md, str 136 | 137 | @children = Optdown::Blocklevel.from_lines lines, ctx 138 | end 139 | 140 | public 141 | 142 | # (see Optdown::List#tight?) 143 | def tight? 144 | return @children.tight? 145 | end 146 | 147 | # Only a list item of the same tipe can follow this. 148 | # 149 | # @return [Regexp] pattern that allows the same type. 150 | def same_type_expr 151 | return @same_type 152 | end 153 | 154 | # @return [true, false] if the task is checked (makes sense for task item). 155 | def checked? 156 | (defined? @checked) and @checked 157 | end 158 | 159 | # (see Optdown::Blocklevel#accept) 160 | def accept visitor, tightp: false 161 | inner = visitor.visit @children, tightp: tightp 162 | return visitor.visit_list_item self, inner 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /lib/optdown/matcher.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'xprintf' 28 | 29 | # @see http://spec.commonmark.org/0.28/#tabs 30 | class Optdown::Matcher 31 | using Optdown::XPrintf 32 | using Module.new { 33 | refine Optdown::Matcher.singleton_class do 34 | alias construct new 35 | end 36 | 37 | refine Optdown::Matcher do 38 | attr_reader :logical, :map 39 | end 40 | } 41 | private_class_method :new 42 | 43 | attr_reader :last_match # @return [MatchData, nil] last match result, if any. 44 | 45 | # Main constructor. 46 | # 47 | # @param string [String] source string. 48 | # @return [Matcher] created instance. 49 | def self.new string 50 | tab_width = 4 # change? insane. 51 | pad = 0 52 | map = 0.chr * string.length 53 | logical = string.gsub("\u0000", "\uFFFD") 54 | # rexp = /#{Optdown::EXPR}(?(?~\g|\g))\g/o 55 | rexp = /(?[^\t\r\n]*)\t/o 56 | logical.gsub! rexp do 57 | b4 = $~[:b4] 58 | x = $~.end :b4 59 | y = tab_width - b4.length % tab_width 60 | z = y - 1 61 | w = 1.chr + 2.chr * z 62 | map[x + pad, 1] = w 63 | pad += z 64 | next b4 + ' ' * y 65 | end 66 | return construct logical, map 67 | end 68 | 69 | # This method is faster than `inject(:+)` 70 | # 71 | # @param ary [Array] targets to join 72 | # @return [Matcher] joined matcher 73 | def self.join ary 74 | jl = ary.map(&:logical).join 75 | jm = ary.map(&:map).join 76 | return construct jl, jm 77 | end 78 | 79 | # (Ignore this method; you can't call it by hand. YARD is so friendly that 80 | # it provides no way to hide #initialize.) 81 | def initialize logical, map 82 | @logical = logical.freeze 83 | @map = map.freeze 84 | @pos = 0 85 | @last_match = nil 86 | end 87 | 88 | Empty = construct '', '' 89 | private_constant :Empty 90 | 91 | # Stringify. 92 | # 93 | # @return [String] the rendered string. 94 | def compile 95 | case @map when /\A\x0*\z/ then 96 | return @logical # fast path 97 | else 98 | # :FIXME: slow 99 | ret = String.new encoding: @logical.encoding, capacity: length 100 | @logical.each_char.with_index do |c, i| 101 | case @map.getbyte i 102 | when 0 then ret << c 103 | when 1 then ret << "\t" 104 | # else # do nothing 105 | end 106 | end 107 | return ret 108 | end 109 | end 110 | 111 | alias to_s compile 112 | alias to_str compile 113 | 114 | # Length, in logical columns. 115 | # 116 | # @return [Integer] how many logical columns are there. 117 | def length 118 | # @map is a binary string. Its length is binary length, which can be 119 | # obtained O(1). On the other hand @logical is a UTF-8 string whose length 120 | # needs be calculated in O(n). 121 | return @map.length 122 | end 123 | 124 | alias size length 125 | 126 | # Inspection. 127 | def pretty_print pp 128 | pp.text '@' 129 | compile.pretty_print pp 130 | end 131 | 132 | # Inspection. 133 | def inspect 134 | '@' + compile.inspect 135 | end 136 | 137 | # Checks if nothing is inside. 138 | # 139 | # @return [true] it is. 140 | # @return [false] it isn't. 141 | def empty? 142 | @map.empty? 143 | end 144 | 145 | # Checks if the content is blank. "Blank"-ness of a string is 146 | # defined in the spec so we take that definition. 147 | # 148 | # @see http://spec.commonmark.org/0.28/#blank-lines 149 | # @return [true] it is. 150 | # @return [false] it isn't. 151 | def blank? 152 | match? %r/#{Optdown::EXPR}\G\g*\z/o 153 | end 154 | 155 | # Match, ignoring tabs 156 | # 157 | # @param rexp [Regexp] pattern to consider. 158 | # @return [MatchData] successful match. 159 | # @return [nil] failure in match. 160 | def match rexp 161 | _, ret = match_internal rexp 162 | return ret 163 | end 164 | 165 | # Tries to match the given pattern at the current position. No seek, also no 166 | # MatchData generation. 167 | # 168 | # @param rexp [Regexp] pattern to consider. 169 | # @return [true] successful match. 170 | # @return [false] failure in match. 171 | def match? rexp 172 | return @logical.match? rexp, @pos 173 | end 174 | 175 | # Same as `match?(/\G\z/)` 176 | # 177 | # @return [true] end of string. 178 | # @return [false] not yet. 179 | def eos? 180 | # This method is a super duper hot spot that is worth optimizing. 181 | return @pos == @map.length 182 | end 183 | 184 | # Read until the end of string (or leftmost n characters, whichever reached 185 | # first). 186 | # 187 | # @param n [Integer] characters to read. 188 | # @return [String] what was read. 189 | def read n = length 190 | if @pos == 0 and n == length then 191 | # fast path 192 | ret = dup 193 | @pos = length 194 | else 195 | ret = slice @pos, n 196 | @pos += ret.length 197 | end 198 | return ret 199 | end 200 | 201 | # Seek back from current position. 202 | # 203 | # @param n [Integer] logical columns to back. 204 | # @note There is no method named getc. 205 | def ungetc n = 1 206 | @pos -= n 207 | @pos = @pos.clamp 0, length 208 | end 209 | 210 | # Read until the end of line. 211 | # 212 | # @param rs [Regexp] newline pattern, like `$/` 213 | # @return [String] what was read. 214 | def gets rs = /#{Optdown::EXPR}\g/o 215 | beg_, md = match_internal rs 216 | if md then 217 | end_ = md.end 0 218 | return slice beg_...end_ 219 | else 220 | return read 221 | end 222 | end 223 | 224 | # Read until the regexp matches. 225 | # 226 | # @param re [Regexp] pattern. 227 | # @return [String] prematch. 228 | def advance re 229 | beg_, md = match_internal re 230 | if md then 231 | end_ = md.begin 0 232 | prematch = slice beg_...end_ 233 | return prematch, md 234 | else 235 | return read 236 | end 237 | end 238 | 239 | # `md` is a MatchData generated by some other methods of self. Given such md 240 | # and a capture name, return the corresponding substring. 241 | # 242 | # @param md [MatchData] position info. 243 | # @param name [String, Integer] capture name, or capture #. 244 | # @return [String] substring for `md`'s `name`. 245 | # @return [nil] no match. 246 | def get_anchor md = @last_match, name 247 | return nil unless md 248 | # this is faster than calling slice 249 | beg_, end_ = md.offset name 250 | return nil unless beg_ 251 | substr = md[name] 252 | submap = @map[beg_...end_] 253 | submap.sub! %r/\A\x2+/ do |i| 0.chr * i.length end 254 | return self.class.construct substr, submap 255 | end 256 | 257 | alias [] get_anchor 258 | 259 | # Poor man's simulation of String#split 260 | # 261 | # @param sep [Regexp] split separator. 262 | # @param limit [Integer] split limit. 263 | # @return [Array] self, split. 264 | def split sep, limit = 0 265 | a = [] 266 | return a if empty? 267 | sep = Regexp.quote sep unless sep.kind_of? Regexp 268 | pos = @pos 269 | max = @map.size 270 | while pos < max do 271 | break if limit > 0 and a.size >= limit - 1 272 | md = @logical.match sep, pos 273 | break unless md 274 | 275 | beg_, end_ = md.offset 0 276 | if beg_ == end_ then 277 | str = slice pos 278 | pos += 1 279 | else 280 | str = slice pos...beg_ 281 | pos = end_ 282 | end 283 | a << str 284 | 285 | m = md.size - 1 286 | 1.upto m do |n| 287 | beg_, end_ = md.offset n 288 | str = slice beg_...end_ 289 | a << str 290 | end 291 | end 292 | last = slice pos..max 293 | a << last 294 | 295 | if limit == 0 then 296 | a.pop while a.last&.empty? 297 | end 298 | return a 299 | end 300 | 301 | private 302 | 303 | def match_internal rexp 304 | beg = @pos 305 | md = @logical.match rexp, beg 306 | @last_match = md 307 | @pos = md.end 0 if md 308 | return beg, md 309 | end 310 | 311 | # @overload slice(range) 312 | # 313 | # generate a substring of the given range. 314 | # 315 | # @param range [Range] a range of the string. 316 | # @return [Matcher] requested substring. 317 | # 318 | # @overload slice(from, to) 319 | # 320 | # generate a substring of the given range. 321 | # 322 | # @param from [Integer] staring index of the requested substring. 323 | # @param to [Integer] terminating index of the requested substring. 324 | # @return [Matcher] requested substring. 325 | def slice *argv 326 | subl = @logical[*argv] 327 | subm = @map[*argv] 328 | # we need to take care when cutting a middle of a tab. 329 | subm.sub! %r/\A\x2+/ do |i| 0.chr * i.length end 330 | return self.class.construct subl, subm 331 | end 332 | 333 | public 334 | 335 | # This is the refinements that refines Regexp#===. Should be `using`-ed 336 | # beforehand. 337 | module Refinements 338 | refine Regexp do 339 | private 340 | 341 | alias rb_reg_eqq === 342 | 343 | public 344 | 345 | # Case equality. 346 | # 347 | # @param other [Optdown::Scanner] scanable string. 348 | # @return [true] successful match. 349 | # @return [false] failure in match. 350 | def === other 351 | case other when Optdown::Matcher then 352 | return other.match self 353 | else 354 | return rb_reg_eqq other 355 | end 356 | end 357 | end 358 | end 359 | end 360 | -------------------------------------------------------------------------------- /lib/optdown/newline.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # http://spec.commonmark.org/0.28/#hard-line-breaks 27 | class Optdown::Newline 28 | attr_reader :type # @return [:hard, :soft] newline type 29 | 30 | # @param tok [Token] terminal token. 31 | def initialize tok 32 | md = tok.yylval 33 | case 34 | when md['br:hard'] then @type = :hard 35 | when md['br:soft'] then @type = :soft 36 | end 37 | end 38 | 39 | # (see Optdown::Inline#accept) 40 | def accept visitor 41 | return visitor.visit_newline self 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/optdown/paragraph.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'matcher' 28 | require_relative 'inline' 29 | require_relative 'deeply_frozen' 30 | 31 | # @see http://spec.commonmark.org/0.28/#paragraphs 32 | class Optdown::Paragraph 33 | using Optdown::DeeplyFrozen 34 | using Optdown::Matcher::Refinements 35 | 36 | # Paragraph continuations shall construct paragraphs, not other block levels. 37 | # For instance "> foo\n ====" shall not be an h2 inside of a blockquote. 38 | # This constant sneaks into parsed DOM trees to prevent such misconceptions. 39 | PAD = deeply_frozen_copy_of Optdown::Matcher.new("\t\t") 40 | 41 | # (see Optdown::Blocklevel#initialize) 42 | def initialize str, ctx 43 | a = [ str.gets ] # at least one line shall be there. 44 | a << str.gets until str.match? %r/#{Optdown::EXPR}\G\g/o 45 | b = trim a 46 | @children = Optdown::Inline.from_lines b, ctx 47 | end 48 | 49 | private 50 | 51 | # > The paragraph’s raw content is formed by concatenating the lines and 52 | # > removing initial and final whitespace. 53 | # 54 | # So we have to trim the input here. 55 | # 56 | # @see http://spec.commonmark.org/0.28/#paragraphs 57 | def trim a 58 | a.map! do |i| 59 | i.match %r/#{Optdown::EXPR}\A\g/o 60 | i.read 61 | end 62 | a[-1], = a[-1].advance %r/#{Optdown::EXPR}\g\z/o 63 | return a 64 | end 65 | 66 | public 67 | 68 | # @todo description TBW. 69 | def children 70 | @children&.children 71 | end 72 | 73 | # (see Optdown::Blocklevel#accept) 74 | def accept visitor, tightp: false 75 | inner = visitor.visit @children 76 | return visitor.visit_paragraph self, tightp, inner 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/optdown/parser.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'matcher' 27 | require_relative 'xprintf' 28 | 29 | # Parser proper. 30 | class Optdown::Parser 31 | using Optdown::XPrintf 32 | 33 | # This method has no params. It exactly parses the GFM so no parameters are 34 | # there. 35 | def initialize 36 | @links = {} 37 | @inlines = [] 38 | end 39 | 40 | # @param str [String] target string to parse. 41 | # @return [Blocklevel] parsed AST. 42 | def parse str 43 | @links.clear 44 | @inlines.clear 45 | m = Optdown::Matcher.new str 46 | b = Optdown::Blocklevel.new m, self 47 | # above parsing of blocklevel should have registered inlines 48 | @inlines.map(&:parse) 49 | return b 50 | end 51 | 52 | # @param link [LinkDef] definition body. 53 | # @return [void] 54 | def define_link link 55 | if @links[link.label] then 56 | # > If there are multiple matching reference link definitions, the one 57 | # > that comes first in the document is used. (It is desirable in such 58 | # > cases to emit a warning.) 59 | # 60 | # @see http://spec.commonmark.org/0.28/#matches 61 | wprintf "link %s defined more than once\n", link.label 62 | else 63 | @links[link.label] = link 64 | end 65 | end 66 | 67 | # @param label [Matcher] label string. 68 | # @return [LinkDef] corresponding definition. 69 | # @return [nil] not found. 70 | def find_link_by label 71 | canon = Optdown::LinkDef.labelize label 72 | return @links[canon] 73 | end 74 | 75 | # @param inline [Inline] definition body. 76 | # @return [void] 77 | def define_inline inline 78 | @inlines << inline 79 | end 80 | 81 | # Because this parser object holds complex object graphs, its inspection 82 | # output tends to become huge; normally not something readable by human eyes. 83 | # We would like to suppress a bit. 84 | def inspect 85 | '(parser)' 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/optdown/plugins/houdini_compat.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative '../deeply_frozen' 27 | 28 | # GFM uses https://github.com/vmg/houdini for escaping HTML tags. The routines 29 | # work differently from what Ruby provides through CGI::Escape. We have to fill 30 | # the gap. 31 | module Optdown::HoudiniCompat 32 | using Optdown::DeeplyFrozen 33 | 34 | module_function 35 | 36 | TMAP = deeply_frozen_copy_of({ 37 | '&' => '&', 38 | '<' => '<', 39 | '>' => '>', 40 | '"' => '"', 41 | # "'" => ''', # not enabled in GFM 42 | # '/' => '/', # not enabled in GFM 43 | }) 44 | TRE = deeply_frozen_copy_of Regexp.union(TMAP.keys.sort.reverse) 45 | 46 | private_constant :TMAP, :TRE 47 | 48 | # Escapes HTML tags 49 | # @param str [String] target. 50 | # @return [String] escaped content. 51 | def escape_tag str 52 | return str.to_s.gsub TRE, TMAP 53 | end 54 | 55 | HRE = deeply_frozen_copy_of %r/[^#{eval <<~'end'}]/n 56 | 57 | # This table from `static const char HREF_SAFE[]` in houdini_href_e.c. 58 | [ 59 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 61 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 62 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 63 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 64 | 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 67 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 68 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 70 | ] \ 71 | . each_with_index \ 72 | . each_with_object("".dup) {|(x, y), z| z << y.chr('binary') if x == 1 } \ 73 | . gsub(%/[\[\-\]]/, '\\\\\\&') 74 | end 75 | 76 | private_constant :HRE 77 | 78 | # Escapes hrefs. 79 | # 80 | # @note Don't blame @shyouhei for the behaviour. This is a direct 81 | # translation of https://github.com/vmg/houdini and nothing more. 82 | # @param str [String] target. 83 | # @return [String] escaped content. 84 | def escape_href str 85 | s = str.to_s 86 | e = s.encoding 87 | return s \ 88 | . to_s \ 89 | . b \ 90 | . gsub(HRE) {|m| 91 | case m 92 | when '&' then '&' 93 | when "'" then ''' 94 | else '%' + m.unpack('H2' * m.bytesize).join('%').upcase 95 | end 96 | } \ 97 | . force_encoding(e) 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/optdown/plugins/html_renderer.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative '../expr' 27 | require_relative '../renderer' 28 | require_relative '../link_title' 29 | require_relative 'plaintext_renderer' 30 | require_relative 'houdini_compat' 31 | 32 | # GFM compatible HTML renderer. 33 | class Optdown::HTMLRenderer < Optdown::Renderer 34 | include Optdown::HoudiniCompat 35 | 36 | Newline = Object.new 37 | private_constant :Newline 38 | 39 | # Render HTML 40 | # 41 | # @param node [Object] AST node. 42 | # @return [String] node, in HTML. 43 | def render node 44 | return tagfilter visit(node) \ 45 | . flatten \ 46 | . lazy \ 47 | . chunk {|i| i == Newline } \ 48 | . map {|_, b| b } \ 49 | . map {|b| b[0] == Newline ? "\n" : b } \ 50 | . force \ 51 | . join \ 52 | . sub %r/\A\n+/, '' 53 | end 54 | 55 | private 56 | 57 | # Tag filter extension not enabled right now. 58 | # See https://github.github.com/gfm/ and search for instance `" string. 82 | def visit_thematic_break *; 83 | return [ Newline, '
    ', Newline ] 84 | end 85 | 86 | # Visit a link definition. 87 | # 88 | # @return [String] an empty string. 89 | def visit_link_definition _, _ 90 | return Newline # or...? 91 | end 92 | 93 | # Visit a blockquote. 94 | # 95 | # @param inner [Array] leaf node visiting result. 96 | # @return [Array] rendered blockquote. 97 | def visit_blockquote _, inner 98 | return [ Newline, '
    ', Newline, inner, '
    ', Newline] 99 | end 100 | 101 | # Visit a list. 102 | # 103 | # @param list [List] list node. 104 | # @param items [Array] leaf node visiting result. 105 | # @return [Array] rendered list. 106 | def visit_list list, items 107 | case list.type 108 | when :bullet, :task then 109 | return [ Newline, '
      ', Newline, items, '
    ', Newline ] 110 | when :ordered then 111 | start = list.start 112 | if start == '1' then 113 | return [ Newline, '
      ', Newline, items, '
    ', Newline ] 114 | else 115 | return [ Newline, '
      ', Newline, 116 | items, '
    ', Newline ] 117 | end 118 | else 119 | rprintf RuntimeError, 'unsupported list type %s', list.type 120 | end 121 | end 122 | 123 | # Visit a list item. 124 | # 125 | # @param li [ListItem] list item node. 126 | # @param inner [Array] leaf node visiting result. 127 | # @return [Array] rendered list item. 128 | def visit_list_item li, inner 129 | ret = [ '
  • ' ] 130 | 131 | if li.type == :task then 132 | if li.checked? then 133 | ret << ' ' 134 | else 135 | ret << ' ' 136 | end 137 | end 138 | ret << [ inner, '
  • ', Newline ] 139 | return ret 140 | end 141 | 142 | # Visit an HTML block. 143 | # 144 | # @param tag [BlockHTML] block html node. 145 | # @return [String] verbatim input. 146 | def visit_blockhtml tag 147 | return [ Newline, tag.html.to_s.chomp, Newline ] 148 | end 149 | 150 | # Visit a code block. 151 | # 152 | # @param pre [IndentedCodeBlock, FencedCodeBlock] code block node. 153 | # @return [Array] rendered code. 154 | def visit_code_block pre 155 | if i = pre.info then 156 | md = i.match %r/#{Optdown::EXPR}(?\g<^WS>+)/o 157 | lang0 = i[md, 'lang'] 158 | lang1 = Optdown::LinkTitle.new lang0 159 | lang2 = visit lang1 160 | tag = ['
    ']
    161 |     else
    162 |       tag = '
    '
    163 |     end
    164 |     esc = escape_tag pre.pre
    165 |     return [ Newline, tag, esc, '
    ', Newline ] 166 | end 167 | 168 | # Visit a heading. 169 | # 170 | # @param h [ATXHeading, SetextHeading] heading node. 171 | # @param inner [Array] leaf node visiting result. 172 | # @return [Array] rendered heading. 173 | def visit_heading h, inner 174 | return [ Newline, '', 175 | inner, '', Newline ] 176 | end 177 | 178 | # Visit a table. 179 | # 180 | # @param table [Table] Table node. 181 | # @param thead [Array] first line leaf node visiting result. 182 | # @param tbody [Array] visiting results for 3rd line and beyond. 183 | # @return [Array] rendered table. 184 | def visit_table table, thead, tbody 185 | al = table.alignments 186 | ha = visit_table_internal al, [ thead ], 'th' 187 | ba = visit_table_internal al, tbody, 'td' 188 | ret = [ 189 | '
    ', Newline, 190 | '', Newline, 191 | ha, Newline, 192 | '' 193 | ] 194 | unless tbody.empty? then 195 | ret << [ Newline, '', Newline, ba, '' ] 196 | end 197 | ret << [ '
    ', Newline ] 198 | return ret 199 | end 200 | 201 | # Visit a paragraph. 202 | # 203 | # @param paragraph [Paragraph] paragraph node. 204 | # @param tightp [true, false] paragraph tightness. 205 | # @param inner [Object] leaf node visiting result. 206 | # @return [Object] rendered paragraph. 207 | def visit_paragraph paragraph, tightp, inner 208 | if tightp then 209 | return inner 210 | else 211 | return [ Newline, '

    ', inner, '

    ', Newline ] 212 | end 213 | end 214 | 215 | # Visit an inline. 216 | # 217 | # @param inline [Inline] inline node. 218 | # @param leafs [Array] leaf node visiting result. 219 | # @return [Array] leaf result. 220 | def visit_inline inline, leafs 221 | return leafs 222 | end 223 | 224 | # Visit a token. 225 | # 226 | # @param token [Token] terminal token. 227 | # @return [String] verbatim input. 228 | def visit_token token 229 | return escape_tag token.yytext 230 | end 231 | 232 | # Visit a newline. 233 | # 234 | # @param br [Newline] terminal token. 235 | # @return [String] newline, or a "
    " 236 | def visit_newline br 237 | case br.type when :hard then 238 | return '
    ', Newline 239 | else 240 | return Newline 241 | end 242 | end 243 | 244 | # Visit an escape. 245 | # 246 | # @param escape [Escape] terminal token. 247 | # @return [String] unescaped entity. 248 | def visit_escape escape 249 | # for instance `\<` shall render `<` 250 | return escape_tag escape.entity 251 | end 252 | 253 | # Visit an entity. 254 | # 255 | # @param entity [Entity] terminal token. 256 | # @return [String] the entity as-is. 257 | def visit_entity entity 258 | # for instance `"` shall render `"` 259 | return escape_tag entity.entity 260 | end 261 | 262 | # Visit a code span. 263 | # 264 | # @param span [CodeSpan] terminal token. 265 | # @return [Array] rendered code. 266 | def visit_code_span span 267 | ent = escape_tag span.entity 268 | return [ '', ent, ''] 269 | end 270 | 271 | # Visit a raw html. 272 | # 273 | # @param tag [RawHTML] terminal token. 274 | # @return [String] verbatim input. 275 | def visit_raw_html tag 276 | return tag.entity 277 | end 278 | 279 | # Visit an autolink. 280 | # 281 | # @param url [AutoLink] terminal token. 282 | # @return [Array] rendered link. 283 | def visit_auto_link url 284 | href = escape_href url.href 285 | disp = escape_tag url.display 286 | return [ '', disp, ''] 287 | end 288 | 289 | # Visit an image. 290 | # 291 | # @param img [Link::Img] Img node. 292 | # @param label [Array] leaf node visiting result. 293 | # @param title [Array] leaf node visiting result. 294 | # @return [Array] rendered image. 295 | def visit_image img, label, title 296 | # `label` and `title` not used due to its HTML tags 297 | dest = escape_href img.attr[:dest] 298 | ret = [ ' in rendering to HTML, only the plain string content of the image 301 | # > description be used. 302 | # 303 | # So the HTML-rendered `label` variable cannot be used herein. 304 | plain = Optdown::PlaintextRenderer.new.render img.attr[:label] 305 | escaped = escape_tag plain 306 | ret << [ ' alt="', escaped, '"' ] 307 | end 308 | ret << [ ' title="', title, '"' ] if title 309 | ret << ' />' 310 | return ret 311 | end 312 | 313 | # Visit a link. 314 | # 315 | # @param link [Link::A] Link node. 316 | # @param label [Array] leaf node visiting result. 317 | # @param title [Array] leaf node visiting result. 318 | # @return [Array] rendered link. 319 | def visit_link link, label, title 320 | dest = escape_href link.attr[:dest] 321 | ret = [ '', label, '' ] 324 | return ret 325 | end 326 | 327 | # Visit a link title. 328 | # 329 | # @param title [LinkTitle] Link title node. 330 | # @param children [Array] leaf node visiting result. 331 | # @return [Array] rendered title. 332 | def visit_link_title title, children 333 | return children 334 | end 335 | 336 | # Visit an emphasis. 337 | # 338 | # @param emphasis [Emphasis] Emphasis node. 339 | # @param leafs [Array] leaf node visiting result. 340 | # @return [Array] rendered emphasis. 341 | def visit_emphasis emphasis, leafs 342 | # According to the rule #14 of the spec, `***` is `` rather 343 | # than ``. 344 | ret = leafs 345 | lv = emphasis.level 346 | while lv > 1 do 347 | ret = [ '', ret, '' ] 348 | lv -= 2 349 | end 350 | if lv.odd? 351 | ret = [ '', ret, '' ] 352 | end 353 | return ret 354 | end 355 | 356 | # Visit a strikethrough. 357 | # 358 | # @param st [Strikethrough] Strikethrough node. 359 | # @param leafs [Array] leaf node visiting result. 360 | # @return [Array] rendered strikethrough. 361 | def visit_strikethrough st, leafs 362 | return ['', leafs, ''] 363 | end 364 | 365 | private 366 | 367 | def visit_table_internal x, y, t 368 | flag = false 369 | ret = [] 370 | y.each do |tr| 371 | if flag then 372 | ret << Newline 373 | else 374 | flag = true 375 | end 376 | z = tr.map.with_index do |td, i| 377 | case x[i] when NilClass then 378 | tag = ['<', t, '>'] 379 | else 380 | tag = [ '<', t, ' align="', x[i], '">' ] 381 | end 382 | next [ tag, td, '', Newline ] 383 | end 384 | ret << [ '', Newline, z, '' ] 385 | end 386 | return ret 387 | end 388 | end 389 | -------------------------------------------------------------------------------- /lib/optdown/plugins/plaintext_renderer.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # GFM compatible HTML renderer. 27 | class Optdown::PlaintextRenderer < Optdown::Renderer 28 | # Visit a blocklevel. 29 | # 30 | # @param leafs [Array] leaf node visiting result. 31 | # @return [Array] leaf result. 32 | def visit_blocklevel _, leafs 33 | return leafs 34 | end 35 | 36 | # Visit a thematic break. 37 | # 38 | # @return [String] the verbatim input. 39 | def visit_thematic_break hr; 40 | return hr.entity 41 | end 42 | 43 | # Visit a link definition. 44 | # 45 | # @return [String] an empty string. 46 | def visit_link_definition _, _ 47 | return '' 48 | end 49 | 50 | # Visit a blockquote. 51 | # 52 | # @param inner [Array] leaf node visiting result. 53 | # @return [Array] leaf result. 54 | def visit_blockquote _, inner 55 | return indent '> ', inner 56 | end 57 | 58 | # Visit a list. 59 | # 60 | # @param list [List] list node. 61 | # @param items [Array] leaf node visiting result. 62 | # @return [Array] leaf result. 63 | def visit_list list, items 64 | return items 65 | end 66 | 67 | # Visit a list item. 68 | # 69 | # @param li [ListItem] list item node. 70 | # @param inner [Array] leaf node visiting result. 71 | # @return [Array] leaf result. 72 | def visit_list_item li, inner 73 | case li.type 74 | when :bullet then 75 | ret = indent ' ', inner 76 | ret.sub! ' ', '- ' 77 | return ret 78 | when :ordered then 79 | mark = li.marker + ' ' 80 | pad = ' ' * mark.length 81 | ret = indent pad, inner 82 | ret.sub! pad, mark 83 | return ret 84 | when :task then 85 | ret = indent ' ', inner 86 | if li.checked? then 87 | ret.sub! ' ', '- [x] ' 88 | else 89 | ret.sub! ' ', '- [ ] ' 90 | end 91 | return ret 92 | else 93 | rprintf RuntimeError, 'unsupported list type %s', list.type 94 | end 95 | end 96 | 97 | # Visit an HTML block. 98 | # 99 | # @param tag [BlockHTML] block html node. 100 | # @return [Array] nothing to do. 101 | def visit_blockhtml tag 102 | return tag.html.to_s 103 | end 104 | 105 | # Visit a code block. 106 | # 107 | # @param pre [IndentedCodeBlock, FencedCodeBlock] code block node. 108 | # @return [Array] nothing to do. 109 | def visit_code_block pre 110 | return indent ' ', pre.pre.to_s 111 | end 112 | 113 | # Visit a heading. 114 | # 115 | # @param h [ATXHeading, SetextHeading] heading node. 116 | # @param inner [Array] leaf node visiting result. 117 | # @return [Array] leaf result. 118 | def visit_heading h, inner 119 | case h.level 120 | when 1 then 121 | return inner + "\n" + "=" * inner.length 122 | when 2 then 123 | return inner + "\n" + "-" * inner.length 124 | else 125 | return "#" * h.level + " " + inner + "\n" 126 | end 127 | end 128 | 129 | # Visit a table. 130 | # 131 | # @param table [Table] Table node. 132 | # @param thead [Array] first line leaf node visiting result. 133 | # @param tbody [Array] visiting results for 3rd line and beyond. 134 | # @return [Array] leaf result. 135 | # def visit_table table, thead, tbody 136 | # # sorry, not yet. 137 | # # TBW 138 | # end 139 | 140 | # Visit a paragraph. 141 | # 142 | # @param paragraph [Paragraph] paragraph node. 143 | # @param tightp [true, false] paragraph tightness. 144 | # @param inner [Object] leaf node visiting result. 145 | # @return [Object] leaf result. 146 | def visit_paragraph paragraph, tightp, inner 147 | if tightp then 148 | return inner + "\n" 149 | else 150 | return inner + "\n\n" 151 | end 152 | end 153 | 154 | # Visit an inline. 155 | # 156 | # @param inline [Inline] inline node. 157 | # @param leafs [Array] leaf node visiting result. 158 | # @return [Array] leaf result. 159 | def visit_inline inline, leafs 160 | return leafs 161 | end 162 | 163 | # Visit a token. 164 | # 165 | # @param token [Token] terminal token. 166 | # @return [Array] nothing to do. 167 | def visit_token token 168 | return token.to_s 169 | end 170 | 171 | # Visit an escape. 172 | # 173 | # @param escape [Escape] terminal token. 174 | # @return [Array] nothing to do. 175 | def visit_escape escape 176 | return escape.entity 177 | end 178 | 179 | # Visit an entity. 180 | # 181 | # @param entity [Entity] terminal token. 182 | # @return [Array] nothing to do. 183 | def visit_entity entity 184 | return entity.entity 185 | end 186 | 187 | # Visit a code span. 188 | # 189 | # @param span [CodeSpan] terminal token. 190 | # @return [Array] nothing to do. 191 | def visit_code_span span 192 | return span 193 | end 194 | 195 | # Visit a raw html. 196 | # 197 | # @param tag [RawHTML] terminal token. 198 | # @return [Array] nothing to do. 199 | def visit_raw_html tag 200 | return tag.entity 201 | end 202 | 203 | # Visit an autolink. 204 | # 205 | # @param url [AutoLink] terminal token. 206 | # @return [Array] nothing to do. 207 | def visit_auto_link url 208 | return url.display 209 | end 210 | 211 | # Visit an image. 212 | # 213 | # @param img [Link::Img] Img node. 214 | # @param label [Array] leaf node visiting result. 215 | # @param title [Array] leaf node visiting result. 216 | # @return [Array] leaf result. 217 | def visit_image img, label, title 218 | return label 219 | end 220 | 221 | # Visit a link. 222 | # 223 | # @param link [Link::A] Link node. 224 | # @param label [Array] leaf node visiting result. 225 | # @param title [Array] leaf node visiting result. 226 | # @return [Array] leaf result. 227 | def visit_link link, label, title 228 | return label 229 | end 230 | 231 | # Visit an emphasis. 232 | # 233 | # @param emphasis [Emphasis] Emphasis node. 234 | # @param leafs [Array] leaf node visiting result. 235 | # @return [Array] leaf result. 236 | def visit_emphasis emphasis, leafs 237 | return leafs 238 | end 239 | 240 | # Visit a strikethrough. 241 | # 242 | # @param st [Strikethrough] Strikethrough node. 243 | # @param leafs [Array] leaf node visiting result. 244 | # @return [Array] leaf result. 245 | def visit_strikethrough st, leafs 246 | return leafs 247 | end 248 | 249 | private 250 | 251 | def idnent pad, lines 252 | return lines.gsub %r/#{Optdown::EXPR}\g/o, pad 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /lib/optdown/raw_html.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # @see http://spec.commonmark.org/0.28/#raw-html 27 | class Optdown::RawHTML 28 | attr_reader :entity # @return [String] the content. 29 | 30 | # @param tok [Token] terminal token. 31 | def initialize tok 32 | @entity = tok.yylval['tag'] 33 | end 34 | 35 | # (see Optdown::Inline#accept) 36 | def accept visitor 37 | return visitor.visit_raw_html self 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/optdown/renderer.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # Renderer. 27 | # 28 | # This class is not only the base class of each custom renderers, but also a 29 | # null renderer. When a node is passed to it an empty string would be 30 | # rendered. Child classes shall override each visit_* methods. 31 | class Optdown::Renderer 32 | 33 | # Render the node. In this particular class, it returns an empty string 34 | # after traversing the AST. 35 | # 36 | # @param node [Object] AST node. 37 | # @return [Object] node's acceptance. 38 | def render node 39 | ary = visit node 40 | return [ ary ].join 41 | end 42 | 43 | # https://bugs.ruby-lang.org/issues/10856 44 | if RUBY_VERSION < '2.5' then 45 | # Visit a node. 46 | # 47 | # @param node [Object] AST node. 48 | # @return [Object] node's acceptance. 49 | def visit node, **args 50 | if args.empty? then 51 | node.accept self 52 | else 53 | node.accept self, **args 54 | end 55 | end 56 | else 57 | # Visit a node. 58 | # 59 | # @param node [Object] AST node. 60 | # @return [Object] node's acceptance. 61 | def visit node, **args 62 | node.accept self, **args 63 | end 64 | end 65 | 66 | # @!group Methods that are expected to be overridden by child classes 67 | 68 | # Visit a blocklevel. 69 | # 70 | # @param block [Blocklevel] blocklevel node. 71 | # @param leafs [Array] leaf node visiting result. 72 | # @return [Array] leaf result. 73 | def visit_blocklevel block, leafs 74 | return leafs 75 | end 76 | 77 | # Visit a thematic break. 78 | # 79 | # @param hr [ThematicBreak] thematic break node. 80 | # @return [Array] nothing to do. 81 | def visit_thematic_break hr 82 | return [] 83 | end 84 | 85 | # Visit a link definition. 86 | # 87 | # @param ld [LinkDef] link def node. 88 | # @param title [Array] leaf node visiting result. 89 | # @return [Array] leaf result. 90 | def visit_link_definition ld, title 91 | return title 92 | end 93 | 94 | # Visit a blockquote. 95 | # 96 | # @param bq [BlockQuote] blockquote node. 97 | # @param inner [Array] leaf node visiting result. 98 | # @return [Array] leaf result. 99 | def visit_blockquote bq, inner 100 | return inner 101 | end 102 | 103 | # Visit a list. 104 | # 105 | # @param list [List] list node. 106 | # @param items [Array] leaf node visiting result. 107 | # @return [Array] leaf result. 108 | def visit_list list, items 109 | return items 110 | end 111 | 112 | # Visit a list item. 113 | # 114 | # @param li [ListItem] list item node. 115 | # @param inner [Array] leaf node visiting result. 116 | # @return [Array] leaf result. 117 | def visit_list_item li, inner 118 | return inner 119 | end 120 | 121 | # Visit an HTML block. 122 | # 123 | # @param tag [BlockHTML] block html node. 124 | # @return [Array] nothing to do. 125 | def visit_blockhtml tag 126 | return [] 127 | end 128 | 129 | # Visit a code block. 130 | # 131 | # @param pre [IndentedCodeBlock, FencedCodeBlock] code block node. 132 | # @return [Array] nothing to do. 133 | def visit_code_block pre 134 | return [] 135 | end 136 | 137 | # Visit a heading. 138 | # 139 | # @param h [ATXHeading, SetextHeading] heading node. 140 | # @param inner [Array] leaf node visiting result. 141 | # @return [Array] leaf result. 142 | def visit_heading h, inner 143 | return inner 144 | end 145 | 146 | # Visit a table. 147 | # 148 | # @param table [Table] Table node. 149 | # @param thead [Array] first line leaf node visiting result. 150 | # @param tbody [Array] visiting results for 3rd line and beyond. 151 | # @return [Array] leaf result. 152 | def visit_table table, thead, tbody 153 | return thead && tbody 154 | end 155 | 156 | # Visit a paragraph. 157 | # 158 | # @param paragraph [Paragraph] paragraph node. 159 | # @param tightp [true, false] paragraph tightness. 160 | # @param inner [Object] leaf node visiting result. 161 | # @return [Object] leaf result. 162 | def visit_paragraph paragraph, tightp, inner 163 | return inner 164 | end 165 | 166 | # Visit an inline. 167 | # 168 | # @param inline [Inline] inline node. 169 | # @param leafs [Array] leaf node visiting result. 170 | # @return [Array] leaf result. 171 | def visit_inline inline, leafs 172 | return leafs 173 | end 174 | 175 | # Visit a token. 176 | # 177 | # @param token [Token] terminal token. 178 | # @return [Array] nothing to do. 179 | def visit_token token 180 | return [] 181 | end 182 | 183 | # Visit a newline. 184 | # 185 | # @param br [Newline] terminal token. 186 | # @return [Array] nothing to do. 187 | def visit_newline br 188 | return [] 189 | end 190 | 191 | # Visit an escape. 192 | # 193 | # @param escape [Escape] terminal token. 194 | # @return [Array] nothing to do. 195 | def visit_escape escape 196 | return [] 197 | end 198 | 199 | # Visit an entity. 200 | # 201 | # @param entity [Entity] terminal token. 202 | # @return [Array] nothing to do. 203 | def visit_entity entity 204 | return [] 205 | end 206 | 207 | # Visit a code span. 208 | # 209 | # @param span [CodeSpan] terminal token. 210 | # @return [Array] nothing to do. 211 | def visit_code_span span 212 | return [] 213 | end 214 | 215 | # Visit a raw html. 216 | # 217 | # @param tag [RawHTML] terminal token. 218 | # @return [Array] nothing to do. 219 | def visit_raw_html tag 220 | return [] 221 | end 222 | 223 | # Visit an autolink. 224 | # 225 | # @param url [AutoLink] terminal token. 226 | # @return [Array] nothing to do. 227 | def visit_auto_link url 228 | return [] 229 | end 230 | 231 | # Visit an image. 232 | # 233 | # @param img [Img] Img node. 234 | # @param label [Array] leaf node visiting result. 235 | # @param title [Array] leaf node visiting result. 236 | # @return [Array] leaf result. 237 | def visit_image img, label, title 238 | return label 239 | end 240 | 241 | # Visit a link. 242 | # 243 | # @param link [Link] Link node. 244 | # @param label [Array] leaf node visiting result. 245 | # @param title [Array] leaf node visiting result. 246 | # @return [Array] leaf result. 247 | def visit_link link, label, title 248 | return label 249 | end 250 | 251 | # Visit a link title. 252 | # 253 | # @param title [LinkTitle] Link title node. 254 | # @param children [Array] leaf node visiting result. 255 | # @return [Array] leaf result. 256 | def visit_link_title title, children 257 | return children 258 | end 259 | 260 | # Visit an emphasis. 261 | # 262 | # @param emphasis [Emphasis] Emphasis node. 263 | # @param leafs [Array] leaf node visiting result. 264 | # @return [Array] leaf result. 265 | def visit_emphasis emphasis, leafs 266 | return leafs 267 | end 268 | 269 | # Visit a strikethrough. 270 | # 271 | # @param st [Strikethrough] Strikethrough node. 272 | # @param leafs [Array] leaf node visiting result. 273 | # @return [Array] leaf result. 274 | def visit_strikethrough st, leafs 275 | return leafs 276 | end 277 | 278 | # @!endgroup 279 | end 280 | -------------------------------------------------------------------------------- /lib/optdown/setext_heading.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'matcher' 28 | 29 | # @see http://spec.commonmark.org/0.28/#setext-headings 30 | class Optdown::SetextHeading 31 | using Optdown::Matcher::Refinements 32 | 33 | attr_reader :level # return [Integer] heading level 34 | 35 | # This class is specified as "the lines of text must be such that, were they 36 | # not followed by the setext heading underline, they would be interpreted as 37 | # a paragraph" so we have to check that part here by actually deleting the 38 | # last line and parse it as a paragraph. 39 | # 40 | # @param (see Optdown::Blocklevel#initialize) 41 | # @return [SetextHeading] peaceful creation of an instance. 42 | # @return [Blocklevel] other classes are possible depending on contexts. 43 | # @return [nil] ... or completely fails to parse, at worst. 44 | def self.new str, ctx 45 | ptr = str.dup 46 | ptr.match %r/ 47 | #{Optdown::EXPR}\G (? \g+? ) (?= \g ) 48 | /xo 49 | cand = Optdown::Blocklevel.new ptr['txt'], ctx 50 | ary = cand.children 51 | klass = ary.first.class 52 | 53 | return klass.new str, ctx unless ary.length == 1 54 | return klass.new str, ctx unless klass == Optdown::Paragraph 55 | return super # OK, this is a valid setext heading. 56 | end 57 | 58 | # (see Optdown::Blocklevel#initialize) 59 | def initialize str, ctx 60 | a = [] 61 | until str.eos? do 62 | case str when /#{Optdown::EXPR}\G\g/o then 63 | break 64 | else 65 | a << str.gets 66 | end 67 | end 68 | b = Optdown::Matcher.join a 69 | @children = Optdown::Paragraph.new b, ctx 70 | 71 | if str.last_match.begin 'sh:lv1' then 72 | @level = 1 73 | else 74 | @level = 2 75 | end 76 | end 77 | 78 | # (see Optdown::Blocklevel#accept) 79 | def accept visitor, tightp: false 80 | inner = visitor.visit @children, tightp: true # always tight here. 81 | return visitor.visit_heading self, inner 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/optdown/strikethrough.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: false -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'flanker' 27 | 28 | # @see https://github.github.com/gfm/#strikethrough-extension- 29 | class Optdown::Strikethrough < Optdown::Flanker 30 | def self.opener? tok 31 | return super && tok.yylval['flanker:run:~'] 32 | end 33 | 34 | def self.closer? tok 35 | return super && tok.yylval['flanker:run:~'] 36 | end 37 | 38 | # strilethrough allow mismatching number of tildes. 39 | def self.reduce tokens, iopen, iclose, ctx 40 | range = iopen..iclose 41 | body = tokens[range].compact 42 | t1 = body.shift 43 | t2 = body.pop 44 | recur body, ctx 45 | node = new t1, body, t2 46 | tokens.fill nil, range 47 | tokens[iopen, 3] = [node] 48 | tokens.compact! 49 | end 50 | 51 | # (see Optdown::Inline#accept) 52 | def accept visitor 53 | elems = @children.map {|i| visitor.visit i } 54 | return visitor.visit_strikethrough self, elems 55 | end 56 | 57 | def initialize open, body, close 58 | @children = body 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/optdown/table.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'expr' 27 | require_relative 'matcher' 28 | require_relative 'inline' 29 | 30 | # @see https://github.github.com/gfm/#tables-extension- 31 | class Optdown::Table 32 | using Optdown::Matcher::Refinements 33 | 34 | # Table cells can include pipes by escaping. Should be tokenized before 35 | # splitting cells. 36 | # 37 | # @param line [Matcher] table row. 38 | # @return [Array>] tokenized table cells. 39 | def self.split line 40 | return Optdown::Inline \ 41 | . tokenize(line) \ 42 | . chunk {|t| t.yylex == :'table' and :_separator } \ 43 | . map {|_, i| i } \ 44 | . to_a \ 45 | end 46 | 47 | # Header and delimiter rows must match in the number of cells. We cannot 48 | # check that criteria using regular expression (can we?). We cannot but check 49 | # here by hand-written logic instead. All illegal table-ish inputs are 50 | # considered to be paragraphs. 51 | # 52 | # @param (see Optdown::Blocklevel#initialize) 53 | # @return [Talbe] peaceful creation of an instance. 54 | # @return [Paragraph] unmatched delimiter row. 55 | def self.new str, ctx 56 | tmp = str.dup 57 | tmp.match %r/#{Optdown::EXPR}\G\g/o 58 | th = split tmp['table:th'] 59 | dr = split tmp['table:dr'] 60 | if th.length == dr.length then 61 | return super # ok 62 | else 63 | return Optdown::Paragraph.new str, ctx 64 | end 65 | end 66 | 67 | # (see Optdown::Blocklevel#initialize) 68 | def initialize str, ctx 69 | # at least 2 lines must be readable at this point. 70 | th = str.match %r/#{Optdown::EXPR} 71 | \G\g\g\g/xo 72 | dr = str.match %r/#{Optdown::EXPR} 73 | \G\g\g\g/xo 74 | @th = self.class.split str[th, 'table:th'] 75 | @th.map! {|i| Optdown::Inline.new i, ctx } 76 | str[dr, 'table:dr'].split('|').each_with_index do |i, j| 77 | @th[j] = [ 78 | @th[j], 79 | case i 80 | when /\A\s*:-+:\s*\z/ then :center 81 | when /\A\s*:-+\s*\z/ then :left 82 | when /\A\s*-+:\s*\z/ then :right 83 | else nil 84 | end 85 | ] 86 | end 87 | 88 | # OK then, read until non-table. 89 | @td = [] 90 | until str.eos? do 91 | case str 92 | when /#{Optdown::EXPR}\G\g/o then break 93 | when /#{Optdown::EXPR}\G\g\g\g/o then 94 | tr = self.class.split str['table:tr'] 95 | row = [] 96 | @th.size.times {|i| row << Optdown::Inline.new(tr[i] || [], ctx) } 97 | @td << row 98 | end 99 | end 100 | end 101 | 102 | # Extracted list of align specifiers, for each columns. 103 | # 104 | # @return [Array] align specifiers. 105 | def alignments 106 | return @th.map{|a| a[1] } 107 | end 108 | 109 | # (see Optdown::Blocklevel#accept) 110 | def accept visitor, tightp: false 111 | thead = @th.map {|i| visitor.visit i[0] } 112 | tbody = @td.map {|tr| tr.map {|td| visitor.visit td } } 113 | return visitor.visit_table self, thead, tbody 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/optdown/thematic_break.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # @see http://spec.commonmark.org/0.28/#thematic-breaks 27 | class Optdown::ThematicBreak 28 | 29 | attr_reader :entity # @return [String] the content. 30 | 31 | # (see Optdown::Blocklevel#initialize) 32 | def initialize str, ctx 33 | @entity = str['hr'] 34 | end 35 | 36 | # (see Optdown::Blocklevel#accept) 37 | def accept visitor, tightp: false 38 | return visitor.visit_thematic_break self 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/optdown/token.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'xprintf' 27 | 28 | # Token is an intermediate node that would eventually be "reduce"-d into some 29 | # other kind of inline nodes. 30 | class Optdown::Token 31 | using Optdown::XPrintf 32 | 33 | attr_reader :yylex # @return [Symbol] terminal symbol. 34 | attr_reader :yytext # @return [String] terminal physical text. 35 | attr_reader :yylval # @return [String] terminal value. 36 | 37 | alias to_sym yylex 38 | alias to_s yytext 39 | 40 | # @param yylex [Symbol] terminal symbol. 41 | # @param yylval [String] terminal value. 42 | def initialize yylex, yylval, yytext = yylval 43 | @yylval = yylval 44 | @yytext = yytext 45 | @yylex = yylex || symbolize 46 | end 47 | 48 | # throw away the parsed symbol and convert into verbatim cdata. 49 | def cdataify 50 | return self.class.new :cdata, @yytext 51 | end 52 | 53 | # easy debug 54 | # @return [String] inspection. 55 | def inspect 56 | sprintf '%p%p', yytext, yylex 57 | end 58 | 59 | # (see Optdown::Inline#accept) 60 | def accept visitor 61 | return visitor.visit_token self 62 | end 63 | 64 | private 65 | 66 | def symbolize 67 | md = yylval 68 | case 69 | when md['br'] then return :'break' 70 | when md['a:left'] then return :'link' 71 | when md['a:right'] then return :'link' 72 | when md['auto'] then return :'autolink' 73 | when md['auto:GH'] then return :'autolink' 74 | when md['code'] then return :'code' 75 | when md['entity:dec'] then return :'entity' 76 | when md['entity:hex'] then return :'entity' 77 | when md['entity:named'] then return :'entity' 78 | when md['escape'] then return :'escape' 79 | when md['flanker:and'] then return :'flanker' 80 | when md['flanker:left'] then return :'flanker' 81 | when md['flanker:right'] then return :'flanker' 82 | when md['img:left'] then return :'link' 83 | when md['tag'] then return :'tag' 84 | when md['table:delim'] then return :'table' 85 | else rprintf RuntimeError, 'TBW: %p', md 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/optdown/xprintf.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: false -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | # require 'logger' # intentionally left not required. 27 | 28 | # This module is a Refinements. It extends several existing methods so that 29 | # they can have printf-like format specifiers. 30 | module Optdown::XPrintf 31 | 32 | refine Kernel do 33 | 34 | # Utility logger + printf function. It is quite hard to think of loggings 35 | # that only concern fixed strings. @shyouhei really doesn't understand why 36 | # this is not a canon. 37 | # 38 | # @param logger [Logger] log destination. 39 | # @param lv [Symbol] log level. 40 | # @param fmt [String] printf-format string. 41 | # @param va_args [Array] anything. 42 | def lprintf logger, lv, fmt, *va_args 43 | str = fmt % va_args 44 | logger.send lv, str 45 | end 46 | 47 | # Utility raise + printf function. It is quite hard to think of exceptions 48 | # that only concern fixed strings. @shyouhei really doesn't understand why 49 | # this is not a canon. 50 | # 51 | # @param exc [Class] exception class. 52 | # @param fmt [String] printf-format string. 53 | # @param va_args [Array] anything. 54 | def rprintf exc, fmt, *va_args 55 | msg = fmt % va_args 56 | raise exc, msg, caller # caller() == caller(1) i.e. skip this frame. 57 | end 58 | 59 | # Utility warn + printf function. It is quite hard to think of warnings 60 | # that only concern fixed strings. @shyouhei really doesn't understand why 61 | # this is not a canon. 62 | # 63 | # @param fmt [String] printf-format string. 64 | # @param va_args [Array] anything. 65 | def wprintf fmt, *va_args 66 | msg = fmt % va_args 67 | Warning.warn msg 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | spec.json 2 | -------------------------------------------------------------------------------- /test/000_compile.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'test_helper' 27 | 28 | class TC000_compile < Test::Unit::TestCase 29 | def test_compile 30 | assert_nothing_raised do 31 | require 'optdown' 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/always_frozen.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'test_helper' 27 | require 'optdown' 28 | 29 | class TestTarget 30 | prepend ::Optdown::AlwaysFrozen 31 | end 32 | 33 | class TC_AlwaysFrozen < Test::Unit::TestCase 34 | subject = TestTarget.new 35 | 36 | data( 37 | "#frozen?" => subject, 38 | "#dup" => subject.dup, 39 | "#clone" => subject.clone, 40 | "freeze: false" => subject.clone(freeze: false) 41 | ) 42 | 43 | test "#frozen?" do |obj| 44 | assert_equal(true, obj.frozen?) 45 | end 46 | 47 | test ".included" do 48 | assert_raise_message(/must be prepended/) do 49 | Class.new do 50 | include ::Optdown::AlwaysFrozen 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/blocklevel.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'test_helper' 27 | require 'optdown' 28 | 29 | class TC_Blocklevel < Test::Unit::TestCase 30 | sub_test_case '.new' do 31 | data( 32 | 'empty' => ['', []], 33 | 'p' => ['foo', %w[Paragraph]], 34 | 'table' => ["foo|bar\n|-|-|\nfoo|bar\n", %w[Table]], 35 | 'setext' => ["foo\n===", %w[SetextHeading]], 36 | 'atx' => ['### foo ###', %w[ATXHeading]], 37 | 'pre' => [' foo', %w[IndentedCodeBlock]], 38 | 'fence' => ["```\nfoo\n```", %w[FencedCodeBlock]], 39 | 'tag' => ['', %w[BlockHTML]], 40 | 'li' => ['- foo', %w[List]], 41 | 'quote' => ['> foo', %w[Blockquote]], 42 | 'link' => ['[foo]: "baz"', %w[LinkDef]], 43 | 'hr' => ['--------', %w[ThematicBreak]], 44 | 45 | 'link00' => [%'[foo]:\n(bar)', %w[LinkDef]], 46 | 'link01' => [%"[foo]:\n'bar'", %w[LinkDef]], 47 | 'link02' => [%'[foo]:\n"bar"', %w[LinkDef]], 48 | 'linkb' => [%'[foo]:\nbar\n"baz"', %w[LinkDef]], 49 | 'link<' => [%'[foo]:\n', %w[LinkDef]], 50 | 'link\n' => [%'[foo]: "\nb\na\nz\n"', %w[LinkDef]], 51 | 'link\n\n' => [%'[foo]: "\nb\n\na\nz\n"', %w[Paragraph Paragraph]], 52 | 'link10' => [%' [foo]:\n \n "baz"', %w[LinkDef]], 53 | 'link20' => [%' [foo]:\n \n "baz"', %w[LinkDef]], 54 | 'link30' => [%' [foo]:\n \n "baz"', %w[LinkDef]], 55 | 'link40' => [%' [foo]:\n \n "baz"', %w[IndentedCodeBlock]], 56 | 'link41' => [%'[foo]:\n \n "baz"', %w[LinkDef]], 57 | 58 | 'quote00' => [%'>foo', %w[Blockquote]], 59 | 'quote01' => [%'> foo', %w[Blockquote]], 60 | 'quote02' => [%'> foo', %w[Blockquote]], 61 | 'quote03' => [%'> foo', %w[Blockquote]], 62 | 'quote04' => [%'> foo', %w[Blockquote]], 63 | 'quote n1' => [%'> foo\n> nbar\n', %w[Blockquote]], 64 | 'quote n2' => [%'> foo\n\n> bar', %w[Blockquote Blockquote]], 65 | 'quote l1' => [%'> foo\nbar\n', %w[Blockquote]], 66 | 'quote l2' => [%'> foo\n- bar\n', %w[Blockquote List]], 67 | 'quote l3' => [%'> foo\n1. bar\n', %w[Blockquote List]], 68 | 'quote l4' => [%'> foo\n2. bar\n', %w[Blockquote]], # https://github.com/commonmark/cmark/issues/204 69 | 'quote 01' => [%'>\nfoo', %w[Blockquote Paragraph]], 70 | 71 | 'li -' => [%'- foo', %w[List]], 72 | 'li *' => [%'* foo', %w[List]], 73 | 'li +' => [%'+ foo', %w[List]], 74 | 'li [ ]' => [%'- [ ] foo', %w[List]], 75 | 'li [x]' => [%'- [x] foo', %w[List]], 76 | 'li 1' => [%'1. foo', %w[List]], 77 | 'li 0' => [%'0. foo', %w[List]], 78 | 'li 9' => [%'9. foo', %w[List]], 79 | 'li 9+' => [%'1234567890. foo', %w[Paragraph]], 80 | 'li hr' => [%'- - -', %w[ThematicBreak]], 81 | 'li i0' => [%"- foo\n> bar", %w[List Blockquote]], 82 | 'li i1' => [%"- foo\n > bar", %w[List Blockquote]], 83 | 'li i2' => [%"- foo\n > bar", %w[List]], 84 | 'li i3' => [%"- foo\n > bar", %w[List]], 85 | 'li i4' => [%"- foo\n > bar", %w[List]], 86 | 'li n1' => [%"- foo\n\nbar", %w[List Paragraph]], 87 | 'li n2' => [%"- foo\n\n- bar", %w[List]], 88 | 'li n3' => [%"- foo\n\n+ bar", %w[List List]], 89 | 'li pre' => [%"- \n\t foo\n", %w[List]], 90 | 'li eol' => [%"-\n foo", %w[List]], 91 | 'li p' => [%"- foo\n\n foo\n", %w[List Paragraph]], 92 | 93 | 'tag10' => ['
    foo
    ', %w[BlockHTML]], 94 | 'tag11' => ["
    \nfoo\n
    \nbar", %w[BlockHTML Paragraph]], 95 | 'tag20' => ['', %w[BlockHTML]], 96 | 'tag21' => ["\nbar", %w[BlockHTML Paragraph]], 97 | 'tag30' => ['', %w[BlockHTML]], 98 | 'tag31' => ["\nbar", %w[BlockHTML Paragraph]], 99 | 'tag40' => ['', %w[BlockHTML]], 100 | 'tag41' => ["\nbar", %w[BlockHTML Paragraph]], 101 | 'tag50' => [']]>', %w[BlockHTML]], 102 | 'tag51' => ["\nbar", %w[BlockHTML Paragraph]], 103 | 'tag60' => ['', %w[BlockHTML]], 104 | 'tag61' => ["\nfoo\n\n\nbar", %w[BlockHTML Paragraph]], 105 | 'tag70' => ['', %w[BlockHTML]], 106 | 'tag71' => ["\nfoo\n\n\nbar", %w[BlockHTML Paragraph]], 107 | 'tag72' => ["foo\n\nbar", %w[Paragraph Paragraph]], 108 | 109 | 'fence3' => ["```\nfoo\n```\nfoo", %w[FencedCodeBlock Paragraph]], 110 | 'fence4' => ["````\nfoo\n````", %w[FencedCodeBlock]], 111 | 'fence4+' => ["````\nfoo\n```````````", %w[FencedCodeBlock]], 112 | 'fence i1' => [" ```\nfoo\n ```", %w[FencedCodeBlock]], 113 | 'fence i2' => [" ```\nfoo\n ```", %w[FencedCodeBlock]], 114 | 'fence i3' => [" ```\nfoo\n ```", %w[FencedCodeBlock]], 115 | 'fence i4' => [" ```\nfoo\n ```", %w[IndentedCodeBlock Paragraph]], 116 | 117 | 'pre\t' => ["\tfoo\n", %w[IndentedCodeBlock]], 118 | 'pre+' => [" foo\n \n bar", %w[IndentedCodeBlock]], 119 | 120 | 'atx 0' => ['### foo', %w[ATXHeading]], 121 | 'atx 1' => [' ### foo', %w[ATXHeading]], 122 | 'atx 2' => [' ### foo', %w[ATXHeading]], 123 | 'atx 3' => [' ### foo', %w[ATXHeading]], 124 | 'atx 4' => [' ### foo', %w[IndentedCodeBlock]], 125 | 'atx t' => ['### foo #', %w[ATXHeading]], 126 | 127 | 'setext p' => ["- foo\n====", %w[List]], 128 | 'setext hr' => ["- foo\n----", %w[List ThematicBreak]], 129 | 'setext bq' => ["> foo\n====", %w[Blockquote]], 130 | 'setext 1' => [" foo\n----", %w[SetextHeading]], 131 | 'setext 2' => [" foo\n----", %w[SetextHeading]], 132 | 'setext 3' => [" foo\n----", %w[SetextHeading]], 133 | 'setext 4' => [" foo\n----", %w[IndentedCodeBlock ThematicBreak]], 134 | 135 | 'table 0' => ["foo|bar\n|-|-|\nfoo\n", %w[Table]], 136 | 'table 1' => [" foo|bar\n |-|-|\n foo\n", %w[Table]], 137 | 'table 2' => [" foo|bar\n |-|-|\n foo\n", %w[Table]], 138 | 'table 3' => [" foo|bar\n |-|-|\n foo\n", %w[Table]], 139 | 'table 4' => [" foo|bar\n |-|-|\n foo\n", %w[IndentedCodeBlock]], 140 | 'table -' => ["|foo|\n|-|\nfoo\n", %w[Table]], 141 | 'table :-' => ["|foo|\n|:-|\nfoo\n", %w[Table]], 142 | 'table -:' => ["|foo|\n|-:|\nfoo\n", %w[Table]], 143 | 'table ::' => ["|foo|\n|:-:|\nfoo\n", %w[Table]], 144 | 'table hr' => ["|foo|\n|-|\n----", %w[Table ThematicBreak]], 145 | 'table bq' => ["|foo|\n|-|\n> foo", %w[Table Blockquote]], 146 | 'table li' => ["|foo|\n- |\nfoo", %w[Paragraph List]], 147 | 'table h2' => ["|foo|\n---\nfoo", %w[SetextHeading Paragraph]], 148 | 149 | 'p ul 1' => ["foo\n- bar", %w[Paragraph List]], 150 | 'p ul 2' => ["foo\n-\nbar", %w[SetextHeading Paragraph]], 151 | 'p ol 1' => ["foo\n1. bar", %w[Paragraph List]], 152 | 'p ol 2' => ["foo\n2. bar", %w[Paragraph]], 153 | ) 154 | 155 | @@parser = Optdown::Parser.new 156 | 157 | test '.new' do |(src, expected)| 158 | obj = @@parser.parse src 159 | actual = obj.children.map{|i|/\w+\z/.match(i.class.to_s)[0]} 160 | # if expected != actual then 161 | # p obj.children 162 | # end 163 | assert_equal expected, actual 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /test/deeply_frozen.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'test_helper' 27 | require 'optdown' 28 | 29 | class TC_DeeplyFrozen < Test::Unit::TestCase 30 | using Optdown::DeeplyFrozen 31 | 32 | data( 33 | 'nil' => nil, 34 | '0' => 0, 35 | '1' => 1, 36 | '""' => "", 37 | 'String.new' => String.new, 38 | '[]' => [], 39 | '{}' => {}, 40 | 'Object.new' => Object.new, 41 | '[{}]' => [{}] 42 | ) 43 | 44 | test '#deeply_frozen_copy_of' do |subject| 45 | target = deeply_frozen_copy_of subject 46 | assert_true target.frozen? 47 | end 48 | 49 | test '#deeply_frozen_copy' do |subject| 50 | target = subject.deeply_frozen_copy 51 | assert_true target.frozen? 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/inline.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'test_helper' 27 | require 'optdown' 28 | 29 | class TC_Inline < Test::Unit::TestCase 30 | sub_test_case '.tokenize' do 31 | data( 32 | 'empty' => ['', %i[]], 33 | 'cdata' => ['foo', %i[cdata]], 34 | 35 | 'entity1' => ['\\&', %i[escape cdata]], 36 | 'entity2' => ['\\\\&', %i[escape entity]], 37 | 'entity3' => ['&', %i[entity]], 38 | 'entity4' => ['&', %i[entity]], 39 | 'entity5' => ['&', %i[cdata]], 40 | 'entity6' => ['�', %i[cdata]], 41 | 'entity7' => ['⫋︀', %i[entity]], 42 | 43 | 'code1' => ['`1`', %i[code]], 44 | 'code2' => ['`` `2` ``', %i[code]], 45 | 'code3' => ['\\``3``', %i[escape cdata]], 46 | 'code4' => ['```7\\```', %i[code]], 47 | 48 | ' left !right 1' => ['***abc', %i[flanker cdata]], 49 | ' left !right 2' => [' _abc,', %i[cdata flanker cdata]], 50 | ' left !right 3' => ['**"abc"', %i[flanker cdata]], 51 | ' left !right 4' => [' _"abc"', %i[cdata flanker cdata]], 52 | '!left right 1' => ['abc***', %i[cdata flanker]], 53 | '!left right 2' => ['abc_ ', %i[cdata flanker cdata]], 54 | '!left right 3' => ['"abc"**', %i[cdata flanker]], 55 | '!left right 4' => ['"abc"_ ', %i[cdata flanker cdata]], 56 | ' left right 1' => [' abc***def ', %i[cdata flanker cdata]], 57 | ' left right 2' => ['"abc"_"def"', %i[cdata flanker cdata]], 58 | '!left !right 1' => ['abc *** def', %i[cdata]], 59 | '!left !right 2' => ['a _ b', %i[cdata]], 60 | 61 | 'emph1' => ['*foo*', %i[flanker cdata flanker]], 62 | 63 | 'link1' => ['\\](foo)', %i[escape cdata]], 64 | 'link2' => ['\\\\](foo)', %i[escape link]], 65 | 'link3' => [']\\(foo)', %i[link escape cdata]], 66 | 'link4' => ['](f\\)oo)', %i[link]], 67 | 68 | 'html1' => ['\\', %i[escape cdata]], 69 | 'html2' => ['\\\\', %i[escape tag]], 70 | 'html3' => ['\\\\', %i[escape tag]], 71 | 72 | 'auto1' => ['foo ', %i[cdata autolink]], 73 | 'auto2' => ['foo ', %i[cdata autolink]], 74 | 'auto3' => ['foo www.example.com', %i[cdata autolink]], 75 | 'auto4' => ['foo http://www.example.com', %i[cdata autolink]], 76 | 'auto5' => ['foo root@mput.dip.jp', %i[cdata autolink]], 77 | 78 | 'br1' => ["foo \nbar", %i[cdata break cdata]], 79 | 'br2' => ["foo\\\nbar", %i[cdata break cdata]], 80 | 'br3' => ["foo \n bar", %i[cdata break cdata]] 81 | ) 82 | 83 | test '.tokenize' do |(src, expected)| 84 | str = Optdown::Matcher.new src 85 | actual = Optdown::Inline.tokenize str 86 | assert_equal expected, actual.map(&:to_sym) 87 | end 88 | end 89 | 90 | sub_test_case '.parse' do 91 | data( 92 | 'empty' => ['', nil], 93 | 'cdata' => ['foo', %w[Token]], 94 | 95 | 'escape1' => ['\\&', %w[Escape Token]], 96 | 'escape2' => ['\\\\&', %w[Escape Entity]], 97 | 98 | 'entityD' => ['&', %w[Entity]], 99 | 'entityX' => ['&', %w[Entity]], 100 | 'entityE' => ['⫋︀', %w[Entity]], 101 | 'entity;' => ['&', %w[Token]], 102 | 'entityo' => ['�', %w[Token]], 103 | 104 | 'code1' => ['`foo`bar', %w[CodeSpan Token]], 105 | 'code2' => ['`` ` ``foo', %w[CodeSpan Token]], 106 | 'code3' => ['```foo``', %w[Token]], 107 | 'code\\' => ['`esc\\`foo', %w[CodeSpan Token]], 108 | 109 | 'aster1' => ['*foo*bar', %w[Aster Token]], 110 | 'aster2' => ['**foo**bar', %w[Aster Token]], 111 | 'aster3' => ['***foo*** bar', %w[Aster Token]], 112 | 'aster3-' => ['***foo***bar', %w[Token Token Token Token]], 113 | 'aster\\' => ['**\\***', %w[Aster]], 114 | 115 | 'under1' => ['_foo_ bar', %w[Under Token]], 116 | 'under1-' => ['_foo_bar', %w[Token Token Token Token]], 117 | 'under2' => ['__foo__ bar', %w[Under Token]], 118 | 'under3' => ['___foo___ bar', %w[Under Token]], 119 | 'under\\' => ['__\\___', %w[Under]], 120 | 121 | 'tilde1' => ['~foo~ bar', %w[Strikethrough Token]], 122 | 'tilde2' => ['~~foo~~ bar', %w[Strikethrough Token]], 123 | 'tilde3' => ['foo ~~~bar~~~', %w[Token Strikethrough]], # avoid fence 124 | 'tilde\\' => ['~~\\~~~', %w[Strikethrough]], 125 | 'tilden' => ['~foo~~~ bar', %w[Strikethrough Token]], 126 | 127 | 'link inline 1' => ['[foo](bar)', %w[A]], 128 | 'link inline 2' => ['[foo]()', %w[A]], 129 | 'link inline 3' => ['[foo]( (baz))', %w[A]], 130 | 'link inline 4' => ['[foo]( "baz")', %w[A]], 131 | 'link inline 5' => ["[foo]( 'baz')", %w[A]], 132 | 'link inline 6' => ["[foo](\n'baz')", %w[A]], 133 | 'link inline 7' => ["[foo](\n\n'baz')", %w[Token Token Token Token RawHTML]], 134 | 135 | 'link label 0' => ['[foo][bar]', %w[Token Token Token Token Token Token]], 136 | 'link label 1' => ["[foo][bar]\n\n[bar]: bar", %w[A]], 137 | 'link collapsed 0' => ["[foo][]: bar", %w[Token Token Token Token]], 138 | 'link collapsed 1' => ["[foo][]: bar\n\n[foo]: bar", %w[A Token]], 139 | 'link sc 0' => ['[foo]', %w[Token Token Token]], 140 | 'link sc 1' => ["[foo]\n\n[foo]: bar", %w[A]], 141 | 142 | 'auto1' => ['foo ', %w[Token Autolink]], 143 | 'auto2' => ['foo ', %w[Token Autolink]], 144 | 'auto3' => ['foo www.example.com', %w[Token Autolink]], 145 | 'auto4' => ['foo http://www.example.com', %w[Token Autolink]], 146 | 'auto5' => ['foo root@mput.dip.jp', %w[Token Autolink]], 147 | 148 | 'br1' => ["foo \nbar", %w[Token Newline Token]], 149 | 'br2' => ["foo\\\nbar", %w[Token Newline Token]], 150 | 'br3' => ["foo \n bar", %w[Token Newline Token]] 151 | ) 152 | 153 | @@parser = Optdown::Parser.new 154 | 155 | test '.new' do |(src, expected)| 156 | obj = @@parser.parse src 157 | actual = obj.children&.first&.children&.map{|i|/\w+\z/.match(i.class.to_s)[0]} 158 | if expected != actual then 159 | p obj.children 160 | end 161 | assert_equal expected, actual 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /test/integrated.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'test_helper' 27 | require 'optdown' 28 | require 'json' 29 | 30 | class TC_Integrated < Test::Unit::TestCase 31 | @@subject = Optdown::HTMLRenderer.new 32 | @@parser = Optdown::Parser.new 33 | path = File.expand_path 'spec.json', __dir__ 34 | File.open path, 'r:utf-8' do |fp| 35 | # For this use of create_additions option: 36 | # @see https://www.ruby-lang.org/en/news/2013/02/22/json-dos-cve-2013-0269/ 37 | json = JSON.parse fp.read, create_additions: false 38 | skip = [ 39 | # known failing ones due to GH extensions 40 | 579, 582, 583, 41 | ] 42 | hash = {} 43 | json.each do |h| 44 | next if skip.include? h['example'] 45 | key = sprintf 'example %d at spec.json:%d', h['example'], h['start_line'] 46 | hash[key] = h 47 | end 48 | data hash 49 | end 50 | 51 | test '#render' do |h| 52 | src = h['markdown'] 53 | expected = h['html'] 54 | dom = @@parser.parse src 55 | actual = @@subject.render dom 56 | if expected != actual 57 | require 'pp' 58 | pp dom 59 | end 60 | assert_equal expected, actual 61 | end 62 | 63 | sub_test_case 'GFM plugins' do 64 | data( 65 | 'table1' => [ <<~'begin', <<~'end' ], 66 | | foo | bar | 67 | | --- | --- | 68 | | baz | bim | 69 | begin 70 |
    71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
    foobar
    bazbim
    82 | end 83 | 84 | 'table2' => [ <<~'begin', <<~'end' ], 85 | | abc | defghi | 86 | :-: | -----------: 87 | bar | baz 88 | begin 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
    abcdefghi
    barbaz
    101 | end 102 | 103 | 'table3' => [ <<~'begin', <<~'end' ], 104 | | f\|oo | 105 | | ------ | 106 | | b `\|` az | 107 | | b **\|** im | 108 | begin 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 |
    f|oo
    b \| az
    b | im
    122 | end 123 | 124 | 'table4' => [ <<~'begin', <<~'end' ], 125 | | abc | def | 126 | | --- | --- | 127 | | bar | baz | 128 | > bar 129 | begin 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
    abcdef
    barbaz
    142 |
    143 |

    bar

    144 |
    145 | end 146 | 147 | 'table5' => [ <<~'begin', <<~'end' ], 148 | | abc | def | 149 | | --- | --- | 150 | | bar | baz | 151 | bar 152 | 153 | bar 154 | begin 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 |
    abcdef
    barbaz
    bar
    171 |

    bar

    172 | end 173 | 174 | 'table6' => [ <<~'begin', <<~'end' ], 175 | | abc | def | 176 | | --- | 177 | | bar | 178 | begin 179 |

    | abc | def | 180 | | --- | 181 | | bar |

    182 | end 183 | 184 | 'table7' => [ <<~'begin', <<~'end' ], 185 | | abc | def | 186 | | --- | --- | 187 | | bar | 188 | | bar | baz | boo | 189 | begin 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 |
    abcdef
    bar
    barbaz
    206 | end 207 | 208 | 'table8' => [ <<~'begin', <<~'end' ], 209 | | abc | def | 210 | | --- | --- | 211 | begin 212 | 213 | 214 | 215 | 216 | 217 | 218 |
    abcdef
    219 | end 220 | 221 | 'task1' => [ <<~'begin', <<~'end' ], 222 | - [ ] foo 223 | - [x] bar 224 | begin 225 |
      226 |
    • foo
    • 227 |
    • bar
    • 228 |
    229 | end 230 | 231 | 'task2' => [ <<~'begin', <<~'end' ], 232 | - [x] foo 233 | - [ ] bar 234 | - [x] baz 235 | - [ ] bim 236 | begin 237 |
      238 |
    • foo 239 |
        240 |
      • bar
      • 241 |
      • baz
      • 242 |
      243 |
    • 244 |
    • bim
    • 245 |
    246 | end 247 | 248 | 'del1' => [ <<~'begin', <<~'end' ], 249 | ~Hi~ Hello, world! 250 | begin 251 |

    Hi Hello, world!

    252 | end 253 | 254 | 'del2' => [ <<~'begin', <<~'end' ], 255 | This ~text~~~~ is ~~~~curious~. 256 | begin 257 |

    This text is curious.

    258 | end 259 | 260 | 'del3' => [ <<~'begin', <<~'end' ], 261 | This ~~has a 262 | 263 | new paragraph~~. 264 | begin 265 |

    This ~~has a

    266 |

    new paragraph~~.

    267 | end 268 | 269 | 'url1' => [ <<~'begin', <<~'end' ], 270 | www.commonmark.org 271 | begin 272 |

    www.commonmark.org

    273 | end 274 | 275 | 'url2' => [ <<~'begin', <<~'end' ], 276 | Visit www.commonmark.org/help for more information. 277 | begin 278 |

    Visit www.commonmark.org/help for more information.

    279 | end 280 | 281 | 'url3' => [ <<~'begin', <<~'end' ], 282 | Visit www.commonmark.org. 283 | 284 | Visit www.commonmark.org/a.b. 285 | begin 286 |

    Visit www.commonmark.org.

    287 |

    Visit www.commonmark.org/a.b.

    288 | end 289 | 290 | 'url4' => [ <<~'begin', <<~'end' ], 291 | www.google.com/search?q=Markup+(business) 292 | 293 | (www.google.com/search?q=Markup+(business)) 294 | begin 295 |

    www.google.com/search?q=Markup+(business)

    296 |

    (www.google.com/search?q=Markup+(business))

    297 | end 298 | 299 | 'url5' => [ <<~'begin', <<~'end' ], 300 | www.google.com/search?q=(business))+ok 301 | begin 302 |

    www.google.com/search?q=(business))+ok

    303 | end 304 | 305 | 'url6' => [ <<~'begin', <<~'end' ], 306 | www.google.com/search?q=commonmark&hl=en 307 | 308 | www.google.com/search?q=commonmark½ 309 | begin 310 |

    www.google.com/search?q=commonmark&hl=en

    311 |

    www.google.com/search?q=commonmark½

    312 | end 313 | 314 | 'url7' => [ <<~'begin', <<~'end' ], 315 | www.commonmark.org/hewww.commonmark.org/he<lp

    318 | end 319 | 320 | 'url8' => [ <<~'begin', <<~'end' ], 321 | http://commonmark.org 322 | 323 | (Visit https://encrypted.google.com/search?q=Markup+(business)) 324 | 325 | Anonymous FTP is available at ftp://foo.bar.baz. 326 | begin 327 |

    http://commonmark.org

    328 |

    (Visit https://encrypted.google.com/search?q=Markup+(business))

    329 |

    Anonymous FTP is available at ftp://foo.bar.baz.

    330 | end 331 | 332 | 'url9' => [ <<~'begin', <<~'end' ], 333 | foo@bar.baz 334 | begin 335 |

    foo@bar.baz

    336 | end 337 | 338 | 'url10' => [ <<~'begin', <<~'end' ], 339 | hello@mail+xyz.example isn't valid, but hello+xyz@mail.example is. 340 | begin 341 |

    hello@mail+xyz.example isn't valid, but hello+xyz@mail.example is.

    342 | end 343 | 344 | 'url11' => [ <<~'begin', <<~'end' ], 345 | a.b-c_d@a.b 346 | 347 | a.b-c_d@a.b. 348 | 349 | a.b-c_d@a.b- 350 | 351 | a.b-c_d@a.b_ 352 | begin 353 |

    a.b-c_d@a.b

    354 |

    a.b-c_d@a.b.

    355 |

    a.b-c_d@a.b-

    356 |

    a.b-c_d@a.b_

    357 | end 358 | ) 359 | 360 | test '#render' do |(src, expected)| 361 | dom = @@parser.parse src 362 | actual = @@subject.render dom 363 | if expected != actual 364 | require 'pp' 365 | pp dom 366 | end 367 | assert_equal expected, actual 368 | end 369 | end 370 | end 371 | -------------------------------------------------------------------------------- /test/matcher.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'test_helper' 27 | require 'optdown' 28 | 29 | class TC_Matcher < Test::Unit::TestCase 30 | setup do 31 | @subject = Optdown:: Matcher.new <<~'end' 32 | Lorem ipsum dolor sit amet, 33 | consectetur adipiscing elit, 34 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 35 | end 36 | end 37 | 38 | test ".new" do 39 | assert_instance_of Optdown::Matcher, @subject 40 | end 41 | 42 | sub_test_case ".join" do 43 | setup do 44 | ary = ["foo\n", "bar\n", "baz\n"].map {|i| Optdown::Matcher.new i} 45 | @subject = Optdown::Matcher.join ary 46 | end 47 | 48 | test ".join" do 49 | assert_instance_of Optdown::Matcher, @subject 50 | assert_equal "foo\nbar\nbaz\n", @subject.to_s 51 | end 52 | end 53 | 54 | sub_test_case "#compile" do 55 | data( 56 | "empty" => "", 57 | "blank" => " \r\n", 58 | "tab" => "\t", 59 | "long" => "\t" * 32768, 60 | "bq" => ">\tfoo", 61 | "li" => "-\tfoo", 62 | "2li" => " -\tx\t\n" * 2, 63 | "mix" => "\tfoo\r\nbar\r\nba\tz", 64 | ) 65 | 66 | test "#compile" do |str| 67 | subject = Optdown::Matcher.new str 68 | assert_equal str, subject.compile 69 | end 70 | end 71 | 72 | sub_test_case "#length" do 73 | data( 74 | 0 => [0, ''], 75 | 3 => [3, 'foo'], 76 | 4 => [4, "foo\t"], 77 | 5 => [5, "fo\to"], 78 | 6 => [6, "f\too"], 79 | 7 => [7, "\tfoo"], 80 | 8 => [8, "foo\nfoo\n"], 81 | 9 => [9, "foo\nfoo\t\n"], 82 | 10 => [10, "foo\nfo\to\n"], 83 | 11 => [11, "foo\nf\too\n"] 84 | ) 85 | 86 | test "#length" do |(n, str)| 87 | subject = Optdown::Matcher.new str 88 | if n != subject.length 89 | p [str, subject] 90 | end 91 | assert_equal n, subject.length 92 | end 93 | end 94 | 95 | sub_test_case "#empty?" do 96 | data( 97 | "yes" => [true, ""], 98 | "no" => [false, "foo"] 99 | ) 100 | 101 | test "#empty?" do |(expected, src)| 102 | subject = Optdown::Matcher.new src 103 | assert_equal expected, subject.empty? 104 | end 105 | end 106 | 107 | test "#match?" do 108 | assert_true @subject.match?(/\A/) 109 | assert_true @subject.match?(/\w+/) 110 | assert_true @subject.match?(/Lorem/) 111 | assert_false @subject.match?(/\G\W/) 112 | end 113 | 114 | test "#match" do 115 | assert_equal '', @subject.match(/\G\A/)[0] 116 | assert_equal 'Lorem', @subject.match(/\G\w+/)[0] 117 | assert_equal ' ', @subject.match(/\G\W+/)[0] 118 | assert_equal 'ipsum', @subject.match(/\G\w+/)[0] 119 | assert_equal ' ', @subject.match(/\G\W+/)[0] 120 | assert_equal 'dolor', @subject.match(/\G\w+/)[0] 121 | assert_equal ' ', @subject.match(/\G\W+/)[0] 122 | assert_equal 'sit', @subject.match(/\G\w+/)[0] 123 | assert_equal ' ', @subject.match(/\G\W+/)[0] 124 | assert_equal 'amet', @subject.match(/\G\w+/)[0] 125 | assert_equal ',', @subject.match(/\G\W/)[0] 126 | assert_equal '', @subject.match(/\G$/)[0] 127 | assert_equal "\n", @subject.match(/\G\W+/)[0] 128 | assert_equal '', @subject.match(/\G^/)[0] 129 | assert_equal nil, @subject.match(/\G\z/) 130 | end 131 | 132 | test "#eos?" do 133 | assert_false @subject.eos? 134 | @subject.gets 135 | assert_false @subject.eos? 136 | @subject.gets 137 | assert_false @subject.eos? 138 | @subject.gets 139 | assert_true @subject.eos? 140 | @subject.gets 141 | assert_true @subject.eos? 142 | end 143 | 144 | test "#read" do 145 | assert_equal 'Lorem ip', @subject.read(8).to_s 146 | assert_equal 'sum dolo', @subject.read(8).to_s 147 | assert_equal 'r sit am', @subject.read(8).to_s 148 | assert_equal "et,\n", @subject.gets.to_s 149 | end 150 | 151 | sub_test_case "#gets" do 152 | test "#gets" do 153 | assert_equal <<~'end', @subject.gets.to_s 154 | Lorem ipsum dolor sit amet, 155 | end 156 | assert_equal <<~'end', @subject.gets.to_s 157 | consectetur adipiscing elit, 158 | end 159 | assert_equal <<~'end', @subject.gets.to_s 160 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 161 | end 162 | assert_equal '', @subject.gets.to_s 163 | end 164 | end 165 | 166 | test "#advance" do 167 | assert_equal ['Lorem', ' '], @subject.advance(/(?<=m) /).map(&:to_s) 168 | end 169 | 170 | test "#get_anchor" do 171 | md1 = @subject.match(/\w+/) 172 | md2 = @subject.match(/(?\w+)/) 173 | assert_equal 'ipsum', @subject['ipsum'].to_s 174 | assert_equal 'Lorem', @subject[md1, 0].to_s 175 | assert_equal 'ipsum', @subject[md2, 0].to_s 176 | end 177 | 178 | sub_test_case "#split" do 179 | data( 180 | "empty1" => ['', [/./], []], 181 | "empty2" => ['', [/$/], []], 182 | "empty3" => ['', [',', -1], []], 183 | "sp" => [" a \t b \n c", [/\s+/], ["", "a", "b", "c"]], 184 | "//" => ["hi there", [//], %w[h i \ t h e r e]], 185 | "()" => ["1:2:3", [/(:)()()/, 2], ["1", ":", "", "", "2:3"]], 186 | "limit0" => ["1,2,,3,4,,", [','], ["1", "2", "", "3", "4"]], 187 | "limit+" => ["1,2,,3,4,,", [',', 4], ["1", "2", "", "3,4,,"]], 188 | "limit-" => ["1,2,,3,4,,", [',', -4], ["1", "2", "", "3", "4", "", ""]] 189 | ) 190 | 191 | test '#split' do |(src, argv, expected)| 192 | subject = Optdown::Matcher.new src 193 | actual = subject.split(*argv) 194 | assert_instance_of Array, actual 195 | actual.each {|i| assert_instance_of Optdown::Matcher, i } 196 | assert_equal expected, actual.map(&:to_s) 197 | end 198 | end 199 | 200 | class TestRefinemenrs < TC_Matcher 201 | using Optdown::Matcher::Refinements 202 | 203 | test '#===' do 204 | loop do 205 | case @subject 206 | when /\n/ then break 207 | when /(L.+?)\s+/ then assert_equal 'Lorem', @subject.last_match[1] 208 | when /(i.+?)\s+/ then assert_equal 'ipsum', @subject.last_match[1] 209 | when /(\w+),/ then assert_equal 'amet', @subject.last_match[1] 210 | when /\w+\s+/ then next # skip 211 | end 212 | end 213 | end 214 | 215 | test 'other class' do 216 | case "foo" 217 | when Time then flunk 218 | when /bar/ then flunk 219 | when /foo/ then refute nil 220 | else flunk 221 | end 222 | end 223 | end 224 | end 225 | -------------------------------------------------------------------------------- /test/pathological.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'test_helper' 27 | require 'optdown' 28 | require 'json' 29 | 30 | # @see https://github.com/github/cmark/blob/master/test/pathological_tests.py 31 | class TC_Pathological < Test::Unit::TestCase 32 | @@subject = Optdown::HTMLRenderer.new 33 | @@parser = Optdown::Parser.new 34 | data( 35 | 'nested strong emph' => [ 36 | ("*a **a " * 1024) + "b" + (" a** a*" * 1024), 37 | %r{(a a ){1024}b( a a){1024}} 38 | ], 39 | 'many emph closers with no openers' => [ 40 | ("a_ " * 1024), 41 | /(a_ ){1023}a_/ 42 | ], 43 | 'many emph openers with no closers' => [ 44 | ("_a " * 1024), 45 | /(_a ){1023}_a/ 46 | ], 47 | 'many link closers with no openers' => [ 48 | ("a]" * 1024), 49 | /(a\]){1024}/ 50 | ], 51 | 'many link openers with no closers' => [ 52 | ("[a" * 1024), 53 | /(\[a){1024}/ 54 | ], 55 | 'mismatched openers and closers' => [ 56 | ("*a_ " * 1024), 57 | /(\*a_ ){1023}\*a_/ 58 | ], 59 | 'openers and closers multiple of 3' => [ 60 | ("a**b" + ("c* " * 1024)), 61 | /a\*\*b(c\* ){1023}c\*/ 62 | ], 63 | 'link openers and emph closers' => [ 64 | ("[ a_" * 1024), 65 | /(\[ a_){1024}/ 66 | ], 67 | 'hard link/emph case' => [ 68 | "**x [a*b**c*](d)", 69 | %r{\*\*x ab\*\*c} 70 | ], 71 | 'nested brackets' => [ 72 | ("[" * 1024) + "a" + ("]" * 1024), 73 | /\[{1024}a\]{1024}/ 74 | ], 75 | 'nested block quotes' => [ 76 | (("> " * 1024) + "a"), 77 | /(
    \n){1024}/ 78 | ], 79 | 'U+0000 in input' => [ 80 | "abc\u0000de\u0000", 81 | /abc\ufffd?de\ufffd?/ 82 | ], 83 | 'backticks' => [ 84 | (1..1024).map{|x| "e" + "`" * x }.join(""), 85 | %r{^

    [e`]*

    \n$} 86 | ], 87 | ) 88 | 89 | test '#render' do |(src, expected)| 90 | dom = @@parser.parse src 91 | actual = @@subject.render dom 92 | assert_match expected, actual 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test/renderer.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require_relative 'test_helper' 27 | require 'optdown' 28 | 29 | class TC_Renderer < Test::Unit::TestCase 30 | @@subject = Optdown::Renderer.new 31 | @@parser = Optdown::Parser.new 32 | data( 33 | 'empty' => '', 34 | 'p cdata' => 'foo', 35 | 'p br' => "foo\\\nbar", 36 | 'p &' => 'foo & bar', 37 | 'p \\' => 'foo \\& bar', 38 | 'p `' => '`foo`', 39 | 'p <' => 'foo ', # "foo" is to avoid blockhtml 40 | 'p url' => 'foo ', 41 | 'p img' => '![foo](http://www.example.com)', 42 | 'p link' => '[foo](http://www.example.com)', 43 | 'p *' => '*foo**', 44 | 'p _' => '_foo__', 45 | 'p ~' => '~foo~~', 46 | 'p *_*' => '*_*foo*_*', 47 | 48 | '--' => '- - - -', 49 | '[]:' => '[foo]: bar', 50 | '>' => '> foo', 51 | '-' => '- foo', 52 | '*' => '* foo', 53 | '<' => 'foo', 54 | '```' => "```foo\nbar\n```", 55 | "\t" => ' foo', 56 | '#' => '# foo', 57 | '==' => "foo\n===", 58 | '|' => "|foo|bar|\n|---|---|\n|foo|bar|\n", 59 | 'p' => 'foo' 60 | ) 61 | 62 | test '#render' do |src| 63 | dom = @@parser.parse src 64 | actual = @@subject.render dom 65 | assert_equal '', actual 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require 'test-unit' 27 | require 'simplecov' 28 | require 'stackprof' 29 | 30 | SimpleCov.start do 31 | add_filter 'test/' 32 | add_filter 'vendor/' 33 | end 34 | 35 | END { StackProf.results } 36 | class Test::Unit::TestCase 37 | prepend Module.new { 38 | def setup 39 | super 40 | StackProf.start raw: true, out: '/tmp/stackprof.dump' 41 | end 42 | 43 | def teardown 44 | super 45 | StackProf.stop 46 | end 47 | } 48 | end 49 | 50 | # Optdown::EXPR has literally hundreds of named captures. By matching against 51 | # it a MatchData will contain all of them. This is almost impossible to inspect 52 | # at one sight. However the captures tends to be nil; which means most of them 53 | # do not make sense at once. We can cut nil captures from the inspect output 54 | # for better readability. 55 | class MatchData 56 | prepend Module.new { 57 | def inspect 58 | str = super 59 | names.each do |nam| 60 | str.gsub! %r/\s+#{Regexp.quote(nam)}:nil\b/, '' 61 | end 62 | return str 63 | end 64 | } 65 | end 66 | 67 | # So do Regexps. 68 | class Regexp 69 | prepend Module.new { 70 | def inspect 71 | str = super 72 | str.gsub! %r/\n\(\?\.+\)\{0\}\n/m, '...' 73 | return str 74 | end 75 | } 76 | end 77 | -------------------------------------------------------------------------------- /test/xprintf.rb: -------------------------------------------------------------------------------- 1 | #! /your/favourite/path/to/ruby 2 | # -*- mode: ruby; coding: utf-8; indent-tabs-mode: nil; ruby-indent-level: 2 -*- 3 | # -*- frozen_string_literal: true -*- 4 | # -*- warn_indent: true -*- 5 | 6 | # Copyright (c) 2017 Urabe, Shyouhei 7 | # 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the "Software"), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be 16 | # included in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | # SOFTWARE. 25 | 26 | require 'securerandom' 27 | require_relative 'test_helper' 28 | require 'optdown' 29 | 30 | class TC_XPrintf < Test::Unit::TestCase 31 | using Optdown::XPrintf 32 | 33 | sub_test_case "#lprintf" do 34 | class Double 35 | include Test::Unit::Assertions 36 | 37 | def initialize str 38 | @str = " #{str}" 39 | end 40 | 41 | def debug str 42 | assert_equal @str, str 43 | end 44 | end 45 | 46 | setup do 47 | @str = SecureRandom.uuid 48 | @subject = Double.new @str 49 | end 50 | 51 | test "#lprintf" do 52 | lprintf @subject, :debug, " %s", @str 53 | end 54 | end 55 | 56 | test "#rprintf" do 57 | str = SecureRandom.uuid 58 | assert_raise_message " #{str}" do 59 | rprintf RuntimeError, " %s", str 60 | end 61 | end 62 | end 63 | --------------------------------------------------------------------------------