polites 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +18 -0
  5. data/.tool-versions +1 -0
  6. data/.travis.yml +6 -0
  7. data/.yardopts +1 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +15 -0
  10. data/Gemfile.lock +128 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +70 -0
  13. data/Rakefile +9 -0
  14. data/bin/console +15 -0
  15. data/bin/setup +8 -0
  16. data/exe/polites +8 -0
  17. data/lib/polites.rb +33 -0
  18. data/lib/polites/block.rb +65 -0
  19. data/lib/polites/block/blockquote.rb +8 -0
  20. data/lib/polites/block/code_block.rb +18 -0
  21. data/lib/polites/block/divider.rb +8 -0
  22. data/lib/polites/block/heading.rb +8 -0
  23. data/lib/polites/block/heading1.rb +8 -0
  24. data/lib/polites/block/heading2.rb +8 -0
  25. data/lib/polites/block/heading3.rb +8 -0
  26. data/lib/polites/block/heading4.rb +8 -0
  27. data/lib/polites/block/heading5.rb +8 -0
  28. data/lib/polites/block/heading6.rb +8 -0
  29. data/lib/polites/block/list.rb +14 -0
  30. data/lib/polites/block/ordered_list.rb +8 -0
  31. data/lib/polites/block/paragraph.rb +8 -0
  32. data/lib/polites/block/unordered_list.rb +8 -0
  33. data/lib/polites/cli.rb +52 -0
  34. data/lib/polites/convert.rb +29 -0
  35. data/lib/polites/doc/_index.html +85 -0
  36. data/lib/polites/doc/class_list.html +51 -0
  37. data/lib/polites/doc/css/common.css +1 -0
  38. data/lib/polites/doc/css/full_list.css +58 -0
  39. data/lib/polites/doc/css/style.css +496 -0
  40. data/lib/polites/doc/file_list.html +51 -0
  41. data/lib/polites/doc/frames.html +17 -0
  42. data/lib/polites/doc/index.html +85 -0
  43. data/lib/polites/doc/js/app.js +314 -0
  44. data/lib/polites/doc/js/full_list.js +216 -0
  45. data/lib/polites/doc/js/jquery.js +4 -0
  46. data/lib/polites/doc/method_list.html +51 -0
  47. data/lib/polites/doc/top-level-namespace.html +100 -0
  48. data/lib/polites/file.rb +67 -0
  49. data/lib/polites/html_formatter.rb +119 -0
  50. data/lib/polites/list_indenter.rb +34 -0
  51. data/lib/polites/markup.rb +31 -0
  52. data/lib/polites/nanoc.rb +46 -0
  53. data/lib/polites/nanoc/data_source.rb +93 -0
  54. data/lib/polites/nanoc/embedded_images_filter.rb +22 -0
  55. data/lib/polites/nanoc/extract_file_filter.rb +21 -0
  56. data/lib/polites/node.rb +28 -0
  57. data/lib/polites/parser.rb +174 -0
  58. data/lib/polites/plist.rb +28 -0
  59. data/lib/polites/range_tag.rb +29 -0
  60. data/lib/polites/settings.rb +35 -0
  61. data/lib/polites/sheet.rb +63 -0
  62. data/lib/polites/simple_tag.rb +22 -0
  63. data/lib/polites/span.rb +60 -0
  64. data/lib/polites/span/annotation.rb +18 -0
  65. data/lib/polites/span/code.rb +8 -0
  66. data/lib/polites/span/delete.rb +8 -0
  67. data/lib/polites/span/emph.rb +8 -0
  68. data/lib/polites/span/footnote.rb +18 -0
  69. data/lib/polites/span/image.rb +29 -0
  70. data/lib/polites/span/link.rb +21 -0
  71. data/lib/polites/span/mark.rb +8 -0
  72. data/lib/polites/span/strong.rb +8 -0
  73. data/lib/polites/tag.rb +20 -0
  74. data/lib/polites/text.rb +20 -0
  75. data/lib/polites/version.rb +5 -0
  76. data/polites-nanoc.gemspec +31 -0
  77. data/polites.gemspec +33 -0
  78. metadata +153 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polites
