brandish 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +41 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +9 -0
- data/bin/brandish +16 -0
- data/brandish.gemspec +39 -0
- data/defaults/templates/html.liquid +39 -0
- data/lib/brandish.rb +51 -0
- data/lib/brandish/application.rb +163 -0
- data/lib/brandish/application/bench_command.rb +96 -0
- data/lib/brandish/application/build_command.rb +73 -0
- data/lib/brandish/application/initialize_command.rb +83 -0
- data/lib/brandish/application/serve_command.rb +150 -0
- data/lib/brandish/configure.rb +196 -0
- data/lib/brandish/configure/dsl.rb +135 -0
- data/lib/brandish/configure/dsl/form.rb +136 -0
- data/lib/brandish/configure/form.rb +32 -0
- data/lib/brandish/errors.rb +65 -0
- data/lib/brandish/execute.rb +26 -0
- data/lib/brandish/markup.rb +10 -0
- data/lib/brandish/markup/redcarpet.rb +14 -0
- data/lib/brandish/markup/redcarpet/format.rb +127 -0
- data/lib/brandish/markup/redcarpet/html.rb +95 -0
- data/lib/brandish/parser.rb +26 -0
- data/lib/brandish/parser/main.rb +237 -0
- data/lib/brandish/parser/node.rb +89 -0
- data/lib/brandish/parser/node/block.rb +98 -0
- data/lib/brandish/parser/node/command.rb +102 -0
- data/lib/brandish/parser/node/pair.rb +42 -0
- data/lib/brandish/parser/node/root.rb +83 -0
- data/lib/brandish/parser/node/string.rb +18 -0
- data/lib/brandish/parser/node/text.rb +114 -0
- data/lib/brandish/path_set.rb +163 -0
- data/lib/brandish/processor.rb +47 -0
- data/lib/brandish/processor/base.rb +144 -0
- data/lib/brandish/processor/block.rb +47 -0
- data/lib/brandish/processor/command.rb +47 -0
- data/lib/brandish/processor/context.rb +169 -0
- data/lib/brandish/processor/descend.rb +32 -0
- data/lib/brandish/processor/inline.rb +49 -0
- data/lib/brandish/processor/name_filter.rb +67 -0
- data/lib/brandish/processor/pair_filter.rb +96 -0
- data/lib/brandish/processors.rb +26 -0
- data/lib/brandish/processors/all.rb +19 -0
- data/lib/brandish/processors/all/comment.rb +29 -0
- data/lib/brandish/processors/all/embed.rb +56 -0
- data/lib/brandish/processors/all/if.rb +109 -0
- data/lib/brandish/processors/all/import.rb +95 -0
- data/lib/brandish/processors/all/literal.rb +42 -0
- data/lib/brandish/processors/all/verify.rb +47 -0
- data/lib/brandish/processors/common.rb +20 -0
- data/lib/brandish/processors/common/asset.rb +118 -0
- data/lib/brandish/processors/common/asset/paths.rb +93 -0
- data/lib/brandish/processors/common/group.rb +67 -0
- data/lib/brandish/processors/common/header.rb +86 -0
- data/lib/brandish/processors/common/markup.rb +127 -0
- data/lib/brandish/processors/common/output.rb +73 -0
- data/lib/brandish/processors/html.rb +18 -0
- data/lib/brandish/processors/html/group.rb +33 -0
- data/lib/brandish/processors/html/header.rb +46 -0
- data/lib/brandish/processors/html/markup.rb +131 -0
- data/lib/brandish/processors/html/output.rb +62 -0
- data/lib/brandish/processors/html/output/document.rb +127 -0
- data/lib/brandish/processors/html/script.rb +64 -0
- data/lib/brandish/processors/html/script/babel.rb +48 -0
- data/lib/brandish/processors/html/script/coffee.rb +47 -0
- data/lib/brandish/processors/html/script/vanilla.rb +45 -0
- data/lib/brandish/processors/html/style.rb +82 -0
- data/lib/brandish/processors/html/style/highlight.rb +89 -0
- data/lib/brandish/processors/html/style/sass.rb +64 -0
- data/lib/brandish/processors/html/style/vanilla.rb +71 -0
- data/lib/brandish/processors/latex.rb +15 -0
- data/lib/brandish/processors/latex/markup.rb +47 -0
- data/lib/brandish/scanner.rb +64 -0
- data/lib/brandish/version.rb +9 -0
- data/templates/initialize/Gemfile.tt +14 -0
- data/templates/initialize/brandish.config.rb.tt +49 -0
- metadata +296 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "hanami/helpers/html_helper"
|
|
5
|
+
require "hanami/helpers/escape_helper"
|
|
6
|
+
|
|
7
|
+
module Brandish
|
|
8
|
+
module Markup
|
|
9
|
+
module Redcarpet
|
|
10
|
+
# An HTML renderer for redcarpet. This provides integrations with
|
|
11
|
+
# Brandish, as well as code highlighting.
|
|
12
|
+
class HTML < ::Redcarpet::Render::SmartyHTML
|
|
13
|
+
include Hanami::Helpers::HtmlHelper
|
|
14
|
+
include Hanami::Helpers::EscapeHelper
|
|
15
|
+
|
|
16
|
+
# The tags for each level of header.
|
|
17
|
+
#
|
|
18
|
+
# @return [<::Symbol>]
|
|
19
|
+
TAGS = %i(h1 h2 h3 h4 h5 h6).freeze
|
|
20
|
+
|
|
21
|
+
# Initialize the renderer with the given context and options.
|
|
22
|
+
#
|
|
23
|
+
# @param context [Processor::Context]
|
|
24
|
+
# @param options [{::Symbol => ::Object}]
|
|
25
|
+
# @option options [::Symbol] :highlight (:none) Which highlighter to
|
|
26
|
+
# use. Possible values are `:rouge`, `:coderay`, `:pygments`, and
|
|
27
|
+
# `:none`.
|
|
28
|
+
def initialize(context, options)
|
|
29
|
+
@context = context
|
|
30
|
+
@highlighter = options.fetch(:highlight)
|
|
31
|
+
super(options)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Creates a header with the given text and level. If the text ends in
|
|
35
|
+
# `/#([\w-]+)/`, that is removed, and used as the ID for the header;
|
|
36
|
+
# otherwise, it is automagically assumed from the text.
|
|
37
|
+
#
|
|
38
|
+
# @param text [::String]
|
|
39
|
+
# @param level [::Numeric]
|
|
40
|
+
# @return [::String]
|
|
41
|
+
def header(text, level)
|
|
42
|
+
text, id = split_text(text)
|
|
43
|
+
|
|
44
|
+
html.tag(TAGS.fetch(level), raw(text), id: id).to_s
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Highlights a block of code.
|
|
48
|
+
#
|
|
49
|
+
# @param code [::String] The code to highlight
|
|
50
|
+
# @param language [::String, nil] The language that the code was in,
|
|
51
|
+
# or nil if it was not or could not be provided.
|
|
52
|
+
# @return [::String]
|
|
53
|
+
def block_code(code, language)
|
|
54
|
+
basic_language = language || "unknown"
|
|
55
|
+
case @highlighter
|
|
56
|
+
when :rouge then rouge_highlight_code(code, language)
|
|
57
|
+
when :coderay then coderay_highlight_code(code, language)
|
|
58
|
+
when :pygments then pygments_highlight_code(code, language)
|
|
59
|
+
when :none
|
|
60
|
+
html.pre { html.code(code, class: "language-#{basic_language}") }
|
|
61
|
+
end.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def split_text(text)
|
|
67
|
+
# So we don't match entity tags by accident (`�`).
|
|
68
|
+
if (match = text.match(/\A(.*)(?<!&)#([\w-]+)\s*\z/))
|
|
69
|
+
[match[1], match[2]]
|
|
70
|
+
else
|
|
71
|
+
# Remove entity tags, they're not needed.
|
|
72
|
+
[text, text.downcase.gsub(/&(.+?);/, "").gsub(/\W+/, "-")]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def rouge_highlight_code(code, language)
|
|
77
|
+
lexer = (language && Rouge::Lexer.find_fancy(language, code)) ||
|
|
78
|
+
Rouge::Lexers::PlainText
|
|
79
|
+
attributes = { class: "highlight language-#{lexer.tag}" }
|
|
80
|
+
html.pre do
|
|
81
|
+
code(raw(::Rouge.highlight(code, lexer, "html")), attributes)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def coderay_highlight_code(code, langauge)
|
|
86
|
+
CodeRay.scan(code, langauge.intern, css: :style).div
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def pygments_highlight_code(code, language)
|
|
90
|
+
Pygments.highlight(code, lexer: language.intern)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "yoga"
|
|
5
|
+
require "brandish/parser/main"
|
|
6
|
+
require "brandish/parser/node"
|
|
7
|
+
|
|
8
|
+
module Brandish
|
|
9
|
+
# Parses the document into a tree of nodes. This is strictly an LL(0)
|
|
10
|
+
# recursive descent parser - even though the term `peek` is used, this is
|
|
11
|
+
# technically a shifted token that is acted upon.
|
|
12
|
+
#
|
|
13
|
+
# This constructs a tree that can then be walked or modified to construct
|
|
14
|
+
# an end product.
|
|
15
|
+
class Parser
|
|
16
|
+
include Yoga::Parser
|
|
17
|
+
include Parser::Main
|
|
18
|
+
|
|
19
|
+
def initialize(*)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
undef_method :peek_out
|
|
24
|
+
undef_method :peek_out?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Brandish
|
|
5
|
+
class Parser
|
|
6
|
+
# The main parser for the document. This constructs a tree
|
|
7
|
+
# of nodes that can be used to represent the original
|
|
8
|
+
# document.
|
|
9
|
+
module Main
|
|
10
|
+
# Parses the overall document. This parses a sequence of
|
|
11
|
+
# elements until it reaches the `EOF` token.
|
|
12
|
+
#
|
|
13
|
+
# @return [Parser::Node]
|
|
14
|
+
def parse_document
|
|
15
|
+
body = collect(EOF_SYMBOL) { parse_document_element }
|
|
16
|
+
expect(EOF_SYMBOL)
|
|
17
|
+
Node::Root.new(children: body)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
alias_method :parse_root, :parse_document
|
|
21
|
+
|
|
22
|
+
# Parses a single element in the docuemnt. If the next token is a
|
|
23
|
+
# less-than sign, it calls {#parse_document_meta}; otherwise, it calls
|
|
24
|
+
# {#parse_document_text}.
|
|
25
|
+
#
|
|
26
|
+
# @return [Node] The node for the element.
|
|
27
|
+
def parse_document_element
|
|
28
|
+
peek?(LESS_THAN_SYMBOL) ? parse_document_meta : parse_document_text
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# parses a "meta" element from the document. In this sense, it is
|
|
32
|
+
# anything that doesn't correspond 1-to-1 to the destination document.
|
|
33
|
+
# If the next token after the less-than sign is an at-sign, it calls
|
|
34
|
+
# {#parse_document_command}; otherwise, it calls {#parse_document_block}.
|
|
35
|
+
#
|
|
36
|
+
# @return [Node]
|
|
37
|
+
def parse_document_meta(start = expect(LESS_THAN_SYMBOL))
|
|
38
|
+
name = expect(TEXT_SYMBOL)
|
|
39
|
+
parse_skip_space
|
|
40
|
+
arguments = collect(SLASH_OR_GREATER_THAN_SYMBOL) { parse_document_command_argument }
|
|
41
|
+
if peek?(SLASH_SYMBOL)
|
|
42
|
+
parse_document_command(start, name, arguments)
|
|
43
|
+
else
|
|
44
|
+
parse_document_block(start, name, arguments)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Parses a document command. This is essentially a message to the
|
|
49
|
+
# compiler about the document, to alter how the document is processed.
|
|
50
|
+
#
|
|
51
|
+
# command ::= '<' TEXT *command-argument '/' '>'
|
|
52
|
+
#
|
|
53
|
+
# @param start [Scanner::Token] The starting element for the command.
|
|
54
|
+
# @param name [Scanner::Token] The name of the command.
|
|
55
|
+
# @param arguments [<Node::Pair>] The arguments to the command.
|
|
56
|
+
# @return [Node::Command] The command node.
|
|
57
|
+
def parse_document_command(start, name, arguments)
|
|
58
|
+
expect(SLASH_SYMBOL)
|
|
59
|
+
stop = expect(GREATER_THAN_SYMBOL)
|
|
60
|
+
|
|
61
|
+
Node::Command.new(name: name, arguments: arguments,
|
|
62
|
+
location: start.location.union(stop.location))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Parses a document command argument. This is an argument to a command.
|
|
66
|
+
# All arguments are key-value pairs.
|
|
67
|
+
#
|
|
68
|
+
# command-argument ::= *space command-argument-key *space '=' *space command-argument-value *space
|
|
69
|
+
#
|
|
70
|
+
# @return [Node::Pair] The command argument.
|
|
71
|
+
def parse_document_command_argument
|
|
72
|
+
parse_skip_space
|
|
73
|
+
key = parse_document_command_argument_key
|
|
74
|
+
parse_skip_space
|
|
75
|
+
expect(EQUAL_SYMBOL)
|
|
76
|
+
parse_skip_space
|
|
77
|
+
value = parse_document_command_argument_value
|
|
78
|
+
parse_skip_space
|
|
79
|
+
|
|
80
|
+
Node::Pair.new(key: key, value: value)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The key for the command argument.
|
|
84
|
+
#
|
|
85
|
+
# command-argument-key ::= TEXT
|
|
86
|
+
#
|
|
87
|
+
# @return [Scanner::Token]
|
|
88
|
+
def parse_document_command_argument_key
|
|
89
|
+
expect(TEXT_SYMBOL)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The value for the command argument. This can be either a string, or
|
|
93
|
+
# a {Node::Text} with a single token. If it's a string, it's parsed with
|
|
94
|
+
# {#parse_document_string}; otherwise, it grabs one token that's valid
|
|
95
|
+
# for a {Node::Text} node.
|
|
96
|
+
#
|
|
97
|
+
# command-argument-value ::= string / TEXT
|
|
98
|
+
#
|
|
99
|
+
# @return [Node]
|
|
100
|
+
def parse_document_command_argument_value
|
|
101
|
+
if peek?(QUOTE_SYMBOL)
|
|
102
|
+
parse_document_string
|
|
103
|
+
else
|
|
104
|
+
Node::Text.new(tokens: [expect(Node::Text::TOKENS)])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Parses a "block." This is similar to a HTML tag in the sense that it
|
|
109
|
+
# has a name and a body; however, blocks do not have any sort of
|
|
110
|
+
# arguments to them.
|
|
111
|
+
#
|
|
112
|
+
# block ::= '<' TEXT *command-argument '>' *element '</' TEXT '>'
|
|
113
|
+
#
|
|
114
|
+
# @param start [Scanner::Token] The starting element for the block.
|
|
115
|
+
# @param name [Scanner::Token] The name of the block.
|
|
116
|
+
# @param arguments [<Node::Pair>] The arguments to the block.
|
|
117
|
+
# @return [Node::Block]
|
|
118
|
+
def parse_document_block(start, name, arguments)
|
|
119
|
+
expect(GREATER_THAN_SYMBOL)
|
|
120
|
+
body = parse_document_block_body
|
|
121
|
+
expect(SLASH_SYMBOL)
|
|
122
|
+
match = expect(TEXT_SYMBOL)
|
|
123
|
+
stop = expect(GREATER_THAN_SYMBOL)
|
|
124
|
+
|
|
125
|
+
unless name.value == match.value
|
|
126
|
+
fail ParseError.new("Unexpected #{match.value.inspect}, expected" \
|
|
127
|
+
" #{name.value.inspect}", match.location)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
Node::Block.new(name: name, body: body, arguments: arguments,
|
|
131
|
+
location: start.location.union(body.location, match.location,
|
|
132
|
+
stop.location, *arguments.map(&:location)))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Parses the body of a block tag. This keeps attempting to parse text
|
|
136
|
+
# and meta tags until it encounters the phrase `</`, at which point it
|
|
137
|
+
# will stop parsing and return to the parent.
|
|
138
|
+
#
|
|
139
|
+
# @return [Node::Root]
|
|
140
|
+
def parse_document_block_body
|
|
141
|
+
children = []
|
|
142
|
+
loop do
|
|
143
|
+
if peek?(LESS_THAN_SYMBOL)
|
|
144
|
+
start = expect(LESS_THAN_SYMBOL)
|
|
145
|
+
break if peek?(SLASH_SYMBOL)
|
|
146
|
+
children << parse_document_meta(start)
|
|
147
|
+
else
|
|
148
|
+
children << parse_document_text
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
Node::Root.new(children: children)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Parses a "string." This is a series of text encapulated by quotes.
|
|
156
|
+
# Strings can contain more characters than just regular text, but right
|
|
157
|
+
# now, strings are only used for command argument values.
|
|
158
|
+
#
|
|
159
|
+
# string ::= '"' *(TEXT / SPACE / LINE / NUMERIC / ESCAPE / '<' / '>' / '=') '"'
|
|
160
|
+
#
|
|
161
|
+
# @return [Node::String]
|
|
162
|
+
def parse_document_string
|
|
163
|
+
start = expect(QUOTE_SYMBOL)
|
|
164
|
+
children = collect(QUOTE_SYMBOL) { expect(Node::String::TOKENS) }
|
|
165
|
+
stop = expect(QUOTE_SYMBOL)
|
|
166
|
+
location = start.location.union(stop.location)
|
|
167
|
+
|
|
168
|
+
Node::String.new(tokens: children, location: location)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Parses a document for text. This is just regular text tokens. For a
|
|
172
|
+
# list of tokens that are allowed, see {Node::Text::TOKENS}.
|
|
173
|
+
#
|
|
174
|
+
# text ::= *(TEXT / SPACE / LINE / NUMERIC / ESCAPE / '/' / '"' / '=')
|
|
175
|
+
#
|
|
176
|
+
# @return [Node::Text]
|
|
177
|
+
def parse_document_text
|
|
178
|
+
children = [expect(Node::Text::TOKENS)]
|
|
179
|
+
children << expect(Node::Text::TOKENS) while peek?(Node::Text::TOKENS)
|
|
180
|
+
Node::Text.new(tokens: children)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Skips over nodes as long as they're space nodes.
|
|
184
|
+
#
|
|
185
|
+
# @return [void]
|
|
186
|
+
def parse_skip_space
|
|
187
|
+
expect(SPACE_SYMBOLS) while peek?(SPACE_SYMBOLS)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# A set containing the kind symbol for a quote.
|
|
191
|
+
#
|
|
192
|
+
# @return [::Set<::Symbol>]
|
|
193
|
+
QUOTE_SYMBOL = ::Set[:'"']
|
|
194
|
+
|
|
195
|
+
# A set containing the kind symbol for an equal sign.
|
|
196
|
+
#
|
|
197
|
+
# @return [::Set<::Symbol>]
|
|
198
|
+
EQUAL_SYMBOL = ::Set[:'=']
|
|
199
|
+
|
|
200
|
+
# A set containing the kind symbol for a less than symbol.
|
|
201
|
+
#
|
|
202
|
+
# @return [::Set<::Symbol>]
|
|
203
|
+
LESS_THAN_SYMBOL = ::Set[:<]
|
|
204
|
+
|
|
205
|
+
# A set containing the kind symbol for a greater than symbol.
|
|
206
|
+
#
|
|
207
|
+
# @return [::Set<::Symbol>]
|
|
208
|
+
GREATER_THAN_SYMBOL = ::Set[:>]
|
|
209
|
+
|
|
210
|
+
# A set containing the kind symbol for a forward slash symbol.
|
|
211
|
+
#
|
|
212
|
+
# @return [::Set<::Symbol>]
|
|
213
|
+
SLASH_SYMBOL = ::Set[:/]
|
|
214
|
+
|
|
215
|
+
# A set containing the kind symbols for a forward slash or a greater than
|
|
216
|
+
# symbol.
|
|
217
|
+
#
|
|
218
|
+
# @return [::Set<::Symbol>]
|
|
219
|
+
SLASH_OR_GREATER_THAN_SYMBOL = SLASH_SYMBOL | GREATER_THAN_SYMBOL
|
|
220
|
+
|
|
221
|
+
# A set containing the kind symbols for a text symbol.
|
|
222
|
+
#
|
|
223
|
+
# @return [::Set<::Symbol>]
|
|
224
|
+
TEXT_SYMBOL = ::Set[:TEXT]
|
|
225
|
+
|
|
226
|
+
# A set containing the kind symbols for a space symbol.
|
|
227
|
+
#
|
|
228
|
+
# @return [::Set<::Symbol>]
|
|
229
|
+
SPACE_SYMBOLS = ::Set[:SPACE, :LINE]
|
|
230
|
+
|
|
231
|
+
# A set containing the kind symbols for a eof symbol.
|
|
232
|
+
#
|
|
233
|
+
# @return [::Set<::Symbol>]
|
|
234
|
+
EOF_SYMBOL = ::Set[:EOF]
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "brandish/parser/node/block"
|
|
5
|
+
require "brandish/parser/node/command"
|
|
6
|
+
require "brandish/parser/node/pair"
|
|
7
|
+
require "brandish/parser/node/root"
|
|
8
|
+
require "brandish/parser/node/text"
|
|
9
|
+
require "brandish/parser/node/string"
|
|
10
|
+
|
|
11
|
+
module Brandish
|
|
12
|
+
class Parser
|
|
13
|
+
# A parser node. This is the base for all parser nodes.b All parser
|
|
14
|
+
# nodes are frozen, and so cannot be mutated; all mutations need to be
|
|
15
|
+
# set up as a new node, that is then returned.
|
|
16
|
+
class Node
|
|
17
|
+
# The location of the node.
|
|
18
|
+
#
|
|
19
|
+
# @return [Location]
|
|
20
|
+
attr_reader :location
|
|
21
|
+
|
|
22
|
+
# Initialize the node with the given location. This should be
|
|
23
|
+
# overwritten by a decending node, but all decending nodes should
|
|
24
|
+
# include a location.
|
|
25
|
+
#
|
|
26
|
+
# @param location [Location] The location. See
|
|
27
|
+
# {#location}.
|
|
28
|
+
def initialize(location:)
|
|
29
|
+
@location = location
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Pretty inspect.
|
|
33
|
+
#
|
|
34
|
+
# @return [::String]
|
|
35
|
+
def inspect
|
|
36
|
+
"#<#{self.class} location=#{@location}>"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# "Updates" the node with the given attributes. All of the key-value
|
|
40
|
+
# pairs in the attributes are updated on the node by sending an update
|
|
41
|
+
# to the node.
|
|
42
|
+
#
|
|
43
|
+
# @example
|
|
44
|
+
# node.update(some: true, value: true)
|
|
45
|
+
# # this sends `update_some` and `update_value`, each with `true` as
|
|
46
|
+
# # the argument. These functions return a node similar to the
|
|
47
|
+
# # original, but with the requested change. This is functionally
|
|
48
|
+
# # similar to this (with some minor differences):
|
|
49
|
+
# node.update_some(true).update_value(true)
|
|
50
|
+
# @raise [NodeError] if an attribute key is passed that isn't updatable
|
|
51
|
+
# for the node.
|
|
52
|
+
# @param attributes [{::Symbol, ::String => ::Object}] The attributes to
|
|
53
|
+
# update, and the new values for them.
|
|
54
|
+
# @return [Node] The updated node, or `self` if no attributes are given.
|
|
55
|
+
def update(attributes)
|
|
56
|
+
attributes.inject(self) { |a, (n, v)| a.update_attribute(n, v) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Prevents all calls to {#update}. This is used on nodes that should
|
|
60
|
+
# never be updated. This is a stop-gap measure for incorrectly
|
|
61
|
+
# configured projects. This, in line with all other methods, creates
|
|
62
|
+
# a duplicate node.
|
|
63
|
+
#
|
|
64
|
+
# @return [Node]
|
|
65
|
+
def prevent_update
|
|
66
|
+
node = dup
|
|
67
|
+
node.singleton_class.send(:undef_method, :update)
|
|
68
|
+
node
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def update_prevented?
|
|
72
|
+
!respond_to?(:update)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
protected
|
|
76
|
+
|
|
77
|
+
# Updates an attribute on the node. This is used for {#update}.
|
|
78
|
+
#
|
|
79
|
+
# @param name [::Symbol, ::String] The name of the attribute.
|
|
80
|
+
# @param value [::Object] The value of the attribute.
|
|
81
|
+
def update_attribute(name, value)
|
|
82
|
+
return send(:"update_#{name}", value) if respond_to?(:"update_#{name}", true)
|
|
83
|
+
|
|
84
|
+
fail NodeError.new("Unable to update attribute #{name} on #{self}",
|
|
85
|
+
@location)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|