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 +4 -4
- data/Gemfile +4 -7
- data/Gemfile.lock +16 -12
- data/Guardfile +3 -0
- data/NEWS.md +11 -3
- data/README.adoc +218 -0
- data/Rakefile +13 -2
- data/d-mark.gemspec +5 -4
- data/lib/d-mark.rb +2 -0
- data/lib/d-mark/cli.rb +28 -0
- data/lib/d-mark/parser.rb +460 -0
- data/lib/{dmark → d-mark}/translator.rb +5 -3
- data/lib/d-mark/version.rb +3 -0
- data/samples/identifiers-and-patterns.dmark +418 -1
- data/samples/trivial.dmark +1 -0
- data/samples/trivial.rb +20 -0
- data/spec/d-mark/parser_spec.rb +271 -0
- data/spec/spec_helper.rb +2 -0
- metadata +30 -18
- data/README.md +0 -70
- data/lib/dmark.rb +0 -9
- data/lib/dmark/lexer.rb +0 -235
- data/lib/dmark/nodes.rb +0 -76
- data/lib/dmark/parser.rb +0 -28
- data/lib/dmark/tokens.rb +0 -49
- data/lib/dmark/version.rb +0 -3
- data/samples/identifiers-and-patterns.html +0 -59
- data/scripts/translate-to-html.rb +0 -46
- data/tasks/doc.rake +0 -13
- data/tasks/rubocop.rake +0 -6
- data/tasks/test.rake +0 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bee205ce9623266e9787eec60d991ddc1f3b99a3
|
4
|
+
data.tar.gz: 490bbf23b2a0a0b8cb1767f6b7a542f38a749ba1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 13cabb5e530a7067fe5e665c883d42f1bf59208249e2fd2ef9fe49cf87cfa0dffa1974be4307769dbb862d51ba466403154e6bbbbf655a244a275d760b26bacd
|
7
|
+
data.tar.gz: e65a20361865f63a70cf4de93fa27634dcf831ac4d966c64ccb146dabd03796e277b23fdb43dd7ebeef9fdde947f2c1d800e2afeb5710ebf79ec4d9231608bc9
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
61
|
-
parser (>= 2.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
|
-
|
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
|
-
|
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
|
data/Guardfile
ADDED
data/NEWS.md
CHANGED
data/README.adoc
ADDED
@@ -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
|
-
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require 'rubocop/rake_task'
|
2
3
|
|
3
|
-
|
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]
|
data/d-mark.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require_relative 'lib/
|
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['{
|
16
|
+
Dir['{lib,spec,samples}/**/*'] +
|
17
17
|
['d-mark.gemspec']
|
18
18
|
s.require_paths = ['lib']
|
19
19
|
|
20
|
-
s.rdoc_options = ['--main', 'README.
|
21
|
-
s.extra_rdoc_files = ['LICENSE', 'README.
|
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
|
data/lib/d-mark.rb
ADDED
data/lib/d-mark/cli.rb
ADDED
@@ -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
|