marquery 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/CHANGELOG.md +50 -0
- data/LICENSE +21 -0
- data/README.md +473 -0
- data/lib/marquery/asset_handler.rb +60 -0
- data/lib/marquery/attributable.rb +70 -0
- data/lib/marquery/collection.rb +38 -0
- data/lib/marquery/entry.rb +9 -0
- data/lib/marquery/error.rb +19 -0
- data/lib/marquery/helpers.rb +26 -0
- data/lib/marquery/index.rb +9 -0
- data/lib/marquery/markdown_to_html.rb +9 -0
- data/lib/marquery/model.rb +60 -0
- data/lib/marquery/order.rb +16 -0
- data/lib/marquery/parser.rb +144 -0
- data/lib/marquery/query.rb +189 -0
- data/lib/marquery/registry.rb +24 -0
- data/lib/marquery/renderable.rb +54 -0
- data/lib/marquery/renderer.rb +28 -0
- data/lib/marquery/version.rb +5 -0
- data/lib/marquery.rb +47 -0
- metadata +82 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "attributable"
|
|
4
|
+
require_relative "renderable"
|
|
5
|
+
|
|
6
|
+
module Marquery
|
|
7
|
+
module Collection
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.include(Marquery::Renderable)
|
|
10
|
+
base.include(Marquery::Attributable)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :title, :description, :content
|
|
14
|
+
|
|
15
|
+
def initialize(attrs = {})
|
|
16
|
+
attrs = attrs.transform_keys(&:to_sym)
|
|
17
|
+
assign_standard_attributes(attrs)
|
|
18
|
+
assign_declared_attributes(attrs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
{title:, description:, content:, assets:}.tap do |base|
|
|
23
|
+
self.class.attributes.each_key do |name|
|
|
24
|
+
base[name] = public_send(name)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def assign_standard_attributes(attrs)
|
|
32
|
+
@title = attrs.fetch(:title, "")
|
|
33
|
+
@description = attrs[:description]
|
|
34
|
+
@content = attrs.fetch(:content, "")
|
|
35
|
+
@assets = attrs.fetch(:assets, {})
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Marquery
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class EntryNotFound < Error
|
|
7
|
+
def initialize(slug)
|
|
8
|
+
super("Entry not found: #{slug}")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class AssetNotFound < Error
|
|
13
|
+
def initialize(name)
|
|
14
|
+
super("Asset not found: #{name}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class ParseError < Error; end
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "renderable"
|
|
4
|
+
require_relative "renderer"
|
|
5
|
+
|
|
6
|
+
module Marquery
|
|
7
|
+
module Helpers
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
def markdown(content, renderer: Marquery::Renderer)
|
|
11
|
+
case content
|
|
12
|
+
when nil
|
|
13
|
+
""
|
|
14
|
+
when Marquery::Renderable
|
|
15
|
+
content.to_html
|
|
16
|
+
when String
|
|
17
|
+
renderer.new.markdown_to_html(content)
|
|
18
|
+
else
|
|
19
|
+
raise(
|
|
20
|
+
ArgumentError,
|
|
21
|
+
"markdown expects a String, nil, or Marquery::Renderable, got #{content.class}"
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "attributable"
|
|
4
|
+
require_relative "renderable"
|
|
5
|
+
|
|
6
|
+
module Marquery
|
|
7
|
+
module Model
|
|
8
|
+
def self.included(base)
|
|
9
|
+
base.include(Marquery::Renderable)
|
|
10
|
+
base.include(Marquery::Attributable)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :slug, :title, :description, :content, :date, :source
|
|
14
|
+
|
|
15
|
+
def initialize(attrs = {})
|
|
16
|
+
attrs = attrs.transform_keys(&:to_sym)
|
|
17
|
+
assign_standard_attributes(attrs)
|
|
18
|
+
assign_declared_attributes(attrs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def active? = @active
|
|
22
|
+
|
|
23
|
+
def ==(other)
|
|
24
|
+
other.is_a?(self.class) && other.slug == slug
|
|
25
|
+
end
|
|
26
|
+
alias_method :eql?, :==
|
|
27
|
+
|
|
28
|
+
def hash = [self.class, slug].hash
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
{
|
|
32
|
+
slug:,
|
|
33
|
+
title:,
|
|
34
|
+
description:,
|
|
35
|
+
content:,
|
|
36
|
+
date:,
|
|
37
|
+
active: active?,
|
|
38
|
+
source:,
|
|
39
|
+
assets:
|
|
40
|
+
}.tap do |base|
|
|
41
|
+
self.class.attributes.each_key do |name|
|
|
42
|
+
base[name] = public_send(name)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def assign_standard_attributes(attrs)
|
|
50
|
+
@slug = attrs.fetch(:slug)
|
|
51
|
+
@title = attrs.fetch(:title)
|
|
52
|
+
@description = attrs[:description]
|
|
53
|
+
@content = attrs.fetch(:content, "")
|
|
54
|
+
@date = attrs[:date]
|
|
55
|
+
@active = attrs.fetch(:active, true)
|
|
56
|
+
@source = attrs[:source]
|
|
57
|
+
@assets = attrs.fetch(:assets, {})
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Marquery
|
|
4
|
+
module Order
|
|
5
|
+
ASC = :asc
|
|
6
|
+
DESC = :desc
|
|
7
|
+
|
|
8
|
+
VALID = [ASC, DESC].freeze
|
|
9
|
+
|
|
10
|
+
def self.validate!(value)
|
|
11
|
+
return value if VALID.include?(value)
|
|
12
|
+
|
|
13
|
+
raise ArgumentError, "Invalid order: #{value.inspect} (expected :asc or :desc)"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "time"
|
|
6
|
+
require "yaml"
|
|
7
|
+
|
|
8
|
+
require_relative "error"
|
|
9
|
+
require_relative "order"
|
|
10
|
+
|
|
11
|
+
module Marquery
|
|
12
|
+
class Parser
|
|
13
|
+
DATE_REGEX = /\A(?<date>\d{8})_(?<name>[^.]+)/
|
|
14
|
+
CONTENT_REGEX = /\A(?:---\n(?<frontmatter>.*?)\n---\n)?(?<body>.*)\z/m
|
|
15
|
+
ASSET_EXTENSIONS = %w[.avif .gif .jpeg .jpg .mp3 .mp4 .ogg .pdf .png .svg .webm .webp].freeze
|
|
16
|
+
PERMITTED_YAML_CLASSES = [Date, Time, Symbol].freeze
|
|
17
|
+
|
|
18
|
+
Result = Struct.new(:index, :entries, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
def initialize(dir:, model:, index:, order_by:, assets_dir: nil)
|
|
21
|
+
@dir = Pathname.new(dir.to_s)
|
|
22
|
+
@assets_dir = assets_dir ? Pathname.new(assets_dir.to_s) : nil
|
|
23
|
+
@model = model
|
|
24
|
+
@index = index
|
|
25
|
+
@order_field, @order_direction = order_by
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call
|
|
29
|
+
Result.new(index: load_index, entries: sort(load_entries))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def load_index
|
|
35
|
+
file = @dir.join("_index.md")
|
|
36
|
+
return @index.new unless file.file?
|
|
37
|
+
|
|
38
|
+
match = match_content(file.read)
|
|
39
|
+
attrs = {
|
|
40
|
+
content: match[:body].to_s.strip,
|
|
41
|
+
assets: collect_assets(@dir.join("_index"))
|
|
42
|
+
}
|
|
43
|
+
attrs.merge!(parse_frontmatter(match[:frontmatter]))
|
|
44
|
+
@index.new(**attrs)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def load_entries
|
|
48
|
+
return [] unless @dir.directory?
|
|
49
|
+
|
|
50
|
+
Pathname
|
|
51
|
+
.glob(@dir.join("*.md"))
|
|
52
|
+
.reject { _1.basename.to_s == "_index.md" }
|
|
53
|
+
.map { load_entry(_1) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_entry(path)
|
|
57
|
+
basename = path.basename.to_s
|
|
58
|
+
unless filename_match = basename.match(DATE_REGEX)
|
|
59
|
+
raise Marquery::ParseError, "Invalid filename: #{path}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
name = filename_match[:name]
|
|
63
|
+
date_str = filename_match[:date]
|
|
64
|
+
content_match = match_content(path.read)
|
|
65
|
+
|
|
66
|
+
attrs = {
|
|
67
|
+
slug: parameterize(name),
|
|
68
|
+
title: humanize(name),
|
|
69
|
+
date: parse_date(date_str),
|
|
70
|
+
source: path.to_s,
|
|
71
|
+
content: content_match[:body].to_s.strip,
|
|
72
|
+
assets: assets_for(path, date_str)
|
|
73
|
+
}
|
|
74
|
+
attrs.merge!(parse_frontmatter(content_match[:frontmatter]))
|
|
75
|
+
@model.new(**attrs)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def assets_for(path, date_str)
|
|
79
|
+
hash = {}
|
|
80
|
+
if @assets_dir
|
|
81
|
+
hash.merge!(collect_assets(@assets_dir.join("_shared")))
|
|
82
|
+
hash.merge!(collect_assets(@assets_dir.join(date_str)))
|
|
83
|
+
end
|
|
84
|
+
hash.merge!(collect_assets(Pathname.new(path.to_s.sub(/\.md\z/, ""))))
|
|
85
|
+
hash
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def collect_assets(dir)
|
|
89
|
+
return {} unless dir.directory?
|
|
90
|
+
|
|
91
|
+
dir.children.sort.each_with_object({}) do |child, memo|
|
|
92
|
+
basename = child.basename.to_s
|
|
93
|
+
next if basename.start_with?(".")
|
|
94
|
+
next if child.directory?
|
|
95
|
+
next unless ASSET_EXTENSIONS.include?(child.extname.downcase)
|
|
96
|
+
|
|
97
|
+
memo[basename] = child.to_s.delete_prefix("./")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def match_content(text)
|
|
102
|
+
match = text.match(CONTENT_REGEX)
|
|
103
|
+
raise Marquery::ParseError, "Unable to parse content" unless match
|
|
104
|
+
|
|
105
|
+
{frontmatter: match[:frontmatter], body: match[:body]}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def parse_frontmatter(text)
|
|
109
|
+
return {} if text.nil? || text.strip.empty?
|
|
110
|
+
|
|
111
|
+
parsed = YAML.safe_load(text, permitted_classes: PERMITTED_YAML_CLASSES, aliases: false)
|
|
112
|
+
raise Marquery::ParseError, "Frontmatter must be a YAML mapping" unless parsed.is_a?(Hash)
|
|
113
|
+
|
|
114
|
+
parsed.transform_keys(&:to_sym)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def parse_date(date_str)
|
|
118
|
+
Time.strptime(date_str, "%Y%m%d")
|
|
119
|
+
rescue ArgumentError => exception
|
|
120
|
+
raise Marquery::ParseError, "Invalid date prefix #{date_str.inspect}: #{exception.message}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def sort(entries)
|
|
124
|
+
sorted = entries.sort_by { _1.public_send(@order_field) }
|
|
125
|
+
@order_direction == Marquery::Order::DESC ? sorted.reverse : sorted
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parameterize(str)
|
|
129
|
+
str
|
|
130
|
+
.tr("_", "-")
|
|
131
|
+
.downcase
|
|
132
|
+
.gsub(/[^a-z0-9-]/, "")
|
|
133
|
+
.squeeze("-")
|
|
134
|
+
.gsub(/\A-|-\z/, "")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def humanize(str)
|
|
138
|
+
cleaned = str.tr("_", " ").tr("-", " ").gsub(/\s+/, " ").strip
|
|
139
|
+
return cleaned if cleaned.empty?
|
|
140
|
+
|
|
141
|
+
cleaned[0].upcase + cleaned[1..]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require_relative "entry"
|
|
5
|
+
require_relative "index"
|
|
6
|
+
require_relative "order"
|
|
7
|
+
require_relative "parser"
|
|
8
|
+
require_relative "registry"
|
|
9
|
+
|
|
10
|
+
module Marquery
|
|
11
|
+
module Query
|
|
12
|
+
extend Forwardable
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
def self.included(base)
|
|
16
|
+
base.extend(ClassMethods)
|
|
17
|
+
base.instance_variable_set(:@marquery_model, Marquery::Entry)
|
|
18
|
+
base.instance_variable_set(:@marquery_index, Marquery::Index)
|
|
19
|
+
base.instance_variable_set(:@order_field, :date)
|
|
20
|
+
base.instance_variable_set(:@order_direction, Marquery::Order::DESC)
|
|
21
|
+
base.instance_variable_set(:@dir, nil)
|
|
22
|
+
base.instance_variable_set(:@assets_dir, nil)
|
|
23
|
+
base.instance_variable_set(:@marquery_data, nil)
|
|
24
|
+
base.instance_variable_set(:@loaded, false)
|
|
25
|
+
Marquery::Registry.register(base)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
def model(klass = nil)
|
|
30
|
+
@marquery_model = klass if klass
|
|
31
|
+
@marquery_model
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def index(klass = nil)
|
|
35
|
+
@marquery_index = klass if klass
|
|
36
|
+
@marquery_index
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def order_by(field = nil, direction = nil)
|
|
40
|
+
if field
|
|
41
|
+
@order_field = field.to_sym
|
|
42
|
+
@order_direction = Marquery::Order.validate!(direction) if direction
|
|
43
|
+
end
|
|
44
|
+
[@order_field, @order_direction]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def dir(path = nil)
|
|
48
|
+
@dir = path.to_s if path
|
|
49
|
+
@dir ||= derive_dir
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def assets_dir(path = nil)
|
|
53
|
+
@assets_dir = path.to_s unless path.nil?
|
|
54
|
+
@assets_dir
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def data_path
|
|
58
|
+
File.join(Marquery.config.data_dir, dir)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def assets_path
|
|
62
|
+
return nil unless assets_dir
|
|
63
|
+
|
|
64
|
+
File.join(Marquery.config.data_dir, assets_dir)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def loaded?
|
|
68
|
+
@loaded
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def load!
|
|
72
|
+
@marquery_data = Marquery::Parser.new(
|
|
73
|
+
dir: data_path,
|
|
74
|
+
assets_dir: assets_path,
|
|
75
|
+
model: model,
|
|
76
|
+
index: index,
|
|
77
|
+
order_by: order_by
|
|
78
|
+
).call
|
|
79
|
+
@loaded = true
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def reload!
|
|
84
|
+
@loaded = false
|
|
85
|
+
@marquery_data = nil
|
|
86
|
+
load!
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def all_entries
|
|
90
|
+
load! unless loaded?
|
|
91
|
+
@marquery_data.entries
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def index_entry
|
|
95
|
+
load! unless loaded?
|
|
96
|
+
@marquery_data.index
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def derive_dir
|
|
102
|
+
derived = name
|
|
103
|
+
.to_s
|
|
104
|
+
.sub(/Query\z/, "")
|
|
105
|
+
.gsub("::", "")
|
|
106
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
107
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
108
|
+
.downcase
|
|
109
|
+
|
|
110
|
+
return derived unless derived.empty?
|
|
111
|
+
|
|
112
|
+
raise(
|
|
113
|
+
Marquery::Error,
|
|
114
|
+
"Cannot derive directory for #{inspect}; call `dir \"...\"` explicitly"
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
attr_reader :entries
|
|
120
|
+
|
|
121
|
+
def initialize(entries = nil)
|
|
122
|
+
@entries = (entries || self.class.all_entries).freeze
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def each(&block)
|
|
126
|
+
return enum_for(:each) unless block
|
|
127
|
+
|
|
128
|
+
entries.each(&block)
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def all = entries
|
|
133
|
+
|
|
134
|
+
def_delegators :entries, :size, :length, :count, :empty?, :first, :last
|
|
135
|
+
|
|
136
|
+
def find(slug)
|
|
137
|
+
find_by_slug(slug) || raise(Marquery::EntryNotFound.new(slug))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def find_by_slug(slug)
|
|
141
|
+
entries.find { |entry| entry.slug == slug }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def filter(&block)
|
|
145
|
+
return self unless block
|
|
146
|
+
|
|
147
|
+
new_query(entries.select(&block))
|
|
148
|
+
end
|
|
149
|
+
alias_method :select, :filter
|
|
150
|
+
|
|
151
|
+
def reject(&block)
|
|
152
|
+
return self unless block
|
|
153
|
+
|
|
154
|
+
new_query(entries.reject(&block))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def sort_by(&block)
|
|
158
|
+
new_query(entries.sort_by(&block))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def reverse
|
|
162
|
+
new_query(entries.reverse)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def shuffle(random: Random.new)
|
|
166
|
+
new_query(entries.shuffle(random: random))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def previous(entry)
|
|
170
|
+
idx = entries.index(entry)
|
|
171
|
+
return nil if idx.nil? || idx.zero?
|
|
172
|
+
|
|
173
|
+
entries[idx - 1]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def next(entry)
|
|
177
|
+
idx = entries.index(entry)
|
|
178
|
+
return nil if idx.nil?
|
|
179
|
+
|
|
180
|
+
entries[idx + 1]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def new_query(new_entries)
|
|
186
|
+
self.class.new(new_entries)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Marquery
|
|
4
|
+
module Registry
|
|
5
|
+
@classes = []
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def classes
|
|
10
|
+
@mutex.synchronize { @classes.dup }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def register(klass)
|
|
14
|
+
@mutex.synchronize do
|
|
15
|
+
@classes << klass unless @classes.include?(klass)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def reset!
|
|
20
|
+
@mutex.synchronize { @classes.clear }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "renderer"
|
|
4
|
+
|
|
5
|
+
module Marquery
|
|
6
|
+
ASSET_URI_REGEX = %r{asset:([^\s)"'<>]+)}
|
|
7
|
+
|
|
8
|
+
module Renderable
|
|
9
|
+
def self.included(base)
|
|
10
|
+
base.extend(ClassMethods)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
def renderer(klass = nil)
|
|
15
|
+
if klass
|
|
16
|
+
@marquery_renderer = klass
|
|
17
|
+
return klass
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
return @marquery_renderer if defined?(@marquery_renderer) && @marquery_renderer
|
|
21
|
+
return superclass.renderer if superclass.respond_to?(:renderer)
|
|
22
|
+
|
|
23
|
+
Marquery::Renderer
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def assets = @assets ||= {}
|
|
28
|
+
|
|
29
|
+
def asset(name)
|
|
30
|
+
path = assets[name] || raise(Marquery::AssetNotFound.new(name))
|
|
31
|
+
"/#{path}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def asset?(name)
|
|
35
|
+
path = assets[name]
|
|
36
|
+
path && "/#{path}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def rewrite_assets(raw)
|
|
40
|
+
raw.gsub(Marquery::ASSET_URI_REGEX) { asset(::Regexp.last_match(1)) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def process_content(raw)
|
|
44
|
+
preprocessor = Marquery.config.preprocessor
|
|
45
|
+
return preprocessor.call(raw, self) if preprocessor
|
|
46
|
+
|
|
47
|
+
rewrite_assets(raw)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_html
|
|
51
|
+
self.class.renderer.new.markdown_to_html(process_content(content))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "commonmarker"
|
|
4
|
+
|
|
5
|
+
require_relative "markdown_to_html"
|
|
6
|
+
|
|
7
|
+
module Marquery
|
|
8
|
+
class Renderer
|
|
9
|
+
include MarkdownToHtml
|
|
10
|
+
|
|
11
|
+
DEFAULT_OPTIONS = {
|
|
12
|
+
parse: {smart: true},
|
|
13
|
+
render: {unsafe: true, github_pre_lang: true},
|
|
14
|
+
extension: {
|
|
15
|
+
strikethrough: true,
|
|
16
|
+
table: true,
|
|
17
|
+
autolink: true,
|
|
18
|
+
tagfilter: true,
|
|
19
|
+
tasklist: true,
|
|
20
|
+
footnotes: true
|
|
21
|
+
}
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def markdown_to_html(content)
|
|
25
|
+
Commonmarker.to_html(content.strip, options: DEFAULT_OPTIONS)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/marquery.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "marquery/version"
|
|
4
|
+
require_relative "marquery/error"
|
|
5
|
+
require_relative "marquery/order"
|
|
6
|
+
require_relative "marquery/markdown_to_html"
|
|
7
|
+
require_relative "marquery/renderer"
|
|
8
|
+
require_relative "marquery/renderable"
|
|
9
|
+
require_relative "marquery/attributable"
|
|
10
|
+
require_relative "marquery/model"
|
|
11
|
+
require_relative "marquery/entry"
|
|
12
|
+
require_relative "marquery/collection"
|
|
13
|
+
require_relative "marquery/index"
|
|
14
|
+
require_relative "marquery/parser"
|
|
15
|
+
require_relative "marquery/registry"
|
|
16
|
+
require_relative "marquery/query"
|
|
17
|
+
require_relative "marquery/helpers"
|
|
18
|
+
|
|
19
|
+
module Marquery
|
|
20
|
+
class Configuration
|
|
21
|
+
attr_accessor :data_dir, :preprocessor
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@data_dir = "marquery"
|
|
25
|
+
@preprocessor = nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def config
|
|
31
|
+
@config ||= Configuration.new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def configure
|
|
35
|
+
yield config
|
|
36
|
+
config
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset_config!
|
|
40
|
+
@config = Configuration.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def eager_load!
|
|
44
|
+
Marquery::Registry.classes.each(&:load!)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|