4
+ # Modify the AST for a parsed sheet to group list items in a nested structure,
5
+ # rather than a flat structure using levels.
6
+ class ListIndenter
7
+ List = Struct.new(:children)
8
+
9
+ # @param [Array<Polites::Node>] items
10
+ # @return [Array<Polites::Node>]
11
+ def call(items)
12
+ items
13
+ .chunk { |i| i.is_a?(Block::List) }.to_a
14
+ .inject([]) do |acc, (k, contents)|
15
+ acc + (k ? [List.new(indent(contents))] : contents)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def indent(items)
22
+ items
23
+ .chunk { |item| item.level > items.first.level }
24
+ .inject([]) do |acc, (indented, subitems)|
25
+ if indented
26
+ acc.last.children << List.new(indent(subitems))
27
+ acc
28
+ else
29
+ acc + subitems
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polites
4
+ # The markup section defines the markup used in a Polites document, specifying
5
+ # both its versions and patterns defined in {Tag}s.
6
+ class Markup
7
+ # @return [String]
8
+ attr_reader :version
9
+
10
+ # @return [String]
11
+ attr_reader :identifier
12
+
13
+ # @return [String]
14
+ attr_reader :display_name
15
+
16
+ # @return [Array<Tag>]
17
+ attr_reader :tags
18
+
19
+ # @param [String] version
20
+ # @param [String] identifier
21
+ # @param [String] display_name
22
+ # @param [Array<Tag>] tags
23
+ def initialize(version, identifier, display_name, tags = [])
24
+ @version = version
25
+ @identifier = identifier
26
+ @display_name = display_name
27
+ @tags = tags
28
+ freeze
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './nanoc/data_source'
4
+ require_relative './nanoc/extract_file_filter'
5
+ require_relative './nanoc/embedded_images_filter'
6
+
7
+ module Polites
8
+ # The {Polites::Nanoc} module provides integration with the
9
+ # [Nanoc](https://nanoc.ws) static site generator. It allows you to
10
+ # configure a Polites external directory as a data source in a Nanoc site,
11
+ # so you can transform Polites files straight into HTML documents.
12
+ #
13
+ # This gem consists of the following parts:
14
+ #
15
+ # * {Polites::Nanoc::DataSource} implements the data source that reads .ulyz
16
+ # files from disk and creates Nanoc HTML items from them.
17
+ # * {Polites::Nanoc::EmbeddedImagesFilter} implements a filter you can use to
18
+ # transform the image paths in your HTML to the pats generated by your Nanoc
19
+ # rules.
20
+ # * {Polites::Nanoc::ExtractFileFilter} is a filter that will extract embedded
21
+ # files from the .ulyz zip file to actual output files on disk in your Nanoc
22
+ # site.
23
+ #
24
+ # @example Nanoc configuration using Polites data source
25
+ # # in nanoc.yaml
26
+ # data_sources:
27
+ # - type: polites
28
+ # items_root: /articles/
29
+ # path: path/to/articles
30
+ # @example Require Ulussyes::Nanoc in your site
31
+ # # lib/default.rb
32
+ # require 'polites/nanoc'
33
+ # @example Nanoc rule to compile Ulussyes articles
34
+ # compile "/articles/*.ulyz" do
35
+ # filter :polites_embedded_images
36
+ # layout "/default.*"
37
+ # write item.identifier.without_ext + "/index.html"
38
+ # end
39
+ # @example Nanoc rule to extract embedded media
40
+ # compile(%r{\A/articles/.+\.ulyz/media/.+\Z}) do
41
+ # filter :extract_file
42
+ # write item.identifier.to_s.sub(".ulyz", "")
43
+ # end
44
+ module Nanoc
45
+ end
46
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require_relative '../settings'
5
+ require_relative '../parser'
6
+ require_relative '../file'
7
+ require_relative '../html_formatter'
8
+
9
+ module Polites
10
+ module Nanoc
11
+ # A data source for Nanoc that creates Nanoc content items from Polites files
12
+ # in a particular directory.
13
+ class DataSource < ::Nanoc::DataSource
14
+ identifier :polites
15
+
16
+ def up
17
+ @root = Pathname(@config[:path])
18
+ @settings = Settings.from_directory(@root)
19
+ @extension = ".#{@settings['defaultPathExtensions']}"
20
+ @input_files = @root.glob("*#{@extension}")
21
+ @parser = Polites::Parser.new
22
+ @formatter = Polites::HtmlFormatter.new
23
+ end
24
+
25
+ def items
26
+ @input_files.flat_map do |input_file|
27
+ File.open(input_file) do |file|
28
+ sheet = @parser.parse_sheet(file.content)
29
+
30
+ inline_file_items = sheet.inline_files.map do |image|
31
+ build_file_item(file.media(image.image), image.image, input_file, image.filename)
32
+ end
33
+
34
+ file_items = sheet.attached_files.map do |id|
35
+ build_file_item(file.media(id), id, input_file)
36
+ end
37
+
38
+ [
39
+ new_item(
40
+ @formatter.call(sheet),
41
+ {
42
+ keywords: sheet.keywords,
43
+ image: sheet.attached_files.first,
44
+ image_caption: sheet.notes.any? ? @formatter.call(sheet.notes.first) : nil,
45
+ inline_file_items: inline_file_items,
46
+ filename: input_file.to_s,
47
+ mtime: input_file.mtime
48
+ },
49
+ identifier(input_file)
50
+ ),
51
+ *inline_file_items,
52
+ *file_items
53
+ ]
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def build_file_item(entry, id, input_file, filename = nil)
61
+ p = filename ? Pathname(filename) : Pathname(entry.name).basename
62
+ i = "#{identifier(input_file)}/media#{identifier(p, p.extname)}"
63
+ new_item(
64
+ input_file.expand_path.to_s,
65
+ {
66
+ explicit_filename: filename,
67
+ id: id,
68
+ subpath: entry.name,
69
+ mtime: input_file.mtime
70
+ },
71
+ i,
72
+ binary: true
73
+ )
74
+ end
75
+
76
+ def identifier(path, extension = @extension)
77
+ "/#{path
78
+ .relative_path_from(@root)
79
+ .basename(extension)
80
+ .to_s
81
+ .then { |s| underscore(s) }}#{extension}"
82
+ end
83
+
84
+ def underscore(str)
85
+ str
86
+ .gsub(/[^a-zA-Z0-9\-_]/, '-')
87
+ .squeeze('-')
88
+ .gsub(/^-*|-*$/, '')
89
+ .downcase
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polites
4
+ module Nanoc
5
+ # Nanoc filter for replacing the Polites-generated filename to images with
6
+ # actual output filenames as generated by Nanoc.
7
+ class EmbeddedImagesFilter < ::Nanoc::Filter
8
+ identifier :polites_embedded_images
9
+
10
+ def run(content, _params = {})
11
+ return content unless @item[:inline_file_items]&.any?
12
+
13
+ @item[:inline_file_items].inject(content) do |acc, inline_file_item|
14
+ actual_item = @items.find do |item|
15
+ item.attributes[:id] == inline_file_item.attributes[:id]
16
+ end
17
+ acc.gsub(/(?<=src=")(#{actual_item.attributes[:explicit_filename]}|#{actual_item.attributes[:id]})(?=")/, actual_item.path)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../file'
4
+
5
+ module Polites
6
+ module Nanoc
7
+ # Nanoc binary filter to extract files from a zip file to a given output file.
8
+ # This allows a single Polites file to be linked to multiple Nanoc items,
9
+ # which are extracted when needed during the compilation process.
10
+ class ExtractFileFilter < ::Nanoc::Filter
11
+ identifier :extract_file
12
+ type :binary
13
+
14
+ def run(filename, _params = {})
15
+ File.open(filename) do |f|
16
+ f.extract_to(@item[:subpath], output_filename)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polites
4
+ # The Node is the basic building block of the AST we parse Polites document
5
+ # contents into.
6
+ class Node
7
+ # @return [Array<Node>]
8
+ attr_reader :children
9
+
10
+ # @param [Array<Node>] children
11
+ def initialize(children = [])
12
+ @children = children
13
+ end
14
+
15
+ # Assemble the text contents of this node and all its children combined.
16
+ #
17
+ # @return [String]
18
+ def text
19
+ @children.map(&:text).join
20
+ end
21
+
22
+ def eql?(other)
23
+ other.is_a?(self.class) && children.eql?(other.children)
24
+ end
25
+
26
+ alias == eql?
27
+ end
28
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require_relative '../polites'
5
+ require_relative './block'
6
+ require_relative './text'
7
+ require_relative './span'
8
+ require_relative './sheet'
9
+ require_relative './markup'
10
+ require_relative './simple_tag'
11
+ require_relative './range_tag'
12
+
13
+ module Polites
14
+ # The parser takes XML content from a Polites file and parses it into our own
15
+ # abstract syntax tree built from {Node} elements. This can then be modified
16
+ # and formatted into the desired output.
17
+ class Parser
18
+ def initialize
19
+ reset
20
+ end
21
+
22
+ # Parse an entire sheet of content, including all its structural elements.
23
+ #
24
+ # @param [String] source
25
+ # @return [Sheet]
26
+ def parse_sheet(source)
27
+ sheet = Nokogiri(source).xpath('/sheet').first
28
+ Sheet.new(
29
+ version: sheet[:version],
30
+ app_version: sheet[:app_version],
31
+ markup: parse_markup(sheet),
32
+ content: parse_fragment(sheet),
33
+ keywords: parse_keywords(sheet),
34
+ files: parse_files(sheet),
35
+ notes: parse_notes(sheet)
36
+ )
37
+ end
38
+
39
+ # Parse just the markup section of a sheet into a {Markup} container.
40
+ #
41
+ # @param [Nokogiri::Node] element
42
+ # @return [Markup]
43
+ def parse_markup(element)
44
+ markup = element.xpath('markup').first
45
+ tags = markup.xpath('tag').map do |e|
46
+ if e[:pattern]
47
+ SimpleTag.new(e[:definition], e[:pattern])
48
+ elsif e[:startPattern] && e[:endPattern]
49
+ RangeTag.new(e[:definition], e[:startPattern], e[:endPattern])
50
+ end
51
+ end
52
+ Markup.new(markup[:version], markup[:identifier], markup[:displayName], tags)
53
+ end
54
+
55
+ # Parse a content fragment, which consists of multiple source elements but
56
+ # not necessarily an entire sheet.
57
+ #
58
+ # @param [String] source
59
+ # @return [Array<Node>]
60
+ def parse_fragment(source)
61
+ element = case source
62
+ when Nokogiri::XML::Element
63
+ source
64
+ when String
65
+ Nokogiri(source)
66
+ end
67
+ element.xpath('string/p').map do |el|
68
+ parse(el)
69
+ end
70
+ end
71
+
72
+ # Parse a unit into our AST. This will deal with anything passed in, but
73
+ # to parse multiple nodes successfully or to parse and entire sheet, see
74
+ # {parse_fragment} and {parse_sheet}.
75
+ #
76
+ # @param [Nokogiri::Node, String] source
77
+ # @raise [ParseError] when given a `source` we cannot deal with.
78
+ # @return [Node, Array<Node>]
79
+ def parse(source)
80
+ case source
81
+ when Nokogiri::XML::Element
82
+ parse_element(source)
83
+ when Nokogiri::XML::NodeSet
84
+ source.map { |s| parse(s) }.compact
85
+ when Nokogiri::XML::Text
86
+ Text.new(source.text)
87
+ when String
88
+ doc = Nokogiri(source)
89
+ if doc.children.any?
90
+ parse(doc.children)
91
+ else
92
+ Text.new(source)
93
+ end
94
+ else
95
+ raise ParseError, "unexpected #{source.inspect}"
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def parse_element(source)
102
+ case source.name
103
+ when 'p'
104
+ block = Block.build(parse(source.children), **@current_block_attributes)
105
+ reset_block
106
+ block
107
+ when 'element'
108
+ span = Span.build(
109
+ parse(source.children),
110
+ kind: source[:kind],
111
+ **@current_span_attributes
112
+ )
113
+ reset_span
114
+ span
115
+ when 'tag'
116
+ @current_block_attributes[:kind] = source[:kind] if source[:kind]
117
+ @current_block_attributes[:level] += 1 if source.text == "\t"
118
+ nil
119
+ when 'attribute'
120
+ case source[:identifier]
121
+ when 'syntax'
122
+ @current_block_attributes[:syntax] = source.text
123
+ when 'text'
124
+ @current_span_attributes[:text] = parse_fragment(source)
125
+ when 'size'
126
+ parse(source.children)
127
+ else
128
+ @current_span_attributes[source[:identifier].downcase.to_sym] = source.text
129
+ end
130
+ nil
131
+ when 'size'
132
+ @current_span_attributes[:width] = source[:width].to_i
133
+ @current_span_attributes[:height] = source[:height].to_i
134
+ nil
135
+ when 'tags'
136
+ parse(source.children)
137
+ nil
138
+ else
139
+ raise ParseError, "unknown element name #{source.name}"
140
+ end
141
+ end
142
+
143
+ def parse_keywords(element)
144
+ element
145
+ .xpath('./attachment[@type="keywords"]')
146
+ .flat_map { |el| el.text.split(',') }
147
+ end
148
+
149
+ def parse_files(element)
150
+ element
151
+ .xpath('./attachment[@type="file"]')
152
+ .map(&:text)
153
+ end
154
+
155
+ def parse_notes(element)
156
+ element
157
+ .xpath('./attachment[@type="note"]')
158
+ .map { |el| parse_fragment(el) }
159
+ end
160
+
161
+ def reset
162
+ reset_block
163
+ reset_span
164
+ end
165
+
166
+ def reset_block
167
+ @current_block_attributes = { kind: 'paragraph', level: 0 }
168
+ end
169
+
170
+ def reset_span
171
+ @current_span_attributes = {}
172
+ end
173
+ end
174
+ end