d-mark 0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2e6f0ea7fb496bb3aadc7ea266b4a0a7650eaa05
4
- data.tar.gz: 80049f389cec0e03ecf36c7d91320f99d2e45c89
3
+ metadata.gz: bee205ce9623266e9787eec60d991ddc1f3b99a3
4
+ data.tar.gz: 490bbf23b2a0a0b8cb1767f6b7a542f38a749ba1
5
5
  SHA512:
6
- metadata.gz: 65ccc586b328445e4b76e4b6c0d020055b00c209f73a6c392449b47cc4ec14aef7c3d3a4ac2e72769ca34ba270fa0855148363eb1a5d046ddb248ee865e5e079
7
- data.tar.gz: 1854baf6627c1c856255d855cc0f1e4b3dcac4b273843b64c205fc0c3902e5f651b31b152598a26618225a6b21ed89e80811769c77d372f8874203a2ab80314a
6
+ metadata.gz: 13cabb5e530a7067fe5e665c883d42f1bf59208249e2fd2ef9fe49cf87cfa0dffa1974be4307769dbb862d51ba466403154e6bbbbf655a244a275d760b26bacd
7
+ data.tar.gz: e65a20361865f63a70cf4de93fa27634dcf831ac4d966c64ccb146dabd03796e277b23fdb43dd7ebeef9fdde947f2c1d800e2afeb5710ebf79ec4d9231608bc9
data/Gemfile CHANGED
@@ -2,10 +2,7 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- group :devel do
6
- gem 'guard-rake'
7
- gem 'rake'
8
- gem 'rspec'
9
- gem 'rubocop'
10
- gem 'yard'
11
- end
5
+ gem 'guard'
6
+ gem 'guard-rake'
7
+ gem 'rspec'
8
+ gem 'rubocop'
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dmark (0.1)
4
+ d-mark (0.1.0)
5
+ treetop (~> 1.4)
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
@@ -23,17 +24,18 @@ GEM
23
24
  guard-rake (1.0.0)
24
25
  guard
25
26
  rake
26
- listen (3.0.5)
27
+ listen (3.0.6)
27
28
  rb-fsevent (>= 0.9.3)
28
- rb-inotify (>= 0.9)
29
+ rb-inotify (>= 0.9.7)
29
30
  lumberjack (1.0.10)
30
31
  method_source (0.8.2)
31
- nenv (0.2.0)
32
+ nenv (0.3.0)
32
33
  notiffany (0.0.8)
33
34
  nenv (~> 0.1)
34
35
  shellany (~> 0.0)
35
- parser (2.3.0.2)
36
+ parser (2.3.0.4)
36
37
  ast (~> 2.2)
38
+ polyglot (0.3.5)
37
39
  powerpack (0.1.1)
38
40
  pry (0.10.3)
39
41
  coderay (~> 1.1.0)
@@ -42,7 +44,7 @@ GEM
42
44
  rainbow (2.1.0)
43
45
  rake (10.5.0)
44
46
  rb-fsevent (0.9.7)
45
- rb-inotify (0.9.5)
47
+ rb-inotify (0.9.7)
46
48
  ffi (>= 0.5.0)
47
49
  rspec (3.4.0)
48
50
  rspec-core (~> 3.4.0)
@@ -57,28 +59,30 @@ GEM
57
59
  diff-lcs (>= 1.2.0, < 2.0)
58
60
  rspec-support (~> 3.4.0)
59
61
  rspec-support (3.4.1)
60
- rubocop (0.36.0)
61
- parser (>= 2.3.0.0, < 3.0)
62
+ rubocop (0.37.2)
63
+ parser (>= 2.3.0.4, < 3.0)
62
64
  powerpack (~> 0.1)
63
65
  rainbow (>= 1.99.1, < 3.0)
64
66
  ruby-progressbar (~> 1.7)
67
+ unicode-display_width (~> 0.3)
65
68
  ruby-progressbar (1.7.5)
66
69
  shellany (0.0.1)
67
70
  slop (3.6.0)
68
71
  thor (0.19.1)
