hamdown_core 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamdownCore
4
+ module Ast
5
+ module HasChildren
6
+ def initialize(*)
7
+ super
8
+ self.children ||= []
9
+ end
10
+
11
+ def <<(ast)
12
+ self.children << ast
13
+ end
14
+
15
+ def to_h
16
+ super.merge(children: children.map(&:to_h))
17
+ end
18
+ end
19
+
20
+ Root = Struct.new(:children) do
21
+ include HasChildren
22
+
23
+ def to_h
24
+ super.merge(type: 'root')
25
+ end
26
+ end
27
+
28
+ Doctype = Struct.new(:doctype, :filename, :lineno) do
29
+ def to_h
30
+ super.merge(type: 'doctype')
31
+ end
32
+ end
33
+
34
+ Element = Struct.new(
35
+ :children,
36
+ :tag_name,
37
+ :static_class,
38
+ :static_id,
39
+ :old_attributes,
40
+ :new_attributes,
41
+ :oneline_child,
42
+ :self_closing,
43
+ :nuke_inner_whitespace,
44
+ :nuke_outer_whitespace,
45
+ :object_ref,
46
+ :filename,
47
+ :lineno,
48
+ ) do
49
+ include HasChildren
50
+
51
+ def initialize(*)
52
+ super
53
+ self.static_class ||= ''
54
+ self.static_id ||= ''
55
+ self.self_closing ||= false
56
+ self.nuke_inner_whitespace ||= false
57
+ self.nuke_outer_whitespace ||= false
58
+ end
59
+
60
+ def to_h
61
+ super.merge(
62
+ type: 'element',
63
+ oneline_child: oneline_child && oneline_child.to_h,
64
+ )
65
+ end
66
+
67
+ # XXX: For compatibility
68
+ def attributes
69
+ attrs = old_attributes || ''
70
+ if new_attributes
71
+ if attrs.empty?
72
+ attrs = new_attributes
73
+ else
74
+ attrs += ", #{new_attributes}"
75
+ end
76
+ end
77
+ attrs
78
+ end
79
+ end
80
+
81
+ Script = Struct.new(
82
+ :children,
83
+ :script,
84
+ :keyword,
85
+ :escape_html,
86
+ :preserve,
87
+ :filename,
88
+ :lineno,
89
+ ) do
90
+ include HasChildren
91
+
92
+ def initialize(*)
93
+ super
94
+ if escape_html.nil?
95
+ self.escape_html = true
96
+ end
97
+ if preserve.nil?
98
+ self.preserve = false
99
+ end
100
+ end
101
+
102
+ def to_h
103
+ super.merge(type: 'script')
104
+ end
105
+ end
106
+
107
+ SilentScript = Struct.new(:children, :script, :keyword, :filename, :lineno) do
108
+ include HasChildren
109
+
110
+ def to_h
111
+ super.merge(type: 'silent_script')
112
+ end
113
+ end
114
+
115
+ HtmlComment = Struct.new(:children, :comment, :conditional, :filename, :lineno) do
116
+ include HasChildren
117
+
118
+ def initialize(*)
119
+ super
120
+ self.comment ||= ''
121
+ self.conditional ||= ''
122
+ end
123
+
124
+ def to_h
125
+ super.merge(type: 'html_comment')
126
+ end
127
+ end
128
+
129
+ HamlComment = Struct.new(:children, :filename, :lineno) do
130
+ include HasChildren
131
+
132
+ def to_h
133
+ super.merge(type: 'haml_comment')
134
+ end
135
+ end
136
+
137
+ Text = Struct.new(:text, :escape_html, :filename, :lineno) do
138
+ def initialize(*)
139
+ super
140
+ if escape_html.nil?
141
+ self.escape_html = true
142
+ end
143
+ end
144
+
145
+ def to_h
146
+ super.merge(type: 'text')
147
+ end
148
+
149
+ def markdownable?
150
+ true
151
+ end
152
+ end
153
+
154
+ class MdHeader < Text; end
155
+ class MdList < Text; end
156
+ class MdQuote < Text; end
157
+ class MdImageTitle < Text; end
158
+ class MdImage < Text; end
159
+ class MdLinkTitle < Text; end
160
+ class MdLink < Text; end
161
+
162
+ Filter = Struct.new(:name, :texts, :filename, :lineno) do
163
+ def initialize(*)
164
+ super
165
+ self.texts ||= []
166
+ end
167
+
168
+ def to_h
169
+ super.merge(type: 'filter')
170
+ end
171
+ end
172
+
173
+ Empty = Struct.new(:filename, :lineno) do
174
+ def to_h
175
+ super.merge(type: 'empty')
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ require 'optparse'
3
+ require_relative 'version'
4
+
5
+ module HamdownCore
6
+ module Cli
7
+ def self.call(argv)
8
+ file_name = OptionParser.new.tap do |parser|
9
+ parser.version = VERSION
10
+ end.parse!(argv).first
11
+
12
+ if file_name.nil? || file_name.size == 0
13
+ puts 'Error: No file.'
14
+ puts 'Use it like: "exe/hamdown_core path_to/file.hd > output.html"'
15
+ return nil
16
+ end
17
+
18
+ content = File.open(file_name, 'r').read
19
+ output = Engine.call(content)
20
+ puts output
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HamdownCore
4
+ module Compiler
5
+ class << self
6
+ def call(ast_root)
7
+ strings = []
8
+ strings = render_strings(ast_root.children, 0)
9
+ strings.join("\n")
10
+ end
11
+
12
+ private
13
+
14
+ # TODO: refactoring
15
+ def render_strings(ast_nodes, space_deep = 0)
16
+ strings = []
17
+ ast_nodes.each do |node|
18
+ case node
19
+ when HamdownCore::Ast::Filter
20
+ str = (' ' * space_deep)
21
+ str += ":#{node.name}"
22
+ strings << str
23
+ node.texts.each do |row|
24
+ row = (' ' * (space_deep + 2)) + row
25
+ strings << row
26
+ end
27
+ when HamdownCore::Ast::SilentScript
28
+ str = (' ' * space_deep)
29
+ str += "- #{node.script}"
30
+ strings << str
31
+ when HamdownCore::Ast::Script
32
+ str = (' ' * space_deep)
33
+ if node.escape_html == true
34
+ str += "= #{node.script}"
35
+ else
36
+ str += "!= #{node.script}"
37
+ end
38
+ strings << str
39
+ when HamdownCore::Ast::Text
40
+ strings << (' ' * space_deep + "#{node.text}")
41
+ when HamdownCore::Ast::HtmlComment
42
+ strings << (' ' * space_deep + "/ #{node.comment}")
43
+ when HamdownCore::Ast::Empty
44
+ # NODE: or add spaces?
45
+ strings << ''
46
+ when HamdownCore::Ast::Element
47
+ str = (' ' * space_deep)
48
+
49
+ str += "%#{node.tag_name}"
50
+
51
+ if node.static_id.size > 0
52
+ str += "##{node.static_id}"
53
+ end
54
+
55
+ if node.static_class.size > 0
56
+ str += ".#{node.static_class.gsub(' ', '.')}"
57
+ end
58
+
59
+ if !node.new_attributes.nil?
60
+ props = []
61
+ node.new_attributes.split(',').each do |str|
62
+ l,r = str.split(' => ')
63
+ l.gsub!("\"", '')
64
+ r.gsub!("\"", "'")
65
+ props << "#{l}=#{r}"
66
+ end
67
+ str += "(#{props.join(' ')})"
68
+ end
69
+
70
+ if !node.old_attributes.nil?
71
+ str += "{#{node.old_attributes}}"
72
+ end
73
+
74
+ if !node.oneline_child.nil?
75
+ substr = render_strings([node.oneline_child]).first
76
+ unless substr.start_with?('=')
77
+ substr = " #{substr}"
78
+ end
79
+ str += substr
80
+ end
81
+
82
+ strings << str
83
+ end
84
+
85
+ if node.respond_to?(:children) && node.children.size > 0
86
+ render_strings(node.children, space_deep + 2).each do |str|
87
+ strings << str
88
+ end
89
+ end
90
+ end
91
+ strings
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+ require 'strscan'
3
+ require_relative 'ast'
4
+ require_relative 'error'
5
+ require_relative 'ruby_multiline'
6
+ require_relative 'script_parser'
7
+ require_relative 'utils'
8
+
9
+ module HamdownCore
10
+ class ElementParser
11
+ def initialize(line_parser)
12
+ @line_parser = line_parser
13
+ end
14
+
15
+ ELEMENT_REGEXP = /\A%([-:\w]+)([-:\w.#]*)(.+)?\z/o
16
+
17
+ def parse(text)
18
+ m = text.match(ELEMENT_REGEXP)
19
+ unless m
20
+ syntax_error!('Invalid element declaration')
21
+ end
22
+
23
+ element = Ast::Element.new
24
+ element.filename = @line_parser.filename
25
+ element.lineno = @line_parser.lineno
26
+ element.tag_name = m[1]
27
+ element.static_class, element.static_id = parse_class_and_id(m[2])
28
+ rest = m[3] || ''
29
+
30
+ element.old_attributes, element.new_attributes, element.object_ref, rest = parse_attributes(rest)
31
+ element.nuke_inner_whitespace, element.nuke_outer_whitespace, rest = parse_nuke_whitespace(rest)
32
+ element.self_closing, rest = parse_self_closing(rest)
33
+ element.oneline_child = ScriptParser.new(@line_parser).parse(rest)
34
+
35
+ element
36
+ end
37
+
38
+ private
39
+
40
+ def parse_class_and_id(class_and_id)
41
+ classes = []
42
+ id = ''
43
+ scanner = StringScanner.new(class_and_id)
44
+ until scanner.eos?
45
+ unless scanner.scan(/([#.])([-:_a-zA-Z0-9]+)/)
46
+ syntax_error!('Illegal element: classes and ids must have values.')
47
+ end
48
+ case scanner[1]
49
+ when '.'
50
+ classes << scanner[2]
51
+ when '#'
52
+ id = scanner[2]
53
+ end
54
+ end
55
+
56
+ [classes.join(' '), id]
57
+ end
58
+
59
+ OLD_ATTRIBUTE_BEGIN = '{'
60
+ NEW_ATTRIBUTE_BEGIN = '('
61
+ OBJECT_REF_BEGIN = '['
62
+
63
+ def parse_attributes(rest)
64
+ old_attributes = nil
65
+ new_attributes = nil
66
+ object_ref = nil
67
+
68
+ loop do
69
+ case rest[0]
70
+ when OLD_ATTRIBUTE_BEGIN
71
+ if old_attributes
72
+ break
73
+ end
74
+ old_attributes, rest = parse_old_attributes(rest)
75
+ when NEW_ATTRIBUTE_BEGIN
76
+ if new_attributes
77
+ break
78
+ end
79
+ new_attributes, rest = parse_new_attributes(rest)
80
+ when OBJECT_REF_BEGIN
81
+ if object_ref
82
+ break
83
+ end
84
+ object_ref, rest = parse_object_ref(rest)
85
+ else
86
+ break
87
+ end
88
+ end
89
+
90
+ [old_attributes, new_attributes, object_ref, rest]
91
+ end
92
+
93
+ def parse_old_attributes(text)
94
+ text = text.dup
95
+ s = StringScanner.new(text)
96
+ s.pos = 1
97
+ depth = 1
98
+ loop do
99
+ depth = Utils.balance(s, '{', '}', depth)
100
+ if depth == 0
101
+ attr = s.pre_match + s.matched
102
+ return [attr[1, attr.size - 2], s.rest]
103
+ elsif /,\s*\z/ === text && @line_parser.has_next?
104
+ text << "\n" << @line_parser.next_line
105
+ else
106
+ syntax_error!('Unmatched brace')
107
+ end
108
+ end
109
+ end
110
+
111
+ def parse_new_attributes(text)
112
+ text = text.dup
113
+ s = StringScanner.new(text)
114
+ s.pos = 1
115
+ depth = 1
116
+ loop do
117
+ pre_pos = s.pos
118
+ depth = Utils.balance(s, '(', ')', depth)
119
+ if depth == 0
120
+ t = s.string.byteslice(pre_pos...s.pos - 1)
121
+ return [parse_new_attribute_list(t), s.rest]
122
+ elsif @line_parser.has_next?
123
+ text << "\n" << @line_parser.next_line
124
+ else
125
+ syntax_error!('Unmatched paren')
126
+ end
127
+ end
128
+ end
129
+
130
+ def parse_new_attribute_list(text)
131
+ s = StringScanner.new(text)
132
+ attributes = []
133
+ until s.eos?
134
+ name = scan_key(s)
135
+ s.skip(/\s*/)
136
+
137
+ if scan_operator(s)
138
+ s.skip(/\s*/)
139
+ value = scan_value(s)
140
+ else
141
+ value = 'true'
142
+ end
143
+ spaces = s.scan(/\s*/)
144
+ line_count = spaces.count("\n")
145
+
146
+ attributes << "#{name.inspect} => #{value},#{"\n" * line_count}"
147
+ end
148
+ attributes.join
149
+ end
150
+
151
+ def scan_key(scanner)
152
+ scanner.scan(/[-:\w]+/).tap do |name|
153
+ unless name
154
+ syntax_error!('Invalid attribute list (missing attribute name)')
155
+ end
156
+ end
157
+ end
158
+
159
+ def scan_operator(scanner)
160
+ scanner.skip(/=/)
161
+ end
162
+
163
+ def scan_value(scanner)
164
+ quote = scanner.scan(/["']/)
165
+ if quote
166
+ scan_quoted_value(scanner, quote)
167
+ else
168
+ scan_variable_value(scanner)
169
+ end
170
+ end
171
+
172
+ def scan_quoted_value(scanner, quote)
173
+ re = /((?:\\.|\#(?!\{)|[^#{quote}\\#])*)(#{quote}|#\{)/
174
+ pos = scanner.pos
175
+ loop do
176
+ unless scanner.scan(re)
177
+ syntax_error!('Invalid attribute list (mismatched quotation)')
178
+ end
179
+ if scanner[2] == quote
180
+ break
181
+ end
182
+ depth = Utils.balance(scanner, '{', '}')
183
+ if depth != 0
184
+ syntax_error!('Invalid attribute list (mismatched interpolation)')
185
+ end
186
+ end
187
+ str = scanner.string.byteslice(pos - 1..scanner.pos - 1)
188
+
189
+ # Even if the quote is single, string interpolation is performed in Haml.
190
+ str[0] = '"'
191
+ str[-1] = '"'
192
+ str
193
+ end
194
+
195
+ def scan_variable_value(scanner)
196
+ scanner.scan(/(@@?|\$)?\w+/).tap do |var|
197
+ unless var
198
+ syntax_error!('Invalid attribute list (invalid variable name)')
199
+ end
200
+ end
201
+ end
202
+
203
+ def parse_object_ref(text)
204
+ s = StringScanner.new(text)
205
+ s.pos = 1
206
+ depth = Utils.balance(s, '[', ']')
207
+ if depth == 0
208
+ [s.pre_match[1, s.pre_match.size - 1], s.rest]
209
+ else
210
+ syntax_error!('Unmatched brackets for object reference')
211
+ end
212
+ end
213
+
214
+ def parse_nuke_whitespace(rest)
215
+ m = rest.match(/\A(><|<>|[><])(.*)\z/)
216
+ if m
217
+ nuke_whitespace = m[1]
218
+ [
219
+ nuke_whitespace.include?('<'),
220
+ nuke_whitespace.include?('>'),
221
+ m[2],
222
+ ]
223
+ else
224
+ [false, false, rest]
225
+ end
226
+ end
227
+
228
+ def parse_self_closing(rest)
229
+ if rest[0] == '/'
230
+ if rest.size > 1
231
+ syntax_error!("Self-closing tags can't have content")
232
+ end
233
+ [true, '']
234
+ else
235
+ [false, rest]
236
+ end
237
+ end
238
+
239
+ def syntax_error!(message)
240
+ raise Error.new(message, @line_parser.lineno)
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'parser'
3
+ require_relative 'transformer'
4
+ require_relative 'compiler'
5
+
6
+ module HamdownCore
7
+ module Engine
8
+ def self.call(content)
9
+ ast = HamdownCore::Parser.new.call(content)
10
+ transformed_ast = HamdownCore::Transformer.call(ast)
11
+ puts HamdownCore::Compiler.call(transformed_ast)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ module HamdownCore
3
+ class Error < StandardError
4
+ attr_accessor :lineno
5
+
6
+ def initialize(message, lineno)
7
+ super(message)
8
+ @lineno = lineno
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'ast'
3
+
4
+ module HamdownCore
5
+ class FilterParser
6
+ def initialize(indent_tracker)
7
+ @ast = nil
8
+ @indent_level = nil
9
+ @indent_tracker = indent_tracker
10
+ end
11
+
12
+ def enabled?
13
+ !@ast.nil?
14
+ end
15
+
16
+ def start(name, filename, lineno)
17
+ @ast = Ast::Filter.new
18
+ @ast.name = name
19
+ @ast.filename = filename
20
+ @ast.lineno = lineno
21
+ end
22
+
23
+ def append(line)
24
+ indent, text = @indent_tracker.split(line)
25
+ if text.empty?
26
+ @ast.texts << ''
27
+ return
28
+ end
29
+ indent_level = indent.size
30
+
31
+ if @indent_level
32
+ if indent_level < @indent_level
33
+ # Finish filter
34
+ @indent_level = nil
35
+ ast = @ast
36
+ @ast = nil
37
+ return ast
38
+ end
39
+ elsif indent_level > @indent_tracker.current_level
40
+ # Start filter
41
+ @indent_level = indent_level
42
+ else
43
+ # Empty filter
44
+ @ast = nil
45
+ return nil
46
+ end
47
+
48
+ text = line[@indent_level..-1]
49
+ @ast.texts << text
50
+ nil
51
+ end
52
+
53
+ def finish
54
+ @ast
55
+ end
56
+ end
57
+ end