hamdown_core 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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