69
- yard (0.8.7.6)
72
+ treetop (1.6.3)
73
+ polyglot (~> 0.3)
74
+ unicode-display_width (0.3.1)
70
75
 
71
76
  PLATFORMS
72
77
  ruby
73
78
 
74
79
  DEPENDENCIES
75
80
  bundler (>= 1.11.2, < 2.0)
76
- dmark!
81
+ d-mark!
82
+ guard
77
83
  guard-rake
78
- rake
79
84
  rspec
80
85
  rubocop
81
- yard
82
86
 
83
87
  BUNDLED WITH
84
88
  1.11.2
@@ -0,0 +1,3 @@
1
+ guard 'rake', task: 'default' do
2
+ watch(%r{^(lib|test|spec)/})
3
+ end
data/NEWS.md CHANGED
@@ -1,7 +1,15 @@
1
1
  # D★Mark news
2
2
 
3
- ## 0.1 (???)
3
+ ## 0.2 (2016-02-11)
4
4
 
5
- Features:
5
+ Fixes:
6
6
 
7
- * Initial release
7
+ * Many.
8
+
9
+ Enhancements:
10
+
11
+ * Many.
12
+
13
+ ## 0.1 (2016-01-31)
14
+
15
+ Initial release.
@@ -0,0 +1,218 @@
1
+ = D★Mark
2
+ Denis Defreyne <denis@stoneship.org>
3
+
4
+ CAUTION: D★Mark is experimental — use at your own risk!
5
+
6
+ _D★Mark_ is a language for marking up prose. It facilitates writing semantically meaningful text, without limiting itself to the semantics provided by HTML or Markdown.
7
+
8
+ Here’s an example of D★Mark:
9
+
10
+ [source]
11
+ ----
12
+ h2. Patterns
13
+
14
+ para. Patterns are used to find items and layouts based on their identifier. They come in three varieties:
15
+
16
+ list[unordered].
17
+ item. glob patterns
18
+ item. regular expression patterns
19
+ item. legacy patterns
20
+
21
+ para. A glob pattern that matches every item is %pattern{/**/*}. A glob pattern that matches every item/layout with the extension %filename{md} is %glob{/**/*.md}.
22
+ ----
23
+
24
+ == Samples
25
+
26
+ The `samples/` directory contains some sample D★Mark files. They can be processed by invoking the appropriate script with the same filename. For example:
27
+
28
+ ....
29
+ % bundle exec ruby samples/trivial.rb
30
+ <p>I’m a <em>trivial</em> example!</p>
31
+ ....
32
+
33
+ == Structure of a D★Mark document
34
+
35
+ _D★Mark_ knows two constructs:
36
+
37
+ Block-level elements::
38
+ Every non-blank line of a D★Mark document corresponds to a block. A block can be a paragraph, a list, a header, a source code listing, or more. They start with the name of the element, a period, a space character, followed by the content. For example:
39
+ +
40
+ [source]
41
+ ----
42
+ para. Patterns are used to find items and layouts based on their identifier. They come in three varieties.
43
+ ----
44
+
45
+ Inline elements::
46
+ Inside a block, text can be marked up using inline elements, which start with a percentage sign, the name of the element, and the content within braces. For example, `%emph{crazy}` is an `emph` element with the content `crazy`.
47
+
48
+ Block-level elements can be nested. To do so, indent the nested block two spaces deeper than the enclosing block. For example, the following defines a `list` element with three `item` elements inside it:
49
+
50
+ [source]
51
+ ----
52
+ list[unordered].
53
+ item. glob patterns
54
+ item. regular expression patterns
55
+ item. legacy patterns
56
+ ----
57
+
58
+ Block-level elements can also include plain text. In this case, the content is not wrapped inside a nested block-level element. This is particularly useful for source code listing. For example:
59
+
60
+ [source]
61
+ ----
62
+ listing[lang=ruby].
63
+ identifier = Nanoc::Identifier.new('/about.md')
64
+
65
+ identifier.without_ext
66
+ # => "/about"
67
+
68
+ identifier.ext
69
+ # => "md"
70
+ ----
71
+
72
+ Block-level elements and inline elements are identical in the tree representation of D★Mark. This means that any inline element can be rewritten as a block-level element.
73
+
74
+ NOTE: To do: Elaborate on the distinction and similarity of block-level and inline elements.
75
+
76
+ NOTE: To do: Describe escaping rules.
77
+
78
+ === Attributes
79
+
80
+ Both block and inline elements can also have attributes. Attributes are enclosed in square brackets after the element name, as a comma-separated list of key-value pairs separated by an equal sign. The value part, along with the equal sign, can be omitted, in which case the value will be equal to the key name.
81
+
82
+ For example:
83
+
84
+ * `%code[lang=ruby]{Nanoc::VERSION}` is an inline `code` element with the `lang` attribute set to `ruby`.
85
+
86
+ * `%only[web]{Refer to the release notes for details.}` is an inline `only` element with the `web` attribute set to `web`.
87
+
88
+ * `h2[id=donkey]. All about donkeys` is a block-level `h2` element with the `id` attribute set to `donkey`.
89
+
90
+ * `p[print]. This is a paragraph that only readers of the book will see.` is a block-level `para` element with the `print` attribute set to `print`.
91
+
92
+ NOTE: The behavior of keys with missing values might change to default to booleans rather than to the key name.
93
+
94
+ == Goals
95
+
96
+ Be extensible::
97
+ D★Mark defines only the syntax of the markup language, and doesn’t bother with semantics. It does not prescribe which element names are valid in the context of a vocabulary, because it does not come with a vocabulary.
98
+
99
+ Be simple::
100
+ Simplicity implies being easy to write and easy to parse. D★Mark eschews ambiguity and aims to have a short formal syntactical definition. This also means that it is easy to syntax highlight.
101
+
102
+ Be compact::
103
+ Introduce as little extra syntax as possible.
104
+
105
+ == Comparison with other languages
106
+
107
+ D★Mark takes inspiration from a variety of other languages.
108
+
109
+ HTML::
110
+ HTML is syntactically unambiguous, but comparatively more verbose than other languages. It also prescribes only a small set of elements, which makes it awkward to use for prose that requires more thorough markup. It is possible use `span` or `div` elements with custom classes, but this approach turns an already verbose language into something even more verbose.
111
+ +
112
+ [source,html]
113
+ ----
114
+ <p>A glob pattern that matches every item is <span class="pattern attr-kind-glob">/**/*</span>.</p>
115
+ ----
116
+ +
117
+ [source,d-mark]
118
+ ----
119
+ para. A glob pattern that matches every item is %pattern[glob]{/**/*}.
120
+ ----
121
+
122
+ XML::
123
+ Similar to HTML, with the major difference that XML does not prescribe a set of elements.
124
+ +
125
+ [source,xml]
126
+ ----
127
+ <para>A glob pattern that matches every item is <pattern kind="glob">/**/*</pattern>.</para>
128
+ ----
129
+ +
130
+ [source,d-mark]
131
+ ----
132
+ para. A glob pattern that matches every item is %pattern[glob]{/**/*}.
133
+ ----
134
+
135
+ Markdown::
136
+ Markdown has a compact syntax, but is complex and ambiguous, as evidenced by the many different mutually incompatible implementations. It prescribes a small set of elements (smaller even than HTML). It supports embedding raw HTML, which in theory makes it possible to combine the best of both worlds, but in practice leads to markup that is harder to read than either Markdown or HTML separately, and occasionally trips up the parser and syntax highlighter.
137
+ +
138
+ [source]
139
+ ----
140
+ A glob pattern that matches every item is <span class="glob attr-kind-glob">/**/*</span>.
141
+ ----
142
+ +
143
+ [source,d-mark]
144
+ ----
145
+ para. A glob pattern that matches every item is %pattern[glob]{/**/*}.
146
+ ----
147
+
148
+ AsciiDoc::
149
+ AsciiDoc, along with its AsciiDoctor variant, are syntactically unambiguous, but complex languages. They prescribe a comparatively large set of elements which translates well to DocBook and HTML. They do not support custom markup or embedding raw HTML, which makes them harder t use for prose that requires more complex markup.
150
+ +
151
+ _(No example, as this example cannot be represented with AsciiDoc.)_
152
+
153
+ TeX, LaTeX::
154
+ TeX is a turing-complete programming language, as opposed to a markup language, intended for typesetting. This makes it impractical for using it as the source for converting it to other formats. Its syntax is simple and compact, and served as an inspiration for D★Mark.
155
+ +
156
+ [source,latex]
157
+ ----
158
+ A glob pattern that matches every item is \pattern[glob]{/**/*}.
159
+ ----
160
+ +
161
+ [source,d-mark]
162
+ ----
163
+ para. A glob pattern that matches every item is %pattern[glob]{/**/*}.
164
+ ----
165
+
166
+ JSON, YAML::
167
+ JSON and YAML are data interchange formats rather than markup languages, and thus are not well-suited for marking up prose.
168
+ +
169
+ [source,json]
170
+ ----
171
+ [
172
+ "A glob pattern that matches every item is ",
173
+ ["pattern", {"kind": "glob"}, ["/**/*"]],
174
+ "."
175
+ ]
176
+ ----
177
+ +
178
+ [source,d-mark]
179
+ ----
180
+ para. A glob pattern that matches every item is %pattern[glob]{/**/*}.
181
+ ----
182
+
183
+ == Specification
184
+
185
+ NOTE: To do: write this section.
186
+
187
+ == Programmatic usage
188
+
189
+ Handling a D★Mark file consists of two stages: parsing and translating.
190
+
191
+ The parsing stage converts text into a list of nodes. Construct a parser with the tokens as input, and call `#run` to get the list of nodes.
192
+
193
+ [source,ruby]
194
+ ----
195
+ content = File.read(ARGV[0])
196
+ nodes = DMark::Parser.new(content).run
197
+ ----
198
+
199
+ The translating stage is not the responsibility of D★Mark. A translator is part of the domain of the source text, and D★Mark only deals with syntax rather than semantics. A translator will run over the tree and convert it into something else (usually another string). To do so, handle each node type (`DMark::ElementNode` or `String`). For example, the following translator will convert the tree into something that resembles XML:
200
+
201
+ [source,ruby]
202
+ ----
203
+ class MyXMLLikeTranslator < DMark::Translator
204
+ def handle(node)
205
+ case node
206
+ when String
207
+ out << node
208
+ when DMark::Parser::ElementNode
209
+ out << "<#{node.name}>"
210
+ handle_children(node)
211
+ out << "</#{node.name}>"
212
+ end
213
+ end
214
+ end
215
+
216
+ result = MyXMLLikeTranslator.new(nodes).run
217
+ puts result
218
+ ----
data/Rakefile CHANGED
@@ -1,3 +1,14 @@
1
- Rake.add_rakelib 'tasks'
1
+ require 'rspec/core/rake_task'
2
+ require 'rubocop/rake_task'
2
3
 
