glyph 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. data/AUTHORS.textile +1 -1
  2. data/CHANGELOG.textile +119 -222
  3. data/LICENSE.textile +1 -1
  4. data/README.textile +42 -23
  5. data/Rakefile +1 -3
  6. data/VERSION +1 -1
  7. data/benchmark.rb +72 -0
  8. data/book/config.yml +4 -4
  9. data/book/document.glyph +90 -57
  10. data/book/images/document_generation.png +0 -0
  11. data/book/lib/macros/reference.rb +75 -22
  12. data/book/output/html/glyph.html +3183 -2121
  13. data/book/output/html/images/document_generation.png +0 -0
  14. data/book/output/pdf/glyph.pdf +7370 -4913
  15. data/book/resources/document_generation.txt +34 -0
  16. data/book/snippets.yml +6 -0
  17. data/book/text/changelog.glyph +45 -34
  18. data/book/text/compiling/compiling.glyph +23 -0
  19. data/book/text/compiling/lite_mode.glyph +23 -0
  20. data/book/text/compiling/programmatic_usage.glyph +77 -0
  21. data/book/text/extending/bookmarks_headers.glyph +21 -0
  22. data/book/text/extending/further_reading.glyph +13 -0
  23. data/book/text/extending/internals.glyph +79 -0
  24. data/book/text/extending/interpreting.glyph +51 -0
  25. data/book/text/extending/macro_def.glyph +64 -0
  26. data/book/text/extending/params_attrs.glyph +70 -0
  27. data/book/text/extending/placeholders.glyph +34 -0
  28. data/book/text/extending/validators.glyph +16 -0
  29. data/book/text/getting_started/configuration.glyph +49 -0
  30. data/book/text/getting_started/create_project.glyph +41 -0
  31. data/book/text/getting_started/structure.glyph +55 -0
  32. data/book/text/introduction.glyph +49 -26
  33. data/book/text/license.glyph +1 -1
  34. data/book/text/macros/macros_block.glyph +99 -0
  35. data/book/text/macros/macros_core.glyph +208 -0
  36. data/book/text/macros/macros_filters.glyph +40 -0
  37. data/book/text/macros/macros_inline.glyph +50 -0
  38. data/book/text/macros/macros_structure.glyph +100 -0
  39. data/book/text/ref_commands.glyph +94 -73
  40. data/book/text/ref_config.glyph +34 -42
  41. data/book/text/ref_macros.glyph +1 -373
  42. data/book/text/text_editing/code.glyph +51 -0
  43. data/book/text/text_editing/conditionals.glyph +49 -0
  44. data/book/text/text_editing/evaluation.glyph +13 -0
  45. data/book/text/text_editing/glyph_files.glyph +7 -0
  46. data/book/text/text_editing/images.glyph +29 -0
  47. data/book/text/text_editing/inclusions.glyph +44 -0
  48. data/book/text/text_editing/links.glyph +53 -0
  49. data/book/text/text_editing/macro_intro.glyph +111 -0
  50. data/book/text/text_editing/raw_html.glyph +112 -0
  51. data/book/text/text_editing/sections.glyph +63 -0
  52. data/book/text/text_editing/stylesheets.glyph +36 -0
  53. data/book/text/troubleshooting/errors_command.glyph +39 -0
  54. data/book/text/troubleshooting/errors_generic.glyph +29 -0
  55. data/book/text/troubleshooting/errors_intro.glyph +3 -0
  56. data/book/text/troubleshooting/errors_macro.glyph +98 -0
  57. data/book/text/troubleshooting/errors_parser.glyph +29 -0
  58. data/config.yml +77 -58
  59. data/document.glyph +25 -25
  60. data/glyph.gemspec +57 -22
  61. data/lib/glyph.rb +54 -13
  62. data/lib/glyph/commands.rb +84 -17
  63. data/lib/glyph/config.rb +3 -3
  64. data/lib/glyph/document.rb +14 -8
  65. data/lib/glyph/interpreter.rb +18 -58
  66. data/lib/glyph/macro.rb +160 -55
  67. data/lib/glyph/macro_validators.rb +104 -12
  68. data/lib/glyph/node.rb +24 -0
  69. data/lib/glyph/parser.rb +278 -0
  70. data/lib/glyph/syntax_node.rb +225 -0
  71. data/macros/core.rb +212 -0
  72. data/macros/filters.rb +66 -15
  73. data/macros/html/block.rb +43 -105
  74. data/macros/html/inline.rb +11 -12
  75. data/macros/html/structure.rb +123 -58
  76. data/macros/xml.rb +33 -0
  77. data/spec/files/container.textile +2 -2
  78. data/spec/files/document.glyph +2 -2
  79. data/spec/files/document_with_toc.glyph +3 -3
  80. data/spec/files/included.textile +1 -1
  81. data/spec/files/ligature.jpg +0 -0
  82. data/spec/files/markdown.markdown +2 -1
  83. data/spec/lib/commands_spec.rb +46 -3
  84. data/spec/lib/document_spec.rb +4 -4
  85. data/spec/lib/glyph_spec.rb +17 -46
  86. data/spec/lib/interpreter_spec.rb +6 -25
  87. data/spec/lib/macro_spec.rb +141 -43
  88. data/spec/lib/macro_validators_spec.rb +27 -5
  89. data/spec/lib/node_spec.rb +26 -1
  90. data/spec/lib/parser_spec.rb +246 -0
  91. data/spec/lib/syntax_node_spec.rb +111 -0
  92. data/spec/macros/core_spec.rb +195 -0
  93. data/spec/macros/filters_spec.rb +38 -4
  94. data/spec/macros/macros_spec.rb +20 -176
  95. data/spec/macros/textile_spec.rb +13 -71
  96. data/spec/macros/xml_spec.rb +77 -0
  97. data/spec/spec_helper.rb +50 -10
  98. data/spec/tasks/load_spec.rb +13 -2
  99. data/styles/default.css +18 -6
  100. data/styles/pagination.css +1 -19
  101. data/tasks/generate.rake +2 -2
  102. data/tasks/load.rake +27 -17
  103. data/tasks/project.rake +1 -1
  104. metadata +75 -62
  105. data/book/script/compile.rb +0 -8
  106. data/book/script/prof +0 -1
  107. data/book/script/prof_results.htm +0 -21079
  108. data/book/text/authoring.glyph +0 -548
  109. data/book/text/extending.glyph +0 -224
  110. data/book/text/getting_started.glyph +0 -158
  111. data/book/text/troubleshooting.glyph +0 -179
  112. data/lib/glyph/glyph_language.rb +0 -538
  113. data/lib/glyph/glyph_language.treetop +0 -27
  114. data/macros/common.rb +0 -160
