polites 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|