polites 0.1.0

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.
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