mato 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +139 -0
- data/.travis.yml +9 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +104 -0
- data/Rakefile +13 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/example/hello.rb +53 -0
- data/example/toc.rb +20 -0
- data/lib/mato.rb +25 -0
- data/lib/mato/anchor_builder.rb +50 -0
- data/lib/mato/concerns/html_node_checkable.rb +22 -0
- data/lib/mato/config.rb +108 -0
- data/lib/mato/document.rb +65 -0
- data/lib/mato/html_filters/mention_link.rb +60 -0
- data/lib/mato/html_filters/sanitization.rb +21 -0
- data/lib/mato/html_filters/section_anchor.rb +25 -0
- data/lib/mato/html_filters/syntax_highlight.rb +91 -0
- data/lib/mato/html_filters/task_list.rb +65 -0
- data/lib/mato/html_filters/token_link.rb +33 -0
- data/lib/mato/processor.rb +60 -0
- data/lib/mato/renderers/html_renderer.rb +13 -0
- data/lib/mato/renderers/html_toc_renderer.rb +66 -0
- data/lib/mato/version.rb +5 -0
- data/mato.gemspec +33 -0
- metadata +127 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: cc964ed48f7d5e7d1d069e32b2ea3701118a6765
|
4
|
+
data.tar.gz: 310ca6e0226f7ebaf1da5653d17a926580c8c476
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b19a063ae37c6885ae08a1e1d961a3519f36dc19fd42370a09f95a143f5a1b3f84d5d17adb17f48e8247db268d8750f0c1759a54193e6f7c93e73696a08b45ba
|
7
|
+
data.tar.gz: 1b6d1995d00dba709bd52d8138530ce8fe66699b5006060dc10277333569a23582fec03ab0f86a47380e3eee5328fbc6739b841152d5a5a27219d3f842ffca80
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
# See also https://github.com/onk/onkcop/blob/master/config/rubocop.yml
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
TargetRubyVersion: 2.3
|
5
|
+
DisplayCopNames: true
|
6
|
+
Exclude:
|
7
|
+
- bin/**/*
|
8
|
+
|
9
|
+
Lint/UnusedBlockArgument:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Lint/AmbiguousBlockAssociation:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Lint/ScriptPermission:
|
16
|
+
Enabled: false
|
17
|
+
|
18
|
+
Layout/MultilineMethodCallIndentation:
|
19
|
+
Enabled: false
|
20
|
+
|
21
|
+
Layout/EmptyLinesAroundClassBody:
|
22
|
+
Enabled: false
|
23
|
+
|
24
|
+
Metrics/AbcSize:
|
25
|
+
Max: 32
|
26
|
+
|
27
|
+
Metrics/PerceivedComplexity:
|
28
|
+
Max: 10
|
29
|
+
|
30
|
+
Metrics/CyclomaticComplexity:
|
31
|
+
Max: 10
|
32
|
+
|
33
|
+
Metrics/ClassLength:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Metrics/BlockLength:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Metrics/LineLength:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
Metrics/MethodLength:
|
43
|
+
Max: 45
|
44
|
+
|
45
|
+
Metrics/ModuleLength:
|
46
|
+
Enabled: false
|
47
|
+
|
48
|
+
Metrics/ParameterLists:
|
49
|
+
Enabled: false
|
50
|
+
|
51
|
+
Style/BlockDelimiters:
|
52
|
+
Enabled: false
|
53
|
+
|
54
|
+
Style/CommentAnnotation:
|
55
|
+
Enabled: false
|
56
|
+
|
57
|
+
Style/Documentation:
|
58
|
+
Enabled: false
|
59
|
+
|
60
|
+
Style/FileName:
|
61
|
+
Enabled: false
|
62
|
+
|
63
|
+
Style/GuardClause:
|
64
|
+
Enabled: false
|
65
|
+
|
66
|
+
Style/IfInsideElse:
|
67
|
+
Enabled: false
|
68
|
+
|
69
|
+
Style/IfUnlessModifier:
|
70
|
+
Enabled: false
|
71
|
+
|
72
|
+
Style/Lambda:
|
73
|
+
Enabled: false
|
74
|
+
|
75
|
+
Style/MutableConstant:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
Style/NumericPredicate:
|
79
|
+
Enabled: false
|
80
|
+
|
81
|
+
Style/PercentLiteralDelimiters:
|
82
|
+
Enabled: false
|
83
|
+
|
84
|
+
Style/SignalException:
|
85
|
+
Enabled: false
|
86
|
+
|
87
|
+
Style/StringLiterals:
|
88
|
+
Enabled: false
|
89
|
+
|
90
|
+
Style/TrailingCommaInLiteral:
|
91
|
+
EnforcedStyleForMultiline: comma
|
92
|
+
|
93
|
+
Style/TrailingCommaInArguments:
|
94
|
+
EnforcedStyleForMultiline: comma
|
95
|
+
|
96
|
+
Style/WordArray:
|
97
|
+
Enabled: false
|
98
|
+
|
99
|
+
Style/NumericLiterals:
|
100
|
+
Enabled: false
|
101
|
+
|
102
|
+
Style/MultilineBlockChain:
|
103
|
+
Enabled: false
|
104
|
+
|
105
|
+
Style/ConditionalAssignment:
|
106
|
+
Enabled: false
|
107
|
+
|
108
|
+
Style/RescueModifier:
|
109
|
+
Enabled: false
|
110
|
+
|
111
|
+
Style/AsciiComments:
|
112
|
+
Enabled: false
|
113
|
+
|
114
|
+
Style/EmptyMethod:
|
115
|
+
EnforcedStyle: expanded
|
116
|
+
|
117
|
+
Style/VariableNumber:
|
118
|
+
Enabled: false
|
119
|
+
|
120
|
+
Style/PredicateName:
|
121
|
+
Enabled: false
|
122
|
+
|
123
|
+
Style/AccessorMethodName:
|
124
|
+
Enabled: false
|
125
|
+
|
126
|
+
Style/YodaCondition:
|
127
|
+
Enabled: false
|
128
|
+
|
129
|
+
Style/FormatStringToken:
|
130
|
+
Enabled: false
|
131
|
+
|
132
|
+
Style/MultipleComparison:
|
133
|
+
Enabled: false
|
134
|
+
|
135
|
+
Style/StructInheritance:
|
136
|
+
Enabled: false
|
137
|
+
|
138
|
+
Performance/RedundantBlockCall:
|
139
|
+
Enabled: false
|
data/.travis.yml
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
sudo: false
|
2
|
+
language: ruby
|
3
|
+
rvm:
|
4
|
+
- 2.3
|
5
|
+
- 2.4
|
6
|
+
before_install: gem install bundler
|
7
|
+
notifications:
|
8
|
+
slack:
|
9
|
+
secure: W/CuFdMffK19NqZ7hX5Cx1ekLwY5WYtyi91CgYJ5jUYZq6hY7sZAHItfvbB7kwVc5vGzDVzBp1L4+iR44OX1+3sbJQ8OHUfWNksRME5NAHeHKUnkeoMAToTy6gv8GTSDOkBoMxgtZQQwaFFxOVQF4UknLCKvwz1JRBf3zjEG7zyKlBQuYSkMQQm4xA/qmgb6nCAqSzMJxN4Gl57LKphCScKO5ZK6hUuaJv4H6y3NfDbKAEwidEUaMqnU1TtLTkqhSqVve/JMlBoynYFIezBu7R35cyHFdE8l1OaYFvIJo6nsyRZr9RVhp20wlLyL2dsM4IUBTfLf1dCw94U8VE3cCkYEh7MxQv/NAkDpkBsI9E4GGOfmuXRxcoQGoKRI9GHleg8hm/E39134JhVKNL2L5RGg/8ULNPPrQjMf0GWSxhtqBXN7vzuP4ZfAMzEDDGFehCmmzrE5qIdcRwXCI3+QRX7XAWL+Ql+xljoxZjl2f7wHSx3COChkol0V8CEGAyOTJc3qnci5jPs5mSByqv3RyO0meYiJCJ1Ce2FTjCOHs1G9D58xfra6AVA8FmOk43+iN8QZyUIqd+MsenL2k89LqMijoC208xI6WI88C9tMHDA8tmi8yJt6PA00emC+Gzy9xPCYsZ+dZhymOPDcAbBlez+j3weEEDROF0Jlewt4X8Q=
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 FUJI Goro (gfx)
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# Mato - Markdown Toolkit based on CommonMark [![Build Status](https://travis-ci.org/bitjourney/mato.svg?branch=master)](https://travis-ci.org/bitjourney/mato)
|
2
|
+
|
3
|
+
**Mato**, standing for **Ma**rkdown **To**oolkit, is an extensible markdown-based content processing toolkit, inspired by [HTML::Pipeline](https://github.com/jch/html-pipeline).
|
4
|
+
|
5
|
+
|
6
|
+
This gem is built on [commonmarker](https://github.com/gjtorikian/commonmarker), a [CommonMark](https://github.com/jgm/CommonMark) implementation,
|
7
|
+
which can parse markdown documents into AST.
|
8
|
+
|
9
|
+
## Synopsis
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
require 'mato'
|
13
|
+
|
14
|
+
# define a markdown-to-html processor:
|
15
|
+
mato = Mato.define do |config|
|
16
|
+
# append pre-defined HTML filters:
|
17
|
+
config.append_html_filter(Mato::HtmlFilters::SyntaxHighlight.new)
|
18
|
+
config.append_html_filter(Mato::HtmlFilters::TaskList.new)
|
19
|
+
config.append_html_filter(Mato::HtmlFilters::SectionAnchor.new)
|
20
|
+
|
21
|
+
# append MentionLink, a customizable HTML filter:
|
22
|
+
config.append_html_filter(Mato::HtmlFilters::MentionLink.new do |mention_candidate_map|
|
23
|
+
candidate_accounts = mention_candidate_map.keys.map { |name| name.gsub(/^\@/, '') }
|
24
|
+
User.where(account: candidate_accounts).each do |user|
|
25
|
+
mention = "@#{user.account}"
|
26
|
+
mention_candidate_map[mention].each do |node|
|
27
|
+
node.replace("<a href='https://twitter.com/#{mention}' class='mention'>#{mention}</a>")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Prosesses markdown into Mato::Document:
|
34
|
+
doc = mato.process(markdown_content)
|
35
|
+
|
36
|
+
# Renders doc as HTML:
|
37
|
+
html = doc.render_html
|
38
|
+
|
39
|
+
# Renders doc as HTML Table of Contents:
|
40
|
+
html_toc = doc.render_html_toc
|
41
|
+
|
42
|
+
# Extracts elements (e.g. mentions) with CSS selector:
|
43
|
+
doc.css('a').each do |element|
|
44
|
+
# do something with element: Nokogiri::XML::Element
|
45
|
+
end
|
46
|
+
|
47
|
+
# Extracts nodes (e.g. mentions) with XPath selector:
|
48
|
+
doc.xpath('./text()').each do |node|
|
49
|
+
# do something with node: Nokogiri::XML::Text
|
50
|
+
end
|
51
|
+
|
52
|
+
# Mato::Document can be cached with Rails.cache (i.e. Marshal.dump ready)
|
53
|
+
Rails.fetch(digest(markdown_content)) do
|
54
|
+
mato.process(markdown_content)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Applies extra filters and returns a new Mato::Document.
|
58
|
+
# Because Mato::Document is serializable, you can cache the base doc and then apply extra filters on demaond.
|
59
|
+
new_doc = doc.apply_html_filters(
|
60
|
+
-> (fragment) { modify_fragment!(fragment) },
|
61
|
+
SomeHtmlFilter.new, # anything that has #call(node) method
|
62
|
+
)
|
63
|
+
```
|
64
|
+
|
65
|
+
## Installation
|
66
|
+
|
67
|
+
Add this line to your application's Gemfile:
|
68
|
+
|
69
|
+
```ruby:Gemfile
|
70
|
+
gem 'mato'
|
71
|
+
```
|
72
|
+
|
73
|
+
And then execute:
|
74
|
+
|
75
|
+
$ bundle
|
76
|
+
|
77
|
+
Or install it yourself as:
|
78
|
+
|
79
|
+
$ gem install mato
|
80
|
+
|
81
|
+
## Usage
|
82
|
+
|
83
|
+
TODO: Write usage instructions here
|
84
|
+
|
85
|
+
## Optional Dependencies
|
86
|
+
|
87
|
+
* [rouge](https://github.com/jneen/rouge) (>= 2.0) to use `SyntaxHighlight`
|
88
|
+
* [sanitize](https://github.com/rgrove/sanitize) (>= 4.0) to use `Sanitization`
|
89
|
+
|
90
|
+
## Development
|
91
|
+
|
92
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
93
|
+
|
94
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
95
|
+
|
96
|
+
## Contributing
|
97
|
+
|
98
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/bitjourney/mato.
|
99
|
+
|
100
|
+
|
101
|
+
## License
|
102
|
+
|
103
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
104
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.warning = false
|
10
|
+
t.test_files = FileList['test/**/*_test.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
task default: :test
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "mato"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/example/hello.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'mato'
|
5
|
+
|
6
|
+
# User class that mocks ActiveRecord's
|
7
|
+
User = Struct.new(:account)
|
8
|
+
class User
|
9
|
+
VALID_ACCOUNTS = %w(mato)
|
10
|
+
|
11
|
+
# @return [Enumerable<User>]
|
12
|
+
def self.where(account:)
|
13
|
+
(account & VALID_ACCOUNTS).map do |name|
|
14
|
+
User.new(name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
mato = Mato.define do |config|
|
20
|
+
config.append_text_filter ->(text, _context) {
|
21
|
+
# weave text
|
22
|
+
}
|
23
|
+
|
24
|
+
config.append_markdown_filter ->(doc, _context) {
|
25
|
+
# weave doc
|
26
|
+
}
|
27
|
+
|
28
|
+
config.append_html_filter ->(doc, _context) {
|
29
|
+
# weave doc
|
30
|
+
}
|
31
|
+
|
32
|
+
config.append_html_filter(Mato::HtmlFilters::TaskList.new)
|
33
|
+
config.append_html_filter(Mato::HtmlFilters::MentionLink.new do |mention_candidate_map|
|
34
|
+
candidate_accounts = mention_candidate_map.keys.map { |name| name.gsub(/^\@/, '') }
|
35
|
+
User.where(account: candidate_accounts).each do |user|
|
36
|
+
mention = "@#{user.account}"
|
37
|
+
mention_candidate_map[mention].each do |node|
|
38
|
+
node.replace("<a href='https://twitter.com/#{mention}' class='mention'>#{mention}</a>")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end)
|
42
|
+
end
|
43
|
+
|
44
|
+
puts mato.process(<<~'MARKDOWN').render_html
|
45
|
+
# Hello, @mato!
|
46
|
+
|
47
|
+
@this_is_invalid_mention
|
48
|
+
|
49
|
+
https://twitter.com/@kibe_la
|
50
|
+
|
51
|
+
* [ ] a
|
52
|
+
* [x] b
|
53
|
+
MARKDOWN
|
data/example/toc.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'mato'
|
5
|
+
|
6
|
+
mato = Mato.define do |config|
|
7
|
+
config.append_html_filter(Mato::HtmlFilters::SectionAnchor.new)
|
8
|
+
end
|
9
|
+
|
10
|
+
puts mato.process(<<~'MARKDOWN').render_html_toc
|
11
|
+
# **First** Level Title
|
12
|
+
|
13
|
+
## **Second** Level Title
|
14
|
+
|
15
|
+
### **Third** Level Title
|
16
|
+
|
17
|
+
##### **Fifth** Level Title
|
18
|
+
|
19
|
+
# **First** Level Title Again
|
20
|
+
MARKDOWN
|
data/lib/mato.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# core classes
|
4
|
+
require_relative "./mato/version"
|
5
|
+
require_relative "./mato/config"
|
6
|
+
require_relative "./mato/processor"
|
7
|
+
|
8
|
+
# filter classes
|
9
|
+
require_relative "./mato/html_filters/token_link"
|
10
|
+
require_relative "./mato/html_filters/mention_link"
|
11
|
+
require_relative "./mato/html_filters/syntax_highlight"
|
12
|
+
require_relative "./mato/html_filters/task_list"
|
13
|
+
require_relative "./mato/html_filters/section_anchor"
|
14
|
+
require_relative "./mato/html_filters/sanitization"
|
15
|
+
|
16
|
+
module Mato
|
17
|
+
# @param [Proc] block
|
18
|
+
# @yieldparam [Mato::Config] config
|
19
|
+
# @return [Mato::Processor]
|
20
|
+
def self.define(&block)
|
21
|
+
config = Mato::Config.new
|
22
|
+
config.configure(&block)
|
23
|
+
Mato::Processor.new(config)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module Mato
|
6
|
+
class AnchorBuilder
|
7
|
+
|
8
|
+
# assumes use of font-awesome
|
9
|
+
# specify it as "<span aria-hidden=\"true\" class=\"octicon octicon-link\"></span>" if you use octicon
|
10
|
+
DEFAULT_ANCHOR_ICON_ELEMENT = '<i class="fa fa-link"></i>'
|
11
|
+
|
12
|
+
CSS_CLASS_NAME = "anchor"
|
13
|
+
|
14
|
+
attr_reader :anchor_icon_element
|
15
|
+
attr_reader :context
|
16
|
+
|
17
|
+
def initialize(anchor_icon_element = DEFAULT_ANCHOR_ICON_ELEMENT)
|
18
|
+
@anchor_icon_element = anchor_icon_element
|
19
|
+
@id_map = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Nokogiri::XML::Node] hx
|
23
|
+
def make_anchor_element(hx)
|
24
|
+
id = make_anchor_id(hx)
|
25
|
+
%{<a id="#{id}" href="##{id}" aria-hidden="true" class="#{CSS_CLASS_NAME}">#{anchor_icon_element}</a>}
|
26
|
+
end
|
27
|
+
|
28
|
+
def make_anchor_id(hx)
|
29
|
+
prefix = make_anchor_id_prefix(hx.inner_text)
|
30
|
+
"#{prefix}#{make_anchor_id_suffix(prefix)}"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def make_anchor_id_suffix(text)
|
36
|
+
@id_map[text] ||= -1
|
37
|
+
unique_id = @id_map[text] += 1
|
38
|
+
|
39
|
+
if unique_id > 0
|
40
|
+
"-#{unique_id}"
|
41
|
+
else
|
42
|
+
""
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def make_anchor_id_prefix(text)
|
47
|
+
ERB::Util.url_encode(text.downcase.gsub(/[^\p{Word}\- ]/u, "").tr(" ", "-"))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mato
|
4
|
+
module Concerns
|
5
|
+
module HtmlNodeCheckable
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# @param [Nokogiri::XML::Node] node
|
9
|
+
# @param [Array<String>] tags - set of tags
|
10
|
+
# @return [Boolean] true if the node has the specified tags as a parent
|
11
|
+
def has_ancestor?(node, *tags)
|
12
|
+
current = node
|
13
|
+
while (current = current.parent)
|
14
|
+
if tags.include?(current.name)
|
15
|
+
return true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/mato/config.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative('./document')
|
4
|
+
|
5
|
+
require 'nokogiri'
|
6
|
+
|
7
|
+
module Mato
|
8
|
+
class Config
|
9
|
+
# https://github.com/gjtorikian/commonmarker#parse-options
|
10
|
+
DEFAULT_MARKDOWN_PARSE_OPTIONS = %i[
|
11
|
+
DEFAULT
|
12
|
+
VALIDATE_UTF8
|
13
|
+
]
|
14
|
+
|
15
|
+
# https://github.com/gjtorikian/commonmarker#render-options
|
16
|
+
DEFAULT_MARKDOWN_RENDER_OPTIONS = [
|
17
|
+
:DEFAULT,
|
18
|
+
:HARDBREAKS, # convert "\n" as <br/>
|
19
|
+
# :SOURCEPOS, // TODO: enable it after assertions are supported
|
20
|
+
]
|
21
|
+
|
22
|
+
# https://github.com/github/cmark/tree/master/extensions
|
23
|
+
DEFAULT_MARKDOWN_EXTENSIONS = %i[
|
24
|
+
table
|
25
|
+
strikethrough
|
26
|
+
autolink
|
27
|
+
tagfilter
|
28
|
+
]
|
29
|
+
|
30
|
+
# @return [Array<Proc>]
|
31
|
+
attr_accessor :text_filters
|
32
|
+
|
33
|
+
# @return [Array<Proc>]
|
34
|
+
attr_accessor :markdown_filters
|
35
|
+
|
36
|
+
# @return [Array<Proc>]
|
37
|
+
attr_accessor :html_filters
|
38
|
+
|
39
|
+
# @return [Class<CommonMarker]
|
40
|
+
attr_accessor :markdown_parser
|
41
|
+
|
42
|
+
# @return [Cass<Nokogiri::HTML::DocumentFragment>]
|
43
|
+
attr_accessor :html_parser
|
44
|
+
|
45
|
+
# @return [Class<Mato::Document>]
|
46
|
+
attr_accessor :document_factory
|
47
|
+
|
48
|
+
# @return [Array<Symbol>] CommonMarker's parse extensions
|
49
|
+
attr_accessor :markdown_extensions
|
50
|
+
|
51
|
+
# @return [Array<Symbol>] CommonMarker's pars options
|
52
|
+
attr_accessor :markdown_parse_options
|
53
|
+
|
54
|
+
# @return [Array<Symbol>] CommonMarker's HTML rendering options
|
55
|
+
attr_accessor :markdown_render_options
|
56
|
+
|
57
|
+
def initialize
|
58
|
+
@text_filters = []
|
59
|
+
@markdown_filters = []
|
60
|
+
@html_filters = []
|
61
|
+
|
62
|
+
@markdown_parser = CommonMarker
|
63
|
+
@html_parser = Nokogiri::HTML::DocumentFragment
|
64
|
+
|
65
|
+
@document_factory = Document
|
66
|
+
|
67
|
+
@markdown_extensions = DEFAULT_MARKDOWN_EXTENSIONS
|
68
|
+
@markdown_parse_options = DEFAULT_MARKDOWN_PARSE_OPTIONS
|
69
|
+
@markdown_render_options = DEFAULT_MARKDOWN_RENDER_OPTIONS
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param [Proc] block
|
73
|
+
# @yieldparam [Mato::Config] config
|
74
|
+
def configure(&block)
|
75
|
+
block.call(self)
|
76
|
+
end
|
77
|
+
|
78
|
+
def append_text_filter(text_filter)
|
79
|
+
raise "text_filter must respond to call()" unless text_filter.respond_to?(:call)
|
80
|
+
text_filters.push(text_filter)
|
81
|
+
end
|
82
|
+
|
83
|
+
def prepend_text_filter(text_filter)
|
84
|
+
raise "text_filter must respond to call()" unless text_filter.respond_to?(:call)
|
85
|
+
text_filters.unshift(text_filter)
|
86
|
+
end
|
87
|
+
|
88
|
+
def append_markdown_filter(markdown_filter)
|
89
|
+
raise "markdown_filter must respond to call()" unless markdown_filter.respond_to?(:call)
|
90
|
+
markdown_filters.push(markdown_filter)
|
91
|
+
end
|
92
|
+
|
93
|
+
def prepend_markdown_filter(markdown_filter)
|
94
|
+
raise "markdown_filter must respond to call()" unless markdown_filter.respond_to?(:call)
|
95
|
+
markdown_filters.unshift(markdown_filter)
|
96
|
+
end
|
97
|
+
|
98
|
+
def append_html_filter(html_filter)
|
99
|
+
raise "html_filter must respond to call()" unless html_filter.respond_to?(:call)
|
100
|
+
html_filters.push(html_filter)
|
101
|
+
end
|
102
|
+
|
103
|
+
def prepend_html_filter(html_filter)
|
104
|
+
raise "html_filter must respond to call()" unless html_filter.respond_to?(:call)
|
105
|
+
html_filters.unshift(html_filter)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative './renderers/html_renderer'
|
5
|
+
require_relative './renderers/html_toc_renderer'
|
6
|
+
|
7
|
+
# Intermediate document class, which instance is *serializable*.
|
8
|
+
module Mato
|
9
|
+
class Document
|
10
|
+
# @return [Nokogiri::HTML::DocumentFragment]
|
11
|
+
attr_reader :fragment
|
12
|
+
|
13
|
+
def self.empty
|
14
|
+
new(Nokogiri::HTML.fragment(''))
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Nokogiri::HTML::DocumentFragment] fragment
|
18
|
+
def initialize(fragment)
|
19
|
+
@fragment = fragment
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Nokogiri::HTML::DocumentFragment] A copy of fragment that are modified by html_filters
|
23
|
+
def apply_html_filters(*html_filters)
|
24
|
+
new_fragment = fragment.dup
|
25
|
+
html_filters.each do |html_filter|
|
26
|
+
html_filter.call(new_fragment)
|
27
|
+
end
|
28
|
+
self.class.new(new_fragment.freeze)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param [String] selector
|
32
|
+
# @return [Nokogiri::XML::NodeSet]
|
33
|
+
def css(selector)
|
34
|
+
fragment.css(selector)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param [String] query
|
38
|
+
# @return [Nokogiri::XML::NodeSet]
|
39
|
+
def xpath(query)
|
40
|
+
fragment.xpath(query)
|
41
|
+
end
|
42
|
+
|
43
|
+
def render(renderer)
|
44
|
+
renderer.call(fragment)
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_html
|
48
|
+
render(Mato::Renderers::HtmlRenderer.new)
|
49
|
+
end
|
50
|
+
|
51
|
+
def render_html_toc
|
52
|
+
render(Mato::Renderers::HtmlTocRenderer.new)
|
53
|
+
end
|
54
|
+
|
55
|
+
def marshal_dump
|
56
|
+
{
|
57
|
+
fragment: fragment.to_s,
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def marshal_load(data)
|
62
|
+
initialize(Nokogiri::HTML.fragment(data[:fragment]).freeze)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../concerns/html_node_checkable'
|
4
|
+
|
5
|
+
module Mato
|
6
|
+
module HtmlFilters
|
7
|
+
class MentionLink
|
8
|
+
include Concerns::HtmlNodeCheckable
|
9
|
+
|
10
|
+
# @return [Regexp]
|
11
|
+
attr_reader :pattern
|
12
|
+
|
13
|
+
# @return [Proc]
|
14
|
+
attr_reader :link_builder
|
15
|
+
|
16
|
+
MENTION_PATTERN = /\@[a-zA-Z0-9_]+\b/ # e.g. @foo
|
17
|
+
|
18
|
+
# @param [Regexp] pattern
|
19
|
+
# @param [Proc] link_builder A block that takes Hash<String, Array<Nokogiri::XML::Node>>
|
20
|
+
def initialize(pattern = MENTION_PATTERN, &link_builder)
|
21
|
+
@pattern = pattern
|
22
|
+
@link_builder = link_builder
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param [Nokogiri::HTML::DocumentFragment] doc
|
26
|
+
def call(doc)
|
27
|
+
candidate_map = {}
|
28
|
+
candidates = []
|
29
|
+
|
30
|
+
doc.xpath('.//text()').each do |text_node|
|
31
|
+
next if has_ancestor?(text_node, 'a', 'code', 'pre')
|
32
|
+
|
33
|
+
candidate_html = text_node.content.gsub(pattern) do |mention|
|
34
|
+
"<span class='mention-candidate'>#{mention}</span>"
|
35
|
+
end
|
36
|
+
|
37
|
+
next if text_node.content == candidate_html
|
38
|
+
|
39
|
+
candidate_fragment = text_node.replace(candidate_html)
|
40
|
+
candidate_fragment.css('span').each do |mention_element|
|
41
|
+
(candidate_map[mention_element.child.content] ||= []) << mention_element
|
42
|
+
end
|
43
|
+
|
44
|
+
candidates << candidate_fragment
|
45
|
+
end
|
46
|
+
|
47
|
+
unless candidate_map.empty?
|
48
|
+
link_builder.call(candidate_map)
|
49
|
+
|
50
|
+
# cleanup
|
51
|
+
candidates.each do |candidate_fragment|
|
52
|
+
candidate_fragment.css('span.mention-candidate').each do |node|
|
53
|
+
node.replace(node.child.content)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# note: do require 'sanitize' by yourself
|
4
|
+
|
5
|
+
# https://github.com/rgrove/sanitize
|
6
|
+
module Mato
|
7
|
+
module HtmlFilters
|
8
|
+
class Sanitization
|
9
|
+
attr_reader :sanitize
|
10
|
+
|
11
|
+
# @param [Sanitize] sanitize
|
12
|
+
def initialize(sanitize:)
|
13
|
+
@sanitize = sanitize
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(doc)
|
17
|
+
sanitize.node!(doc)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../anchor_builder'
|
4
|
+
|
5
|
+
module Mato
|
6
|
+
module HtmlFilters
|
7
|
+
class SectionAnchor
|
8
|
+
|
9
|
+
HX_PATTERN = 'h1,h2,h3,h4,h5,h6'
|
10
|
+
|
11
|
+
def initialize(anchor_icon_element = AnchorBuilder::DEFAULT_ANCHOR_ICON_ELEMENT)
|
12
|
+
@anchor_icon_element = anchor_icon_element
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param [Nokogiri::HTML::DocumentFragment] doc
|
16
|
+
def call(doc)
|
17
|
+
anchor_builder = AnchorBuilder.new(@anchor_icon_element)
|
18
|
+
|
19
|
+
doc.css(HX_PATTERN).each do |hx|
|
20
|
+
hx.children = anchor_builder.make_anchor_element(hx) + hx.children.to_html
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# do require 'rouge' by yourself
|
4
|
+
|
5
|
+
module Mato
|
6
|
+
module HtmlFilters
|
7
|
+
class SyntaxHighlight
|
8
|
+
|
9
|
+
# @param [Nokogiri::HTML::DocumentFragment] doc
|
10
|
+
def call(doc)
|
11
|
+
doc.search("pre").each do |pre|
|
12
|
+
if pre.at('code')
|
13
|
+
pre.replace(highlight(pre))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [String] language
|
19
|
+
# @param [String] filename
|
20
|
+
# @param [String] source
|
21
|
+
# @return [Rouge::Lexer]
|
22
|
+
def guess_lexer(language, filename, source)
|
23
|
+
Rouge::Lexer.find(language)&.tap do |lexer|
|
24
|
+
return lexer.new
|
25
|
+
end
|
26
|
+
|
27
|
+
lexers = Rouge::Lexer.guesses(filename: filename, source: source)
|
28
|
+
|
29
|
+
if lexers.empty?
|
30
|
+
Rouge::Lexers::PlainText.new
|
31
|
+
else
|
32
|
+
lexers.first.new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param [String,nil] CSS class names, e.g. "foo.js" "ruby:foo.rb"
|
37
|
+
def parse_label(class_name)
|
38
|
+
a = class_name&.split(/:/) || []
|
39
|
+
if a.empty?
|
40
|
+
{}
|
41
|
+
elsif a.size == 1
|
42
|
+
token = a[0].sub(/^language-/, '')
|
43
|
+
if Rouge::Lexer.find(token)
|
44
|
+
{ language: token }
|
45
|
+
else
|
46
|
+
{ filename: token }
|
47
|
+
end
|
48
|
+
else
|
49
|
+
{ language: a[0], filename: a[1] }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# @param [Nokogiri::XML::Element] pre a <pre/> element
|
56
|
+
# @return [Nokogiri::XML::Element] a new <div/> wrapping the given code block
|
57
|
+
def highlight(pre)
|
58
|
+
code = pre.at('code')
|
59
|
+
metadata = parse_label(code['class'])
|
60
|
+
language = metadata[:language]
|
61
|
+
filename = metadata[:filename]
|
62
|
+
source = code.inner_text
|
63
|
+
|
64
|
+
lexer = guess_lexer(language, filename, source)
|
65
|
+
|
66
|
+
document = Nokogiri::HTML.fragment(%{<div class="code-frame"/>})
|
67
|
+
|
68
|
+
div = document.at('div')
|
69
|
+
div.add_child(label_fragment(filename || language || lexer.tag)) if filename || !lexer.is_a?(Rouge::Lexers::PlainText)
|
70
|
+
div.add_child(%{<pre class="highlight"><code data-lang="#{lexer.tag}">#{format(lexer, source)}</code></pre>})
|
71
|
+
|
72
|
+
document
|
73
|
+
end
|
74
|
+
|
75
|
+
def label_fragment(label)
|
76
|
+
Nokogiri::HTML.fragment(%{<div class="code-label"/>}).tap do |fragment|
|
77
|
+
fragment.at('div').add_child(Nokogiri::XML::Text.new(label, fragment))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def formatter
|
82
|
+
@formatter ||= Rouge::Formatters::HTML.new
|
83
|
+
end
|
84
|
+
|
85
|
+
def format(lexer, source)
|
86
|
+
tokens = lexer.lex(source)
|
87
|
+
formatter.format(tokens)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mato
|
4
|
+
module HtmlFilters
|
5
|
+
class TaskList
|
6
|
+
CHECKED_MARK = "[x] "
|
7
|
+
UNCHECKED_MARK = "[ ] "
|
8
|
+
|
9
|
+
DEFAULT_TASK_LIST_CLASS = "task-list-item"
|
10
|
+
DEFAULT_CHECKBOX_CLASS = "task-list-item-checkbox"
|
11
|
+
|
12
|
+
def initialize(task_list_class: DEFAULT_TASK_LIST_CLASS, checkbox_class: DEFAULT_CHECKBOX_CLASS)
|
13
|
+
@task_list_class = task_list_class
|
14
|
+
@checkbox_class = checkbox_class
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [Nokogiri::HTML::DocumentFragment] doc
|
18
|
+
def call(doc)
|
19
|
+
doc.search("li").each do |li|
|
20
|
+
weave(li)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param [Nokogiri::XML::Node] li
|
25
|
+
def weave(li)
|
26
|
+
text_node = li.xpath('.//text()').first
|
27
|
+
checked = has_checked_mark?(text_node)
|
28
|
+
unchecked = has_unchecked_mark?(text_node)
|
29
|
+
|
30
|
+
return unless checked || unchecked
|
31
|
+
|
32
|
+
li["class"] = @task_list_class
|
33
|
+
|
34
|
+
text_node.content = trim_mark(text_node.content, checked)
|
35
|
+
checkbox = build_checkbox_node(checked)
|
36
|
+
text_node.add_previous_sibling(checkbox)
|
37
|
+
end
|
38
|
+
|
39
|
+
def has_checked_mark?(text_node)
|
40
|
+
text_node&.content&.start_with?(CHECKED_MARK)
|
41
|
+
end
|
42
|
+
|
43
|
+
def has_unchecked_mark?(text_node)
|
44
|
+
text_node&.content&.start_with?(UNCHECKED_MARK)
|
45
|
+
end
|
46
|
+
|
47
|
+
def trim_mark(content, checked)
|
48
|
+
if checked
|
49
|
+
content.sub(CHECKED_MARK, '')
|
50
|
+
else
|
51
|
+
content.sub(UNCHECKED_MARK, '')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_checkbox_node(checked)
|
56
|
+
Nokogiri::HTML.fragment('<input type="checkbox"/>').tap do |fragment|
|
57
|
+
checkbox = fragment.children.first
|
58
|
+
checkbox["class"] = @checkbox_class
|
59
|
+
checkbox["disabled"] = 'disabled'
|
60
|
+
checkbox["checked"] = 'checked' if checked
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../concerns/html_node_checkable'
|
4
|
+
|
5
|
+
module Mato
|
6
|
+
module HtmlFilters
|
7
|
+
class TokenLink
|
8
|
+
include Concerns::HtmlNodeCheckable
|
9
|
+
|
10
|
+
# @return [Regexp]
|
11
|
+
attr_reader :pattern
|
12
|
+
|
13
|
+
# @return [Proc]
|
14
|
+
attr_reader :builder
|
15
|
+
|
16
|
+
# @param [Regexp] pattern
|
17
|
+
# @param [Procc] builder link builder that takes
|
18
|
+
def initialize(pattern, &builder)
|
19
|
+
@pattern = pattern
|
20
|
+
@builder = builder
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [Nokogiri::HTML::DocumentFragment] doc
|
24
|
+
def call(doc)
|
25
|
+
doc.xpath('.//text()').each do |text_node|
|
26
|
+
next if has_ancestor?(text_node, 'a', 'code')
|
27
|
+
|
28
|
+
text_node.replace(text_node.content.gsub(pattern, &builder))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative('./document')
|
4
|
+
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'commonmarker'
|
7
|
+
|
8
|
+
module Mato
|
9
|
+
class Processor
|
10
|
+
# @return [Mato::Config]
|
11
|
+
attr_reader :config
|
12
|
+
|
13
|
+
def initialize(config)
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param [String] input
|
18
|
+
# @return [Mato::Document]
|
19
|
+
def process(input)
|
20
|
+
text = input.dup
|
21
|
+
|
22
|
+
config.text_filters.each do |filter|
|
23
|
+
filter.call(text)
|
24
|
+
end
|
25
|
+
|
26
|
+
markdown_node = parse_markdown(text)
|
27
|
+
|
28
|
+
config.markdown_filters.each do |filter|
|
29
|
+
filter.call(markdown_node)
|
30
|
+
end
|
31
|
+
|
32
|
+
html = render_to_html(markdown_node)
|
33
|
+
doc = parse_html(html)
|
34
|
+
|
35
|
+
config.html_filters.each do |filter|
|
36
|
+
filter.call(doc)
|
37
|
+
end
|
38
|
+
|
39
|
+
config.document_factory.new(doc.freeze)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @param [String] text
|
43
|
+
# @return [CommonMarker::Node]
|
44
|
+
def parse_markdown(text)
|
45
|
+
config.markdown_parser.render_doc(text, config.markdown_parse_options, config.markdown_extensions)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param [CommonMarker::Node] markdown_node
|
49
|
+
# @return [String]
|
50
|
+
def render_to_html(markdown_node)
|
51
|
+
markdown_node.to_html(config.markdown_render_options)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @param [String] html
|
55
|
+
# @return [Nokogiri::HTML::DocumentFragment]
|
56
|
+
def parse_html(html)
|
57
|
+
config.html_parser.parse(html)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../anchor_builder'
|
4
|
+
|
5
|
+
module Mato
|
6
|
+
module Renderers
|
7
|
+
class HtmlTocRenderer
|
8
|
+
|
9
|
+
H_SELECTOR = %w(h1 h2 h3 h4 h5 h6).join(',')
|
10
|
+
|
11
|
+
# @param [Nokogiri::HTML::DocumentFragment] doc
|
12
|
+
def call(doc)
|
13
|
+
s = +''
|
14
|
+
|
15
|
+
stack = [0]
|
16
|
+
|
17
|
+
doc.css(H_SELECTOR).each do |hx|
|
18
|
+
h_level = level(hx)
|
19
|
+
if h_level > stack.last
|
20
|
+
stack.push(h_level)
|
21
|
+
s << %{<ul>\n}
|
22
|
+
elsif h_level < stack.last
|
23
|
+
while stack.last > h_level
|
24
|
+
s << %{</li></ul>\n}
|
25
|
+
stack.pop
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
first_child = hx.child
|
30
|
+
if anchor?(first_child)
|
31
|
+
s << %{<li><a href="##{first_child['id']}">}
|
32
|
+
|
33
|
+
child = first_child.next_sibling
|
34
|
+
while child
|
35
|
+
s << child.to_html
|
36
|
+
child = child.next_sibling
|
37
|
+
end
|
38
|
+
|
39
|
+
s << %{</a>}
|
40
|
+
else
|
41
|
+
s << %{<li>#{hx.children}}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
while stack.last != 0
|
46
|
+
stack.pop
|
47
|
+
s << %{</li></ul>\n}
|
48
|
+
end
|
49
|
+
|
50
|
+
s
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# @param [Nokogiri::XML::Node] node
|
56
|
+
def level(node)
|
57
|
+
/\d+/.match(node.name)[0].to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
# @param [Nokogiri::XML::Node] node
|
61
|
+
def anchor?(node)
|
62
|
+
node.name == 'a' && node['class'] == AnchorBuilder::CSS_CLASS_NAME
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/mato/version.rb
ADDED
data/mato.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './lib/mato/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "mato"
|
7
|
+
spec.version = Mato::VERSION
|
8
|
+
spec.authors = ["FUJI Goro"]
|
9
|
+
spec.email = ["goro-fuj@bitjoureny.com"]
|
10
|
+
|
11
|
+
spec.summary = 'MArkdown TOolkit'
|
12
|
+
spec.description = 'An extensible markdown processing toolkit based on CommonMark'
|
13
|
+
spec.homepage = "https://github.com/bitjourney/mato"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = "exe"
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.required_ruby_version = ">= 2.3"
|
24
|
+
|
25
|
+
spec.add_runtime_dependency "nokogiri", ">= 1.6"
|
26
|
+
spec.add_runtime_dependency "commonmarker", ">= 0.14"
|
27
|
+
|
28
|
+
# spec.add_optional_dependency "sanitize", ">= 3.0"
|
29
|
+
# spec.add_optional_dependency "rouge", ">= 2.0"
|
30
|
+
|
31
|
+
spec.add_development_dependency "bundler", ">= 1.14"
|
32
|
+
spec.add_development_dependency "rake", ">= 10.0"
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mato
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- FUJI Goro
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-08-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: commonmarker
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.14'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.14'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.14'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.14'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
description: An extensible markdown processing toolkit based on CommonMark
|
70
|
+
email:
|
71
|
+
- goro-fuj@bitjoureny.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".rubocop.yml"
|
78
|
+
- ".travis.yml"
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE.txt
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- bin/console
|
84
|
+
- bin/setup
|
85
|
+
- example/hello.rb
|
86
|
+
- example/toc.rb
|
87
|
+
- lib/mato.rb
|
88
|
+
- lib/mato/anchor_builder.rb
|
89
|
+
- lib/mato/concerns/html_node_checkable.rb
|
90
|
+
- lib/mato/config.rb
|
91
|
+
- lib/mato/document.rb
|
92
|
+
- lib/mato/html_filters/mention_link.rb
|
93
|
+
- lib/mato/html_filters/sanitization.rb
|
94
|
+
- lib/mato/html_filters/section_anchor.rb
|
95
|
+
- lib/mato/html_filters/syntax_highlight.rb
|
96
|
+
- lib/mato/html_filters/task_list.rb
|
97
|
+
- lib/mato/html_filters/token_link.rb
|
98
|
+
- lib/mato/processor.rb
|
99
|
+
- lib/mato/renderers/html_renderer.rb
|
100
|
+
- lib/mato/renderers/html_toc_renderer.rb
|
101
|
+
- lib/mato/version.rb
|
102
|
+
- mato.gemspec
|
103
|
+
homepage: https://github.com/bitjourney/mato
|
104
|
+
licenses:
|
105
|
+
- MIT
|
106
|
+
metadata: {}
|
107
|
+
post_install_message:
|
108
|
+
rdoc_options: []
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '2.3'
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
requirements: []
|
122
|
+
rubyforge_project:
|
123
|
+
rubygems_version: 2.6.11
|
124
|
+
signing_key:
|
125
|
+
specification_version: 4
|
126
|
+
summary: MArkdown TOolkit
|
127
|
+
test_files: []
|