duxml 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/bin/validate_xml +19 -0
  3. data/lib/duxml/doc/element.rb +157 -0
  4. data/lib/duxml/doc/lazy_ox.rb +153 -0
  5. data/lib/duxml/doc/node_set.rb +39 -0
  6. data/lib/duxml/doc.rb +26 -0
  7. data/lib/duxml/meta/grammar/pattern/attr_name_pattern.rb +36 -0
  8. data/lib/duxml/meta/grammar/pattern/attr_val_pattern.rb +36 -0
  9. data/lib/duxml/meta/grammar/pattern/child_pattern.rb +72 -0
  10. data/lib/duxml/meta/grammar/pattern/text_pattern.rb +30 -0
  11. data/lib/duxml/meta/grammar/pattern.rb +98 -0
  12. data/lib/duxml/meta/grammar/pattern_maker.rb +69 -0
  13. data/lib/duxml/meta/grammar/relax_ng/attrs_rule.rb +58 -0
  14. data/lib/duxml/meta/grammar/relax_ng/children_rule.rb +83 -0
  15. data/lib/duxml/meta/grammar/relax_ng/value_rule.rb +44 -0
  16. data/lib/duxml/meta/grammar/relax_ng.rb +40 -0
  17. data/lib/duxml/meta/grammar/rule/attrs_rule.rb +78 -0
  18. data/lib/duxml/meta/grammar/rule/children_rule.rb +137 -0
  19. data/lib/duxml/meta/grammar/rule/text_rule.rb +40 -0
  20. data/lib/duxml/meta/grammar/rule/value_rule.rb +111 -0
  21. data/lib/duxml/meta/grammar/rule.rb +59 -0
  22. data/lib/duxml/meta/grammar/spreadsheet.rb +35 -0
  23. data/lib/duxml/meta/grammar.rb +134 -0
  24. data/lib/duxml/meta/history/add.rb +36 -0
  25. data/lib/duxml/meta/history/change.rb +71 -0
  26. data/lib/duxml/meta/history/change_attr.rb +34 -0
  27. data/lib/duxml/meta/history/change_text.rb +33 -0
  28. data/lib/duxml/meta/history/error.rb +25 -0
  29. data/lib/duxml/meta/history/new_attr.rb +33 -0
  30. data/lib/duxml/meta/history/new_text.rb +37 -0
  31. data/lib/duxml/meta/history/qualify_error.rb +22 -0
  32. data/lib/duxml/meta/history/remove.rb +28 -0
  33. data/lib/duxml/meta/history/undo.rb +24 -0
  34. data/lib/duxml/meta/history/validate_error.rb +21 -0
  35. data/lib/duxml/meta/history.rb +88 -0
  36. data/lib/duxml/meta.rb +51 -0
  37. data/lib/duxml/reportable.rb +27 -0
  38. data/lib/duxml/ruby_ext/fixnum.rb +56 -0
  39. data/lib/duxml/ruby_ext/module.rb +12 -0
  40. data/lib/duxml/ruby_ext/object.rb +14 -0
  41. data/lib/duxml/ruby_ext/regexp.rb +20 -0
  42. data/lib/duxml/ruby_ext/string.rb +29 -0
  43. data/lib/duxml/saxer.rb +71 -0
  44. data/lib/duxml.rb +97 -0
  45. data/xml/dita_grammar.xml +2133 -0
  46. metadata +117 -0
