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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/.tool-versions +1 -0
- data/.travis.yml +6 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +128 -0
- data/LICENSE.txt +21 -0
- data/README.md +70 -0
- data/Rakefile +9 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/polites +8 -0
- data/lib/polites.rb +33 -0
- data/lib/polites/block.rb +65 -0
- data/lib/polites/block/blockquote.rb +8 -0
- data/lib/polites/block/code_block.rb +18 -0
- data/lib/polites/block/divider.rb +8 -0
- data/lib/polites/block/heading.rb +8 -0
- data/lib/polites/block/heading1.rb +8 -0
- data/lib/polites/block/heading2.rb +8 -0
- data/lib/polites/block/heading3.rb +8 -0
- data/lib/polites/block/heading4.rb +8 -0
- data/lib/polites/block/heading5.rb +8 -0
- data/lib/polites/block/heading6.rb +8 -0
- data/lib/polites/block/list.rb +14 -0
- data/lib/polites/block/ordered_list.rb +8 -0
- data/lib/polites/block/paragraph.rb +8 -0
- data/lib/polites/block/unordered_list.rb +8 -0
- data/lib/polites/cli.rb +52 -0
- data/lib/polites/convert.rb +29 -0
- data/lib/polites/doc/_index.html +85 -0
- data/lib/polites/doc/class_list.html +51 -0
- data/lib/polites/doc/css/common.css +1 -0
- data/lib/polites/doc/css/full_list.css +58 -0
- data/lib/polites/doc/css/style.css +496 -0
- data/lib/polites/doc/file_list.html +51 -0
- data/lib/polites/doc/frames.html +17 -0
- data/lib/polites/doc/index.html +85 -0
- data/lib/polites/doc/js/app.js +314 -0
- data/lib/polites/doc/js/full_list.js +216 -0
- data/lib/polites/doc/js/jquery.js +4 -0
- data/lib/polites/doc/method_list.html +51 -0
- data/lib/polites/doc/top-level-namespace.html +100 -0
- data/lib/polites/file.rb +67 -0
- data/lib/polites/html_formatter.rb +119 -0
- data/lib/polites/list_indenter.rb +34 -0
- data/lib/polites/markup.rb +31 -0
- data/lib/polites/nanoc.rb +46 -0
- data/lib/polites/nanoc/data_source.rb +93 -0
- data/lib/polites/nanoc/embedded_images_filter.rb +22 -0
- data/lib/polites/nanoc/extract_file_filter.rb +21 -0
- data/lib/polites/node.rb +28 -0
- data/lib/polites/parser.rb +174 -0
- data/lib/polites/plist.rb +28 -0
- data/lib/polites/range_tag.rb +29 -0
- data/lib/polites/settings.rb +35 -0
- data/lib/polites/sheet.rb +63 -0
- data/lib/polites/simple_tag.rb +22 -0
- data/lib/polites/span.rb +60 -0
- data/lib/polites/span/annotation.rb +18 -0
- data/lib/polites/span/code.rb +8 -0
- data/lib/polites/span/delete.rb +8 -0
- data/lib/polites/span/emph.rb +8 -0
- data/lib/polites/span/footnote.rb +18 -0
- data/lib/polites/span/image.rb +29 -0
- data/lib/polites/span/link.rb +21 -0
- data/lib/polites/span/mark.rb +8 -0
- data/lib/polites/span/strong.rb +8 -0
- data/lib/polites/tag.rb +20 -0
- data/lib/polites/text.rb +20 -0
- data/lib/polites/version.rb +5 -0
- data/polites-nanoc.gemspec +31 -0
- data/polites.gemspec +33 -0
- 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
|
data/lib/polites/node.rb
ADDED
@@ -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
|