3
- task default: [:test, :rubocop]
4
+ RSpec::Core::RakeTask.new(:spec) do |t|
5
+ t.rspec_opts = '-r ./spec/spec_helper.rb --color'
6
+ t.verbose = false
7
+ end
8
+
9
+ RuboCop::RakeTask.new(:rubocop) do |task|
10
+ task.options = %w( --display-cop-names --format simple )
11
+ task.patterns = ['lib/**/*.rb', 'spec/**/*.rb']
12
+ end
13
+
14
+ task default: [:spec, :rubocop]
@@ -1,4 +1,4 @@
1
- require_relative 'lib/dmark/version'
1
+ require_relative 'lib/d-mark/version'
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'd-mark'
@@ -13,14 +13,15 @@ Gem::Specification.new do |s|
13
13
 
14
14
  s.files =
15
15
  Dir['[A-Z]*'] +
16
- Dir['{bin,lib,tasks,spec,samples,scripts}/**/*'] +
16
+ Dir['{lib,spec,samples}/**/*'] +
17
17
  ['d-mark.gemspec']
18
18
  s.require_paths = ['lib']
19
19
 
20
- s.rdoc_options = ['--main', 'README.md']
21
- s.extra_rdoc_files = ['LICENSE', 'README.md', 'NEWS.md']
20
+ s.rdoc_options = ['--main', 'README.adoc']
21
+ s.extra_rdoc_files = ['LICENSE', 'README.adoc', 'NEWS.md']
22
22
 