@@ -0,0 +1,88 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/history/add')
4
+ require File.expand_path(File.dirname(__FILE__) + '/history/remove')
5
+ require File.expand_path(File.dirname(__FILE__) + '/history/validate_error')
6
+ require File.expand_path(File.dirname(__FILE__) + '/history/qualify_error')
7
+ require File.expand_path(File.dirname(__FILE__) + '/history/new_attr')
8
+ require File.expand_path(File.dirname(__FILE__) + '/history/change_attr')
9
+ require File.expand_path(File.dirname(__FILE__) + '/history/new_text')
10
+ require File.expand_path(File.dirname(__FILE__) + '/history/change_text')
11
+ require File.expand_path(File.dirname(__FILE__) + '/history/undo')
12
+ require File.expand_path(File.dirname(__FILE__) + '/../doc')
13
+ require 'forwardable'
14
+
15
+ module Duxml
16
+ # monitors XML Elements for changes and GrammarClass for errors, recording them and saving to Meta file
17
+ module History
18
+ include Duxml
19
+ include Reportable
20
+ end
21
+
22
+ # as an object, HistoryClass holds events latest first, earliest last
23
+ # it also has delegators that allow the use of Array-style notation e.g. '[]' and #each to search the history.
24
+ class HistoryClass
25
+ include History
26
+ extend Forwardable
27
+
28
+ def_delegators :@nodes, :[], :each
29
+
30
+ # @param harsh_or_kind [Boolean] by default harsh i.e. true so that if this History detects an error it will raise an Exception; otherwise not
31
+ def initialize(harsh_or_kind = true)
32
+ @nodes = []
33
+ @strict = harsh_or_kind
34
+ end
35
+
36
+ attr_reader :nodes
37
+ alias_method :events, :nodes
38
+ end
39
+
40
+ module History
41
+ # used when creating a new metadata file for a static XML file
42
+ #
43
+ # @return [Element] XML element for a new <duxml:history> node
44
+ def self.xml
45
+ Element.new(name.nmtokenize).extend self
46
+ end
47
+
48
+ # @return [Boolean] toggles (true by default) whether History will raise exception or tolerate qualify errors
49
+ def strict?(harsh_or_kind=nil)
50
+ @strict = harsh_or_kind.nil? ? @strict : harsh_or_kind
51
+ @strict
52
+ end
53
+
54
+ # @return [ChangeClass, ErrorClass] the latest event
55
+ def latest
56
+ events[0]
57
+ end
58
+
59
+ # @return [GrammarClass] grammar that is observing this history's events
60
+ def grammar
61
+ @observer_peers.first.first if @observer_peers and @observer_peers.any? and @observer_peers.first.any?
62
+ end
63
+
64
+ # @return [String] shortened self description for debugging
65
+ def inspect
66
+ "#<#{self.class.to_s} #{object_id}: @events=#{nodes.size}>"
67
+ end
68
+
69
+ # @return [String] returns entire history, calling #description on each event in chronological order
70
+ def description
71
+ "history follows: \n" +
72
+ events.reverse.collect do |change_or_error|
73
+ change_or_error.description
74
+ end.join("\n")
75
+ end
76
+
77
+ # @param type [Symbol] category i.e. class symbol of changes/errors reported
78
+ # @param *args [*several_variants] information needed to accurately log the event; varies by change/error class
79
+ def update(type, *args)
80
+ change_class = Duxml::const_get "#{type.to_s}Class".to_sym
81
+ change_comp = change_class.new *args
82
+ @nodes.unshift change_comp
83
+ changed
84
+ notify_observers(change_comp) unless change_comp.respond_to?(:error?)
85
+ raise(Exception, change_comp.description) if strict? && type == :QualifyError
86
+ end
87
+ end # module History
88
+ end # module Duxml
data/lib/duxml/meta.rb ADDED
@@ -0,0 +1,51 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/meta/grammar')
4
+ require File.expand_path(File.dirname(__FILE__) + '/meta/history')
5
+ require File.expand_path(File.dirname(__FILE__) + '/saxer')
6
+
7
+ module Duxml
8
+ module Meta
9
+
10
+ FILE_EXT = '.duxml'
11
+ end
12
+
13
+ # all XML files ready by Duxml have a metadata file generated with a modified, matching file name
14
+ # @see #Meta.meta_path
15
+ class MetaClass
16
+ include Meta
17
+
18
+ # @param grammar_path [String] optional path of grammar file which can be a spreadsheet or Duxml::Grammar file
19
+ def initialize(grammar_path=nil)
20
+ @history = HistoryClass.new
21
+ self.grammar = grammar_path ? Grammar.import(grammar_path) : GrammarClass.new
22
+ @grammar_path = grammar_path
23
+ end
24
+
25
+ attr_reader :history, :grammar
26
+ end
27
+
28
+ module Meta
29
+ # @return [Doc] metadata document
30
+ def self.xml
31
+ d = Doc.new << (Element.new(name.nmtokenize) << Grammar.xml << History.xml)
32
+ d.root.grammar[:ref] = @grammar_path if @grammar_path
33
+ d
34
+ end
35
+
36
+ # @param path [String] path of XML-content file
37
+ # @return [String] full path of metadata file based on content file's name e.g.
38
+ # 'design.xml' => '.design.xml.duxml'
39
+ def self.meta_path(path)
40
+ dir = File.dirname(path)
41
+ "#{dir}/.#{File.basename(path)}#{FILE_EXT}"
42
+ end
43
+
44
+ def grammar=(g)
45
+ @grammar = g.is_a?(GrammarClass) ? g : Grammar.import(g)
46
+ history.delete_observers if history.respond_to?(:delete_observers)
47
+ history.add_observer(grammar, :qualify)
48
+ grammar.add_observer history
49
+ end
50
+ end # module Meta
51
+ end # module Duxml
@@ -0,0 +1,27 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+
3
+ require 'observer'
4
+
5
+ module Reportable
6
+ include Observable
7
+
8
+ # @param obs [Object] observer to add to this Element as well as its NodeSet
9
+ def add_observer(obs, sym=nil)
10
+ super(obs, sym || :update)
11
+ nodes.add_observer(obs) if self.respond_to?(:nodes) and nodes.respond_to?(:add_observer)
12
+ end
13
+
14
+ private
15
+
16
+ # all public methods that alter XML must call #report in the full scope of that public method
17
+ # in order to correctly acquire name of method that called #report
18
+ #
19
+ # @param *args [*several_variants]
20
+ def report(*args)
21
+ return nil if @observer_peers.nil?
22
+ changed
23
+ new_args = [args.first, self]
24
+ args[1..-1].each do |a| new_args << a end if args.size > 1
25
+ notify_observers(*new_args)
26
+ end
27
+ end
@@ -0,0 +1,56 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+
3
+ class Fixnum
4
+ NUM_NAMES = %w(zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty thirty forty fifty sixty seventy eighty ninety)
5
+ ORDINAL_SUFFIXES = %w(th st nd rd th)
6
+ ORDINAL_NAMES = %w(zeroth first second third fourth fifth sixth seventh eighth ninth tenth eleventh twelfth)
7
+
8
+ # @return [String] short string ordinal e.g. 3.ordinal =? 'third'
9
+ def ordinal
10
+ self.to_s + suffix
11
+ end
12
+
13
+ def suffix
14
+ if self%100 < 4 or self%100 > 20
15
+ ORDINAL_SUFFIXES[self%10]
16
+ else
17
+ ORDINAL_SUFFIXES.first
18
+ end
19
+ end
20
+
21
+ # @return [String] full name of number e.g. 200058.to_word => 'two-hundred thousand and fifty-eight' for any Fixnum less than a billion
22
+ def to_word
23
+ case
24
+ when self < 21 then NUM_NAMES[self]
25
+ when self < 100
26
+ ones = self%10
27
+ ones_str = ones.zero? ? '' : "-#{ones.to_word}"
28
+ NUM_NAMES[self/10+18]+ones_str
29
+ when self < 1000
30
+ tens = self%100
31
+ "#{NUM_NAMES[self/100]} hundred #{'and '+(tens).to_word unless tens.zero?}"
32
+ when self < 1000000
33
+ remainder = self%1000 < 100 ? "and #{(self%1000).to_word}" : (self%1000).to_word
34
+ "#{(self/1000).to_word} thousand #{remainder}"
35
+ when self < 1000000000
36
+ "#{(self/1000000).to_word} million #{(self%1000000).to_word}"
37
+ else raise Exception, 'method only supports names for numbers less than 1000000000 i.e. <= 999,999,999'
38
+ end.strip.gsub(' and zero', '')
39
+ end
40
+
41
+ # @return [String] full name of ordinal number e.g. 4281.ordinal_name => 'four thousand and two-hundred eighty-first'
42
+ def ordinal_name
43
+ ones = self%10
44
+ tens = self%100
45
+ case
46
+ when tens.zero? then self.to_word+ORDINAL_SUFFIXES.first
47
+ when ones.zero? && tens > 10 then self.to_word[-3..-1] + 'tieth'
48
+ when ones.zero? && tens == 10 then self.to_word+ORDINAL_SUFFIXES.first
49
+ when tens < 13 then "#{(self-tens).to_word} and #{ORDINAL_NAMES[tens]}"
50
+ when tens < 20 && tens > 12
51
+ "#{(self-tens).to_word} and #{NUM_NAMES[tens]}#{ORDINAL_SUFFIXES.first}"
52
+ when tens-ones != 0 then "#{(self-ones).to_word}-#{ORDINAL_NAMES[ones]}"
53
+ else "#{(self-ones).to_word} and #{ORDINAL_NAMES[ones]}"
54
+ end.strip.gsub('zero and ', '').gsub('zero', '')
55
+ end
56
+ end
@@ -0,0 +1,12 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+
3
+ class Module
4
+ def simple_name
5
+ self.to_s.split('::').last
6
+ end
7
+
8
+ def simple_module
9
+ a = self.to_s.split('::')
10
+ a.size > 1 ? a[-2] : 'Module'
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/string')
4
+
5
+ class Object
6
+ def simple_name
7
+ self.class.to_s.split('::').last
8
+ end
9
+
10
+ def simple_module
11
+ a = self.class.to_s.split('::')
12
+ a.size > 1 ? a[-2] : 'Module'
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+
3
+ class Regexp
4
+ class << self
5
+ # @return [Regexp] C identifier e.g. ident_ifier0, excluding 'true' and 'false'
6
+ def identifier
7
+ /(?:(?!true|false))\b([a-zA-Z_][a-zA-Z0-9_]*)\b/
8
+ end
9
+
10
+ # @return [Regexp] XML NMTOKEN e.g xml:attribute, also-valid, CDATA
11
+ def nmtoken
12
+ /(?!\s)([a-zA-Z0-9_\-.:]*)(?!\s)/
13
+ end
14
+
15
+ # @return [Regexp] Ruby constant e.g. Constant, CONSTANT
16
+ def constant
17
+ /\b([A-Z][a-zA-Z0-9_]*)\b/
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+ require File.expand_path(File.dirname(__FILE__) + '/regexp')
3
+
4
+ class String
5
+ # @return [String] converts string into Ruby constant. self must be String with no whitespaces and match Regexp.nmtoken
6
+ # 'foo-bar'.constantize => 'Foo_bar'
7
+ # 'foo_bar'.constantize => 'FooBar'
8
+ def constantize
9
+ return self if Regexp.constant.match(self)
10
+ raise Exception unless Regexp.nmtoken.match(self)
11
+ s = split('_').collect do |word| word.capitalize unless word == '_' end.join.gsub('-', '_')
12
+ raise Exception unless s.match(Regexp.constant)
13
+ s
14
+ end
15
+
16
+ # @return [String] does reverse of #constantize e.g.
17
+ # 'Foo_b'.nmtokenize => 'foo-bar'
18
+ # 'FooBar'.nmtokenize => 'foo_bar'
19
+ def nmtokenize
20
+ split('::').collect do |word|
21
+ word.gsub(/(?!^)[A-Z_]/) do |match|
22
+ case match
23
+ when '_' then '-'
24
+ else "_#{match.downcase}"
25
+ end
26
+ end.downcase
27
+ end.join(':')
28
+ end
29
+ end # class String
@@ -0,0 +1,71 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/doc')
4
+
5
+ module Duxml
6
+ module Saxer
7
+ @io
8
+
9
+ attr_accessor :io
10
+ # @param path [String] path of file to parse
11
+ # @return [Doc] finished document with each Element's line and column info added
12
+ def sax(path, obs=nil)
13
+ io = File.open path
14
+ saxer = DocuLiner.new(Duxml::Doc.new, obs)
15
+ Ox.sax_parse(saxer, io, {convert_special: true, symbolize: false})
16
+ doc = saxer.cursor
17
+ doc.add_observer obs if obs
18
+ doc
19
+ end
20
+
21
+ class DocuLiner < ::Ox::Sax
22
+ # @param doc [Ox::Document] document that is being constructed as XML is parsed
23
+ # @param _observer [Object] object that will observe this document's content
24
+ def initialize(doc, _observer)
25
+ @cursor_stack = [doc]
26
+ @line = 0
27
+ @column = 0
28
+ @observer = _observer
29
+ end
30
+
31
+ attr_reader :line, :column, :observer
32
+
33
+ def cursor
34
+ cursor_stack.last
35
+ end
36
+
37
+ attr_accessor :cursor_stack
38
+
39
+ def start_element(name)
40
+ cursor << Duxml::Element.new(name, line, column)
41
+ cursor_stack << cursor.nodes.last
42
+ end
43
+
44
+ def attr(name, val)
45
+ cursor[name] = val
46
+ end
47
+
48
+ def text(str)
49
+ cursor << str
50
+ end
51
+
52
+ def end_element(name)
53
+ cursor.add_observer(observer) if observer
54
+ @cursor_stack.pop
55
+ end
56
+
57
+ private
58
+
59
+ def doc
60
+ cursor_stack.first
61
+ end
62
+
63
+ def location_key
64
+ @alocation.inject do |a, index|
65
+ a ||= ""
66
+ a << index.to_s
67
+ end
68
+ end
69
+ end # class DocuLiner < ::Ox::Sax
70
+ end # module Saxer
71
+ end # module Duxml
data/lib/duxml.rb ADDED
@@ -0,0 +1,97 @@
1
+ # Copyright (c) 2016 Freescale Semiconductor Inc.
2
+
3
+ require File.expand_path(File.dirname(__FILE__) + '/duxml/saxer')
4
+ require File.expand_path(File.dirname(__FILE__) + '/duxml/meta')
5
+
6
+ module Duxml
7
+ DITA_GRAMMAR = File.expand_path(File.dirname(__FILE__) + '/../xml/dita_grammar.xml')
8
+ include Saxer
9
+ include Meta
10
+
11
+ # path to XML file
12
+ @file
13
+ # current document
14
+ @doc
15
+ # meta data document
16
+ @meta
17
+
18
+ attr_reader :meta, :file, :doc
19
+
20
+ # @param file [String, Doc] loads or creates given file or document and finds or creates corresponding metadata file e.g. '.xml_file.duxml'
21
+ # @param grammar_path [nil, String, Duxml::Grammar] optional - provide an external grammar file or object
22
+ # @return [Duxml::Meta] combined Object tree from metadata root (metadata and content's XML documents are kept separate)
23
+ def load(_file, grammar_path=nil)
24
+ if _file.is_a?(String) and File.exists?(_file)
25
+ @file = _file
26
+ else
27
+ @file = "#{(_file.respond_to?(:name) ? _file.name : _file.class.to_s) + _file.object_id.to_s}"
28
+ File.write file, ''
29
+ end
30
+
31
+ set_metadata!(grammar_path)
32
+ set_doc!
33
+ end # def load
34
+
35
+ # @param file [String] creates new XML file at given path
36
+ # @param content [Doc, Element] XML content with which to initialize new file
37
+ def create(file, content=nil)
38
+ File.write(file, content.to_s)
39
+ @doc = content.is_a?(Doc) ? content : Doc.new
40
+ end
41
+
42
+ # @param file [String] saves current content XML to given file path (Duxml@file by default)
43
+ def save(file)
44
+ meta_path = Meta.meta_path(file)
45
+ unless File.exists?(meta_path)
46
+ File.new meta_path, 'w+'
47
+ File.write(meta_path, Meta.xml)
48
+ end
49
+ end
50
+
51
+ # @param file [String] output file path for logging human-readable validation error messages
52
+ def log(file)
53
+ File.write(file, meta.history.description)
54
+ end
55
+
56
+ # @param *Args [*several_variants] @see #load
57
+ # @return [Boolean] whether file passed validation
58
+ def validate(*args)
59
+ load(*args) unless args.empty?
60
+ raise Exception, "grammar not defined!" unless meta.grammar.defined?
61
+ raise Exception, "document not loaded!" unless doc.root
62
+ results = []
63
+ doc.root.traverse do |n| results << meta.grammar.validate(n) unless n.is_a?(String) end
64
+ !results.any? do |r| !r end
65
+ end # def validate
66
+
67
+ # @return [Nokogiri::XML::RelaxNG] current metadata's grammar as a relaxng file
68
+ def relaxng
69
+ #meta.grammar.relaxng
70
+ end
71
+
72
+ private
73
+
74
+ # @return [Doc] @doc is set to either file given by user or new Doc
75
+ def set_doc!
76
+ @doc ||= if file.nil?
77
+ f = Doc.new
78
+ f.add_observer meta.history
79
+ f
80
+ else
81
+ f = File.open file
82
+ sax(f, meta.history)
83
+ end
84
+ end
85
+
86
+ # @return [MetaClass] @meta is set to either file extrapolated from path of XML-content file or new MetaClass
87
+ def set_metadata!(grammar_path=nil)
88
+ meta_path = Meta.meta_path(file)
89
+ if file and File.exists?(meta_path)
90
+ @meta = sax(File.open(meta_path)).root
91
+ meta.grammar=grammar_path unless grammar_path.nil? or meta.grammar.defined?
92
+ else
93
+ @meta = MetaClass.new(grammar_path)
94
+ end
95
+ meta
96
+ end
97
+ end # module Duxml