@@ -1,48 +1,140 @@
1
1
  module Glyph
2
2
  class Macro
3
3
 
4
+ # @since 0.2.0
4
5
  module Validators
5
6
 
6
7
  # Validates the macro according to the specified block
7
8
  # @param [String] message the message to display if the validation fails.
8
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
9
+ # @param [Hash] options a hash containing validation options
10
+ # @option options :level the error level (:error, :warning)
11
+ # @return [Boolean] whether the validation passed or not
9
12
  # @example
10
- # validate("Invalid macro value", :level => :error) {@value == 'valid'} # Raises an error in case of failure
11
- # validate("Invalid macro value", :level => :warning) {@value == 'valid'} # Displays a warning in case of failure
13
+ # validate("Invalid macro value", :level => :error) {value == 'valid'} # Raises an error in case of failure
14
+ # validate("Invalid macro value", :level => :warning) {value == 'valid'} # Displays a warning in case of failure
12
15
  def validate(message, options={:level => :error}, &block)
13
- unless instance_eval(&block) then
16
+ result = instance_eval(&block)
17
+ unless result then
14
18
  send("macro_#{options[:level]}".to_sym, message)
15
19
  end
20
+ result
21
+ end
22
+
23
+ # Ensures that the macro element attributes is a valid XML element name.
24
+ # @param [Hash] options a hash containing validation options (for now the only option is :level)
25
+ # @return [Boolean] whether the validation passed or not
26
+ # @since 0.3.0
27
+ def valid_xml_element(options={:level => :error})
28
+ validate("Invalid XML element '#{@node[:element]}'", options) { @node[:element].to_s.match(/^([^[:punct:]0-9<>]|_)[^<>"']*/) }
29
+ end
30
+
31
+ # Ensures that a macro attribute name is a valid XML attribute name.
32
+ # @param [String, Symbol] name the attribute name to validate
33
+ # @param [Hash] options a hash containing validation options
34
+ # @option options :level the error level (:error, :warning)
35
+ # @return [Boolean] whether the validation passed or not
36
+ # @since 0.3.0
37
+ def valid_xml_attribute(name, options={:level => :warning})
38
+ validate("Invalid XML attribute '#{name}'", options) { name.to_s.match(/^([^[:punct:]0-9<>]|_)[^<>"']*/) }
16
39
  end
17
40
 
18
41
  # Ensures that the macro receives up to _n_ parameters.
19
42
  # @param [Integer] n the maximum number of parameters allowed for the macro.
20
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
43
+ # @param [Hash] options a hash containing validation options
44
+ # @option options :level the error level (:error, :warning)
45
+ # @return [Boolean] whether the validation passed or not
21
46
  def max_parameters(n, options={:level=>:error})
22
- validate("Macro '#{@name}' takes up to #{n} parameter(s) (#{params.length} given)", options) { params.length <= n }
47
+ validate("Macro '#{@name}' takes up to #{n} parameter(s) (#{@node.params.length} given)", options) do
48
+ if n == 0 then
49
+ no_parameters options
50
+ else
51
+ @node.params.length <= n
52
+ end
53
+ end
23
54
  end
24
55
 
25
56
  # Ensures that the macro receives at least _n_ parameters.
26
57
  # @param [Integer] n the minimum number of parameters allowed for the macro.
27
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
58
+ # @param [Hash] options a hash containing validation options
59
+ # @option options :level the error level (:error, :warning)
60
+ # @return [Boolean] whether the validation passed or not
28
61
  def min_parameters(n, options={:level=>:error})
29
- validate("Macro '#{@name}' takes at least #{n} parameter(s) (#{params.length} given)", options) { params.length >= n }
62
+ validate("Macro '#{@name}' takes at least #{n} parameter(s) (#{@node.params.length} given)", options) { @node.params.length >= n }
30
63
  end
31
64
 
32
65
  # Ensures that the macro receives exactly _n_ parameters.
33
66
  # @param [Integer] n the number of parameters allowed for the macro.
34
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
67
+ # @param [Hash] options a hash containing validation options
68
+ # @option options :level the error level (:error, :warning)
69
+ # @return [Boolean] whether the validation passed or not
35
70
  def exact_parameters(n, options={:level=>:error})
36
- validate("Macro '#{@name}' takes exactly #{n} parameter(s) (#{params.length} given)", options) { params.length == n }
71
+ validate("Macro '#{@name}' takes exactly #{n} parameter(s) (#{@node.params.length} given)", options) do
72
+ if n == 0 then
73
+ no_parameters options
74
+ else
75
+ @node.params.length == n
76
+ end
77
+ end
37
78
  end
38
79
 
39
80
  # Ensures that the macro receives no parameters.
40
- # @param [Hash] options a hash containing validation options (for now the only option is :level)
81
+ # @param [Hash] options a hash containing validation options
82
+ # @option options :level the error level (:error, :warning)
83
+ # @return [Boolean] whether the validation passed or not
41
84
  def no_parameters(options={:level=>:error})
42
- validate("Macro '#{@name}' takes no parameters (#{params.length} given)", options) { params.length == 0 }
85
+ validate("Macro '#{@name}' takes no parameters (#{@node.params.length} given)", options) do
86
+ case @node.params.length
87
+ when 0 then
88
+ true
89
+ when 1 then
90
+ result = true
91
+ @node.param(0).children.each do |p|
92
+ result = p.is_a?(Glyph::TextNode) && p[:value].blank?
93
+ break unless result
94
+ end
95
+ result
96
+ else
97
+ false
98
+ end
99
+ end
100
+ end
101
+
102
+ # Raises a macro error if Glyph is running in safe mode.
103
+ # @raise [Glyph::MacroError] the macro cannot be used allowed in safe mode
104
+ # @since 0.3.0
105
+ def safety_check
106
+ macro_error "Macro '#@name' cannot be used in safe mode" if Glyph.safe?
43
107
  end
44
108
 
109
+ # Ensure that no mutual inclusion occurs within the specified parameter or attribute
110
+ # @param [Fixnum, Symbol] the parameter index or attribute name to check
111
+ # @raise [Glyph::MacroError] mutual inclusion was detected
112
+ # @since 0.3.0
113
+ def no_mutual_inclusion_in(arg)
114
+ check_type = arg.is_a?(Symbol) ? :attribute : :parameter
115
+ check_value = nil
116
+ found = @node.find_parent do |n|
117
+ if n.is_a?(Glyph::MacroNode) && Glyph::MACROS[n[:name]] == Glyph::MACROS[@name] then
118
+ case check_type
119
+ when :attribute then
120
+ check_value = n.children.select do |node|
121
+ node.is_a?(Glyph::AttributeNode) && node[:name] == arg
122
+ end[0][:value] rescue nil
123
+ check_value == attr(arg)
124
+ when :parameter then
125
+ check_value = n.children.select do |node|
126
+ node.is_a?(Glyph::ParameterNode) && node[:name] == :"#{arg}"
127
+ end[0][:value] rescue nil
128
+ check_value == param(arg)
129
+ end
130
+ end
131
+ end
132
+ if found then
133
+ macro_error "Mutual Inclusion in #{check_type}(#{arg}): '#{check_value}'", Glyph::MutualInclusionError
134
+ end
135
+ end
45
136
  end
46
137
 
47
138
  end
139
+
48
140
  end
data/lib/glyph/node.rb CHANGED
@@ -135,4 +135,28 @@ class Node < Hash
135
135
  ascend(parent) {|e| return e unless e.parent }
136
136
  end
137
137
 
138
+ # Converts self to a hash
139
+ # @return [Hash] the converted hash
140
+ # @since 0.3.0
141
+ def to_hash
142
+ {}.merge(self)
143
+ end
144
+
145
+ # @return [String] a textual representation of self
146
+ # @since 0.3.0
147
+ def inspect
148
+ string = ""
149
+ descend do |e, level|
150
+ string << " "*level+e.to_hash.inspect+"\n"
151
+ end
152
+ string.chomp
153
+ end
154
+
155
+ # @return (Boolean) true if the nodes are equal
156
+ # @since 0.3.0
157
+ def ==(node)
158
+ return false unless node.is_a? Node
159
+ self.to_hash == node.to_hash && self.children == node.children
160
+ end
161
+
138
162
  end
@@ -0,0 +1,278 @@
1
+ # encoding: utf-8
2
+ require 'strscan'
3
+
4
+ module Glyph
5
+
6
+ # The Glyph::Parser class can parse a string of text containing Glyph macros and produce the
7
+ # corresponding syntax tree.
8
+ # @since 0.3.0
9
+ class Parser
10
+
11
+ # Initializes the parser.
12
+ # @param [String] text the text to parse
13
+ # @param [String] source_name the name of the source file (stored in the root node)
14
+ # @since 0.3.0
15
+ def initialize(text, source_name="--")
16
+ @source_name = source_name || "--"
17
+ @input = StringScanner.new text
18
+ @output = create_node DocumentNode, :name => @source_name.to_sym
19
+ @current_macro = nil
20
+ @current_attribute = nil
21
+ end
22
+
23
+ # Parses the string of text provided during initialization
24
+ # @return [Glyph::SyntaxNode] the Abstract Syntax Tree corresponding to the string
25
+ # @since 0.3.0
26
+ def parse
27
+ count = 0
28
+ while result = parse_contents(@output) do
29
+ @output << result
30
+ count +=1
31
+ end
32
+ if @input.pos < @input.string.length then
33
+ current_char = @input.string[@input.pos].chr
34
+ illegal_delimiter = current_char.match(/\]|\[/) rescue nil
35
+ error "Macro delimiter '#{current_char}' not escaped" if illegal_delimiter
36
+ end
37
+ @output
38
+ end
39
+
40
+ protected
41
+
42
+ def parse_contents(current)
43
+ escape_sequence(current) ||
44
+ parameter_delimiter(current) ||
45
+ escaping_attribute(current) ||
46
+ escaping_macro(current) ||
47
+ attribute(current) ||
48
+ macro(current) ||
49
+ text(current)
50
+ end
51
+
52
+ def parse_escaped_contents(current)
53
+ escape_sequence(current) || parameter_delimiter(current) || escaped_text(current)
54
+ end
55
+
56
+ def escaping_macro(current)
57
+ if @input.scan(/[^\[\]\|\\\s]+\[\=/) then
58
+ name = @input.matched
59
+ name.chop!
60
+ name.chop!
61
+ error "#{name}[...] - A macro cannot start with '@' or a digit." if name.match(/^[0-1@]/)
62
+ node = create_node(MacroNode, {
63
+ :name => name.to_sym,
64
+ :escape => true,
65
+ :attributes => [],
66
+ :parameters => []
67
+ })
68
+ while contents = parse_escaped_contents(node) do
69
+ node << contents unless contents.is_a?(AttributeNode)
70
+ end
71
+ @input.scan(/\=\]/) or error "Escaping macro '#{name}' not closed"
72
+ organize_children_for node
73
+ node
74
+ else
75
+ nil
76
+ end
77
+ end
78
+
79
+ def escaping_attribute(current)
80
+ if @input.scan(/@[^\[\]\|\\\s]+\[\=/) then
81
+ error "Attributes cannot be nested" if @current_attribute
82
+ name = @input.matched[1..@input.matched.length-3]
83
+ node = create_node(AttributeNode, {
84
+ :escape => true,
85
+ :name => name.to_sym
86
+ })
87
+ while contents = parse_escaped_contents(node) do
88
+ node << contents
89
+ end
90
+ current[:attributes] << node
91
+ @input.scan(/\=\]/) or error "Attribute @#{name} not closed"
92
+ node
93
+ else
94
+ nil
95
+ end
96
+ end
97
+
98
+ def macro(current)
99
+ if @input.scan(/[^\[\]\|\\\s]+\[/) then
100
+ name = @input.matched
101
+ name.chop!
102
+ error "#{name}[...] - A macro cannot start with '@' or a digit." if name.match(/^[0-1@]/)
103
+ node = create_node(MacroNode, {
104
+ :escape => false,
105
+ :name => name.to_sym,
106
+ :attributes => [],
107
+ :parameters => []
108
+ })
109
+ while contents = parse_contents(node) do
110
+ node << contents unless contents.is_a?(AttributeNode)
111
+ end
112
+ @input.scan(/\]/) or error "Macro '#{name}' not closed"
113
+ organize_children_for node
114
+ node
115
+ else
116
+ nil
117
+ end
118
+ end
119
+
120
+ def attribute(current)
121
+ if @input.scan(/@[^\[\]\|\\\s]+\[/) then
122
+ error "Attributes cannot be nested" if current.is_a?(AttributeNode)
123
+ name = @input.matched[1..@input.matched.length-2]
124
+ node = create_node(AttributeNode, {
125
+ :escape => false,
126
+ :name => name.to_sym
127
+ })
128
+ while contents = parse_contents(node) do
129
+ node << contents
130
+ end
131
+ current[:attributes] << node
132
+ @input.scan(/\]/) or error "Attribute @#{name} not closed"
133
+ node
134
+ else
135
+ nil
136
+ end
137
+ end
138
+
139
+ def text(current)
140
+ start_p = @input.pos
141
+ res = @input.scan_until /(\\.)|(\A(\]|\|)|[^\\](\]|\|)|[^\[\]\|\\\s]+\[|\Z)/
142
+ offset = @input.matched.match(/^[^\\](\]|\|)$/) ? 1 : @input.matched.length
143
+ @input.pos = @input.pos - offset rescue @input.pos
144
+ return nil if @input.pos == start_p
145
+ match = @input.string[start_p..@input.pos-1]
146
+ illegal_macro_delimiter? start_p, match
147
+ if match.length > 0 then
148
+ create_node TextNode, :value => match
149
+ else
150
+ nil
151
+ end
152
+ end
153
+
154
+ def escaped_text(current)
155
+ start_p = @input.pos
156
+ res = @input.scan_until /(\\.)|(\A(\=\]|\|)|[^\\](\=\]|\|)|\Z)/
157
+ case
158
+ when @input.matched.match(/^[^\\]\=\]$/) then
159
+ offset = 2
160
+ when @input.matched.match(/^[^\\]\|$/) then
161
+ offset = 1
162
+ else
163
+ offset = @input.matched.length
164
+ end
165
+ @input.pos = @input.pos - offset rescue @input.pos
166
+ return nil if @input.pos == start_p
167
+ match = @input.string[start_p..@input.pos-1]
168
+ illegal_nesting = match.match(/([^\[\]\|\\\s]+)\[\=/)[1] rescue nil
169
+ if illegal_nesting then
170
+ error "Cannot nest escaping macro '#{illegal_nesting}' within escaping macro '#{current[:name]}'"
171
+ end
172
+ if match.length > 0 then
173
+ create_node TextNode, :value => match, :escaped => true
174
+ else
175
+ nil
176
+ end
177
+ end
178
+
179
+ def parameter_delimiter(current)
180
+ if @input.scan(/\|/) then
181
+ # Parameters are not allowed outside macros or inside attributes
182
+ if current.is_a?(DocumentNode) || current.is_a?(AttributeNode) then
183
+ @input.pos = @input.pos-1
184
+ error "Parameter delimiter '|' not allowed here"
185
+ end
186
+ create_node SyntaxNode, :parameter => true
187
+ else
188
+ nil
189
+ end
190
+ end
191
+
192
+ def escape_sequence(current)
193
+ if @input.scan(/\\./) then
194
+ create_node EscapeNode, :value => @input.matched, :escaped => true
195
+ end
196
+ end
197
+
198
+ private
199
+
200
+ def aggregate_parameters_for(node)
201
+ indices = []
202
+ count = 0
203
+ node.children.each do |n|
204
+ indices << count if n[:parameter]
205
+ count += 1
206
+ end
207
+ # No parameter found
208
+ if indices == [] then
209
+ node[:parameters][0] = create_node ParameterNode, :name => :"0"
210
+ node.children.each do |c|
211
+ node[:parameters][0] << c
212
+ end
213
+ else
214
+ # Parameters found
215
+ current_index = 0
216
+ total_parameters = 0
217
+ save_parameter = lambda do |max_index|
218
+ parameter = create_node ParameterNode, :name => "#{total_parameters}".to_sym
219
+ total_parameters +=1
220
+ current_index.upto(max_index) do |index|
221
+ parameter << (node & index)
222
+ end
223
+ node[:parameters] << parameter
224
+ end
225
+ indices.each do |i|
226
+ save_parameter.call(i-1)
227
+ current_index = i+1
228
+ end
229
+ save_parameter.call(node.children.length-1)
230
+ end
231
+ node[:parameters]
232
+ end
233
+
234
+ def organize_children_for(node)
235
+ aggregate_parameters_for node
236
+ node.children.clear
237
+ node[:parameters].each do |p|
238
+ node << p
239
+ end
240
+ empty_parameter =
241
+ node.children.length == 1 &&
242
+ ((node&0).children.length == 0 ||
243
+ (node&0).children.length == 0 &&
244
+ (node&0&0).is_a?(TextNode) &&
245
+ (node&0&0)[:value].blank?)
246
+ node.children.clear if empty_parameter
247
+ node.delete(:parameters)
248
+ node[:attributes].each do |a|
249
+ node << a
250
+ end
251
+ node.delete(:attributes)
252
+ end
253
+
254
+ def illegal_macro_delimiter?(start_p, string)
255
+ string.match(/\A(\[|\])|[^\\](\[|\])/)
256
+ illegal_delimiter = $1 || $2
257
+ if illegal_delimiter then
258
+ @input.pos = start_p + string.index(illegal_delimiter)
259
+ error "Macro delimiter '#{illegal_delimiter}' not escaped"
260
+ end
261
+ end
262
+
263
+ def error(msg)
264
+ lines = @input.string[0..@input.pos].split(/\n/)
265
+ line = lines.length
266
+ column = lines.last.length
267
+ raise Glyph::SyntaxError.new("#{@source_name} [#{line}, #{column}] "+msg)
268
+ end
269
+
270
+ def create_node(klass, hash={})
271
+ klass.new.from hash
272
+ end
273
+
274
+ end
275
+
276
+ end
277
+
278
+