23
23
  s.required_ruby_version = '>= 2.1.0'
24
24
 
25
+ s.add_runtime_dependency('treetop', '~> 1.4')
25
26
  s.add_development_dependency('bundler', '>= 1.11.2', '< 2.0')
26
27
  end
@@ -0,0 +1,2 @@
1
+ require_relative 'd-mark/parser'
2
+ require_relative 'd-mark/translator'
@@ -0,0 +1,28 @@
1
+ require_relative '../d-mark'
2
+
3
+ data = File.read(ARGV[0]).strip
4
+
5
+ parser = DMark::Parser.new(data)
6
+ begin
7
+ before = Time.now
8
+ result = parser.parse
9
+ after = Time.now
10
+ result.each do |tree|
11
+ puts tree.inspect
12
+ puts
13
+ end
14
+ puts "parse duration: #{(after - before).to_f}s"
15
+ rescue => e
16
+ case e
17
+ when DMark::Parser::ParserError
18
+ line = data.lines[e.line_nr]
19
+
20
+ puts "\e[31mError:\e[0m #{e.message}}"
21
+ puts
22
+ puts line
23
+ puts "\e[31m" + ' ' * e.col_nr + '↑' + "\e[0m"
24
+ exit 1
25
+ else
26
+ raise e
27
+ end
28
+ end
@@ -0,0 +1,460 @@
1
+ module DMark
2
+ class Parser
3
+ class ParserError < StandardError
4
+ attr_reader :line_nr
5
+ attr_reader :col_nr
6
+
7
+ def initialize(line_nr, col_nr, msg)
8
+ @line_nr = line_nr
9
+ @col_nr = col_nr
10
+ @msg = msg
11
+
12
+ super("parse error at line #{@line_nr + 1}, col #{@col_nr + 1}: #{@msg}")
13
+ end
14
+ end
15
+
16
+ class ElementNode
17
+ attr_reader :name
18
+ attr_reader :attributes
19
+ attr_reader :children
20
+
21
+ def initialize(name, attributes, children)
22
+ @name = name
23
+ @attributes = attributes
24
+ @children = children
25
+ end
26
+
27
+ def inspect
28
+ io = ''
29
+ io << 'Element(' << @name << ', '
30
+ if @attributes.any?
31
+ io << @attributes.inspect
32
+ io << ', '
33
+ end
34
+ io << @children.inspect
35
+ io << ')'
36
+ io
37
+ end
38
+
39
+ def ==(other)
40
+ case other
41
+ when ElementNode
42
+ @name == other.name &&
43
+ @children == other.children &&
44
+ @attributes == other.attributes
45
+ else
46
+ false
47
+ end
48
+ end
49
+ end
50
+
51
+ attr_reader :pos
52
+
53
+ def initialize(input)
54
+ @input = input
55
+ @input_chars = @input.chars
56
+
57
+ @pos = 0
58
+ @col_nr = 0
59
+ @line_nr = 0
60
+ end
61
+
62
+ def parse
63
+ res = []
64
+
65
+ loop do
66
+ break if eof?
67
+ res << read_block_with_children
68
+ end
69
+
70
+ res
71
+ end
72
+
73
+ ##########
74
+
75
+ def peek_char(pos = @pos)
76
+ if eof?
77
+ nil
78
+ else
79
+ @input_chars[pos]
80
+ end
81
+ end
82
+
83
+ def eof?(pos = @pos)
84
+ pos >= @input_chars.size
85
+ end
86
+
87
+ def advance
88
+ if !eof? && @input_chars[@pos] == "\n"
89
+ @line_nr += 1
90
+ @col_nr = 0
91
+ end
92
+
93
+ @pos += 1
94
+ @col_nr += 1
95
+ end
96
+
97
+ def read_char(c)
98
+ char = peek_char
99
+ if char != c
100
+ raise_parse_error("expected #{c.inspect}, but got #{char.nil? ? 'EOF' : char.inspect}")
101
+ else
102
+ advance
103
+ char
104
+ end
105
+ end
106
+
107
+ ##########
108
+
109
+ def read_block_with_children(indentation = 0)
110
+ res = read_single_block
111
+
112
+ pending_blanks = 0
113
+ until eof?
114
+ blank_pos = try_read_blank_line
115
+ if blank_pos
116
+ @pos = blank_pos
117
+ @line_nr += 1
118
+ @col_nr = 0
119
+ pending_blanks += 1
120
+ else
121
+ sub_indentation = detect_indentation
122
+ break if sub_indentation < indentation + 1
123
+
124
+ read_indentation(indentation + 1)
125
+ if try_read_block_start
126
+ res.children << read_block_with_children(indentation + 1)
127
+ else
128
+ res.children << "\n" unless res.children.empty?
129
+ pending_blanks.times { res.children << "\n" }
130
+ pending_blanks = 0
131
+
132
+ res.children.concat(read_inline_content)
133
+ read_end_of_inline_content
134
+ end
135
+ end
136
+ end
137
+
138
+ res
139
+ end
140
+
141
+ def try_read_blank_line
142
+ pos = @pos
143
+
144
+ loop do
145
+ case peek_char(pos)
146
+ when ' '
147
+ pos += 1
148
+ when nil
149
+ break pos + 1
150
+ when "\n"
151
+ break pos + 1
152
+ else
153
+ break nil
154
+ end
155
+ end
156
+ end
157
+
158
+ # FIXME: ugly and duplicated
159
+ def try_read_block_start
160
+ old_pos = @pos
161
+
162
+ success =
163
+ if try_read_identifier_head
164
+ if try_read_identifier_tail
165
+ case peek_char
166
+ when '['
167
+ true
168
+ when '.'
169
+ advance
170
+ [' ', "\n", nil].include?(peek_char)
171
+ end
172
+ end
173
+ end
174
+
175
+ @pos = old_pos
176
+ success
177
+ end
178
+
179
+ # FIXME: ugly and duplicated
180
+ def try_read_identifier_head
181
+ char = peek_char
182
+ case char
183
+ when 'a'..'z'
184
+ advance
185
+ char
186
+ end
187
+ end
188
+
189
+ # FIXME: ugly and duplicated
190
+ def try_read_identifier_tail
191
+ res = ''
192
+
193
+ loop do
194
+ char = peek_char
195
+ case char
196
+ when 'a'..'z', '-', '0'..'9'
197
+ advance
198
+ res << char
199
+ else
200
+ break
201
+ end
202
+ end
203
+
204
+ res.to_s
205
+ end
206
+
207
+ def detect_indentation
208
+ indentation_chars = 0
209
+ pos = @pos
210
+
211
+ loop do
212
+ case peek_char(pos)
213
+ when ' '
214
+ pos += 1
215
+ indentation_chars += 1
216
+ else
217
+ break
218
+ end
219
+ end
220
+
221
+ indentation_chars / 2
222
+ end
223
+
224
+ def read_until_eol_or_eof
225
+ res = ''
226
+
227
+ loop do
228
+ char = peek_char
229
+ case char
230
+ when "\n"
231
+ advance
232
+ break
233
+ when nil
234
+ break
235
+ else
236
+ advance
237
+ res << char
238
+ end
239
+ end
240
+
241
+ res.to_s
242
+ end
243
+
244
+ def read_indentation(indentation)
245
+ indentation.times do
246
+ read_char(' ')
247
+ read_char(' ')
248
+ end
249
+ end
250
+
251
+ def read_single_block
252
+ identifier = read_identifier
253
+
254
+ attributes =
255
+ if peek_char == '['
256
+ read_attributes
257
+ else
258
+ {}
259
+ end
260
+
261
+ read_char('.')
262
+
263
+ case peek_char
264
+ when nil, "\n"
265
+ advance
266
+ ElementNode.new(identifier, attributes, [])
267
+ else
268
+ read_char(' ')
269
+ content = read_inline_content
270
+ read_end_of_inline_content
271
+ ElementNode.new(identifier, attributes, content)
272
+ end
273
+ end
274
+
275
+ def read_end_of_inline_content
276
+ char = peek_char
277
+ case char
278
+ when "\n", nil
279
+ advance
280
+ when '}'
281
+ raise_parse_error('unexpected } -- try escaping it as "%}"')
282
+ else
283
+ raise_parse_error('unexpected content')
284
+ end
285
+ end
286
+
287
+ def read_identifier
288
+ a = read_identifier_head
289
+ b = read_identifier_tail
290
+ "#{a}#{b}"
291
+ end
292
+
293
+ def read_identifier_head
294
+ char = peek_char
295
+ case char
296
+ when 'a'..'z'
297
+ advance
298
+ char
299
+ else
300
+ raise_parse_error("expected an identifier, but got #{char.inspect}")
301
+ end
302
+ end
303
+
304
+ def read_identifier_tail
305
+ res = ''
306
+
307
+ loop do
308
+ char = peek_char
309
+ case char
310
+ when 'a'..'z', '-', '0'..'9'
311
+ advance
312
+ res << char
313
+ else
314
+ break
315
+ end
316
+ end
317
+
318
+ res.to_s
319
+ end
320
+
321
+ def read_attributes
322
+ read_char('[')
323
+
324
+ res = {}
325
+
326
+ at_start = true
327
+ loop do
328
+ char = peek_char
329
+ case char
330
+ when ']'
331
+ advance
332
+ break
333
+ else
334
+ read_char(',') unless at_start
335
+
336
+ key = read_attribute_key
337
+ if peek_char == '='
338
+ read_char('=')
339
+ value = read_attribute_value
340
+ else
341
+ value = key
342
+ end
343
+
344
+ res[key] = value
345
+
346
+ at_start = false
347
+ end
348
+ end
349
+
350
+ res
351
+ end
352
+
353
+ def read_attribute_key
354
+ read_identifier
355
+ end
356
+
357
+ def read_attribute_value
358
+ res = ''
359
+
360
+ is_escaping = false
361
+ loop do
362
+ char = peek_char
363
+
364
+ if is_escaping
365
+ case char
366
+ when nil, "\n"
367
+ break
368
+ else
369
+ advance
370
+ res << char
371
+ is_escaping = false
372
+ end
373
+ else
374
+ case char
375
+ when nil, "\n", ']', ','
376
+ break
377
+ when '%'
378
+ advance
379
+ is_escaping = true
380
+ else
381
+ advance
382
+ res << char
383
+ end
384
+ end
385
+ end
386
+
387
+ res.to_s
388
+ end
389
+
390
+ def read_inline_content
391
+ res = []
392
+
393
+ loop do
394
+ char = peek_char
395
+ case char
396
+ when "\n", nil
397
+ break
398
+ when '}'
399
+ break
400
+ when '%'
401
+ advance
402
+ res << read_percent_body
403
+ else
404
+ res << read_string
405
+ end
406
+ end
407
+
408
+ res
409
+ end
410
+
411
+ def read_string
412
+ res = ''
413
+
414
+ loop do
415
+ char = peek_char
416
+ case char
417
+ when nil, "\n", '%', '}'
418
+ break
419
+ else
420
+ advance
421
+ res << char
422
+ end
423
+ end
424
+
425
+ res.to_s
426
+ end
427
+
428
+ def read_percent_body
429
+ char = peek_char
430
+ case char
431
+ when '%', '}'
432
+ advance
433
+ char.to_s
434
+ when nil, "\n"
435
+ raise_parse_error("expected something after %")
436
+ else
437
+ read_inline_element
438
+ end
439
+ end
440
+
441
+ def read_inline_element
442
+ name = read_identifier
443
+ attributes =
444
+ if peek_char == '['
445
+ read_attributes
446
+ else
447
+ {}
448
+ end
449
+ read_char('{')
450
+ contents = read_inline_content
451
+ read_char('}')
452
+
453
+ ElementNode.new(name, attributes, contents)
454
+ end
455
+
456
+ def raise_parse_error(msg)
457
+ raise ParserError.new(@line_nr, @col_nr, msg)
458
+ end
459
+ end
460
+ end