jekyll-obsidian-site 0.2.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/LICENSE +21 -0
- data/README.md +133 -0
- data/website/_config.yml +21 -0
- data/website/_includes/website/archive-timeline.html +8 -0
- data/website/_includes/website/blog-ledger.html +19 -0
- data/website/_includes/website/blog-navigation.html +7 -0
- data/website/_includes/website/comments.html +22 -0
- data/website/_includes/website/contact-icon.html +14 -0
- data/website/_includes/website/context-tags.html +7 -0
- data/website/_includes/website/context.html +3 -0
- data/website/_includes/website/dialogs.html +71 -0
- data/website/_includes/website/docs-navigation.html +7 -0
- data/website/_includes/website/docs-tree.html +8 -0
- data/website/_includes/website/footer.html +14 -0
- data/website/_includes/website/github-icon.html +3 -0
- data/website/_includes/website/head.html +41 -0
- data/website/_includes/website/header.html +35 -0
- data/website/_includes/website/language-switcher.html +16 -0
- data/website/_includes/website/local-graph.html +25 -0
- data/website/_includes/website/note-header.html +35 -0
- data/website/_includes/website/outline.html +9 -0
- data/website/_includes/website/page-actions.html +34 -0
- data/website/_includes/website/portfolio-grid.html +36 -0
- data/website/_includes/website/portfolio-topics.html +4 -0
- data/website/_includes/website/post-cards.html +16 -0
- data/website/_includes/website/related-articles.html +7 -0
- data/website/_includes/website/relations.html +16 -0
- data/website/_includes/website/source-actions.html +28 -0
- data/website/_includes/website/system-page.html +31 -0
- data/website/_includes/website/tab-collection.html +28 -0
- data/website/_includes/website/tag-filter.html +12 -0
- data/website/_layouts/website-docs.html +20 -0
- data/website/_layouts/website-minimal.html +93 -0
- data/website/_layouts/website-redirect.html +17 -0
- data/website/assets/chunks/abnfDiagram-N423BO3Z-TSORAEI2.js +1 -0
- data/website/assets/chunks/architecture-TIHT7OUA-OUC5PEC7.js +1 -0
- data/website/assets/chunks/architectureDiagram-T3A2C74G-6P5MB2DJ.js +36 -0
- data/website/assets/chunks/blockDiagram-VBNYF7ZC-ZWINYROZ.js +132 -0
- data/website/assets/chunks/c4Diagram-5PPSVZJV-R5RMEAOT.js +10 -0
- data/website/assets/chunks/chunk-22DRK7VS.js +1 -0
- data/website/assets/chunks/chunk-25REBZ7M.js +2 -0
- data/website/assets/chunks/chunk-2RQCUDTG.js +1 -0
- data/website/assets/chunks/chunk-34FHPXU4.js +1 -0
- data/website/assets/chunks/chunk-57XXJOQN.js +10 -0
- data/website/assets/chunks/chunk-6DCNDVBX.js +2 -0
- data/website/assets/chunks/chunk-6DHFEWGX.js +1 -0
- data/website/assets/chunks/chunk-6VQ47JJS.js +2 -0
- data/website/assets/chunks/chunk-7CSIRG7W.js +127 -0
- data/website/assets/chunks/chunk-A43YELH5.js +1 -0
- data/website/assets/chunks/chunk-AIX6IH4O.js +1 -0
- data/website/assets/chunks/chunk-B3K7RH5O.js +88 -0
- data/website/assets/chunks/chunk-CBGUD2NP.js +1 -0
- data/website/assets/chunks/chunk-D3WOCELE.js +1 -0
- data/website/assets/chunks/chunk-D6FURU6X.js +1 -0
- data/website/assets/chunks/chunk-DGEGT77F.js +1 -0
- data/website/assets/chunks/chunk-DN52R6U7.js +1 -0
- data/website/assets/chunks/chunk-E3T3IFAC.js +1 -0
- data/website/assets/chunks/chunk-EIBFAW37.js +1 -0
- data/website/assets/chunks/chunk-ERWOH47R.js +1 -0
- data/website/assets/chunks/chunk-ETOVTYWF.js +1 -0
- data/website/assets/chunks/chunk-GHD3PFWA.js +231 -0
- data/website/assets/chunks/chunk-GMBKQ7NZ.js +1 -0
- data/website/assets/chunks/chunk-GQM25EPP.js +156 -0
- data/website/assets/chunks/chunk-GSOU4UTT.js +1 -0
- data/website/assets/chunks/chunk-HEMELP7J.js +1 -0
- data/website/assets/chunks/chunk-HZ5I5NJO.js +1 -0
- data/website/assets/chunks/chunk-IP56FOSI.js +70 -0
- data/website/assets/chunks/chunk-L7BMR7WL.js +1 -0
- data/website/assets/chunks/chunk-LGUG3SQB.js +1 -0
- data/website/assets/chunks/chunk-LOMHZYS2.js +1 -0
- data/website/assets/chunks/chunk-MII4VZEB.js +3 -0
- data/website/assets/chunks/chunk-MYMJLHQL.js +62 -0
- data/website/assets/chunks/chunk-NBH7YOBD.js +15 -0
- data/website/assets/chunks/chunk-NCOVM3YF.js +1 -0
- data/website/assets/chunks/chunk-OAGTSILX.js +1 -0
- data/website/assets/chunks/chunk-OCX5OCIU.js +321 -0
- data/website/assets/chunks/chunk-OE3NOVIH.js +1 -0
- data/website/assets/chunks/chunk-PLP5XUTJ.js +1 -0
- data/website/assets/chunks/chunk-PNOERP6G.js +1 -0
- data/website/assets/chunks/chunk-QBPQ7DPE.js +1 -0
- data/website/assets/chunks/chunk-RWFFDL53.js +1 -0
- data/website/assets/chunks/chunk-SEGIW3MP.js +1 -0
- data/website/assets/chunks/chunk-SZDMZNUS.js +1 -0
- data/website/assets/chunks/chunk-T2QBAPVT.js +1 -0
- data/website/assets/chunks/chunk-UE533OZ7.js +1 -0
- data/website/assets/chunks/chunk-UTWOJE2A.js +1 -0
- data/website/assets/chunks/chunk-VOCDYA3H.js +1 -0
- data/website/assets/chunks/chunk-VYZZPKGK.js +161 -0
- data/website/assets/chunks/chunk-WWLZIRS6.js +1 -0
- data/website/assets/chunks/chunk-XNXYDBKX.js +32 -0
- data/website/assets/chunks/chunk-XQUIO562.js +1 -0
- data/website/assets/chunks/chunk-Y2XVFW7T.js +2 -0
- data/website/assets/chunks/chunk-YORIST7Z.js +1 -0
- data/website/assets/chunks/chunk-Z2WBQPVY.js +206 -0
- data/website/assets/chunks/classDiagram-JCYQIIEL-OFDVDDIM.js +1 -0
- data/website/assets/chunks/classDiagram-v2-OCEON4UE-E36B6BGQ.js +1 -0
- data/website/assets/chunks/cose-bilkent-JH36ORCC-CBAW64AB.js +1 -0
- data/website/assets/chunks/cynefin-VYW2F7L2-CBO6DLNV.js +1 -0
- data/website/assets/chunks/cynefinDiagram-MW4NZA55-DFINLOUY.js +62 -0
- data/website/assets/chunks/dagre-VZM6K2ZE-BZFBSZAH.js +4 -0
- data/website/assets/chunks/diagram-7IWD3JNH-XQHTVOU2.js +30 -0
- data/website/assets/chunks/diagram-B4RE2ZJO-FTSXBZJR.js +3 -0
- data/website/assets/chunks/diagram-LBJQPF4R-ZSZW4OLH.js +24 -0
- data/website/assets/chunks/diagram-Q27KOJAE-2IV5TVCJ.js +24 -0
- data/website/assets/chunks/diagram-UB23O5K3-D6PDUTFL.js +41 -0
- data/website/assets/chunks/ebnfDiagram-BXEA7PRR-C6MAER3M.js +1 -0
- data/website/assets/chunks/erDiagram-JOGREHBK-EDAD2H5M.js +85 -0
- data/website/assets/chunks/eventmodeling-45OFAUF4-4K577DMF.js +1 -0
- data/website/assets/chunks/flowDiagram-UKHOOZJN-545F4YX3.js +1 -0
- data/website/assets/chunks/ganttDiagram-PKOTCBZU-7A7EKMY3.js +292 -0
- data/website/assets/chunks/gitGraph-TEB2WS4Q-3WUB7WYQ.js +1 -0
- data/website/assets/chunks/gitGraphDiagram-DS77QQ5N-A5UWV5FF.js +106 -0
- data/website/assets/chunks/info-DKCQHKI2-EOOHI7XS.js +1 -0
- data/website/assets/chunks/infoDiagram-6WML65LV-CWIOOWJP.js +2 -0
- data/website/assets/chunks/ishikawaDiagram-WSZJBQD7-CEASQVDL.js +70 -0
- data/website/assets/chunks/journeyDiagram-NVQOT4AX-LYG3S2AW.js +139 -0
- data/website/assets/chunks/kanban-definition-27J2QSJJ-HCC6KAYI.js +89 -0
- data/website/assets/chunks/katex-F4MQ6FB3.js +257 -0
- data/website/assets/chunks/mindmap-definition-FAOFIHXS-JMGSM3RE.js +96 -0
- data/website/assets/chunks/packet-7NZHBO7P-W3SJA3MY.js +1 -0
- data/website/assets/chunks/pegDiagram-VL7TDLO6-4JO3Y3Z3.js +1 -0
- data/website/assets/chunks/pie-RZYD4A2V-YOIQBTY7.js +1 -0
- data/website/assets/chunks/pieDiagram-7S7Q4E2Y-VFQ7SE46.js +39 -0
- data/website/assets/chunks/quadrantDiagram-CIZ2JOQS-JISDI2S6.js +7 -0
- data/website/assets/chunks/radar-I7S5WNFK-NMKP5AZO.js +1 -0
- data/website/assets/chunks/railroad-3IZDKUUU-7MOIV7CI.js +1 -0
- data/website/assets/chunks/railroad-abnf-AHOZXSZD-HXNH5SLO.js +1 -0
- data/website/assets/chunks/railroad-ebnf-EBAXGLYW-OWDJEWMK.js +1 -0
- data/website/assets/chunks/railroad-peg-LSFZ7HO6-ASSINGP5.js +1 -0
- data/website/assets/chunks/railroadDiagram-AXF67PYL-AFV5VP5F.js +1 -0
- data/website/assets/chunks/requirementDiagram-LRYGKXZP-RNRPEHHN.js +84 -0
- data/website/assets/chunks/sankeyDiagram-W5VNT64P-O54R2L3E.js +40 -0
- data/website/assets/chunks/sequenceDiagram-SI44F4Z6-42WISKJ7.js +162 -0
- data/website/assets/chunks/sizeCapture-X5ZJPWSS-LRN2Q37T.js +1 -0
- data/website/assets/chunks/stateDiagram-OKZ733FA-BWIBJXIR.js +1 -0
- data/website/assets/chunks/stateDiagram-v2-UEYNNEHI-L73LHL3L.js +1 -0
- data/website/assets/chunks/swimlanes-SLNWSIFB-ECOAE7LQ.js +1 -0
- data/website/assets/chunks/swimlanesDiagram-ULZ7WXOC-3CKKKFZZ.js +8 -0
- data/website/assets/chunks/timeline-definition-Z64GVDOM-HR2OL5G7.js +120 -0
- data/website/assets/chunks/treeView-QDETBFTQ-C6D5WFJA.js +1 -0
- data/website/assets/chunks/treemap-6X3UGDF4-JFCDMSCA.js +1 -0
- data/website/assets/chunks/vennDiagram-T6HMQDX7-62WAAPTF.js +34 -0
- data/website/assets/chunks/wardley-OPB4EBWU-RQIOII7I.js +1 -0
- data/website/assets/chunks/wardleyDiagram-T6FBY63Y-KUXO3NHV.js +78 -0
- data/website/assets/chunks/xychartDiagram-ELKLHX3M-I54W4HJ7.js +7 -0
- data/website/assets/color-scheme-bootstrap-4H4ZR4BW.js +1 -0
- data/website/assets/docs-3WBBQT6K.css +1 -0
- data/website/assets/docs-W3D5DWQF.js +1 -0
- data/website/assets/docs-navigation-75ALFYBA.js +1 -0
- data/website/assets/graph-QWL54LWD.js +1 -0
- data/website/assets/manifest.json +314 -0
- data/website/assets/math-7JTH4OWZ.js +21 -0
- data/website/assets/mermaid-RXC6URG4.js +10 -0
- data/website/assets/minimal-3NPTDWQX.js +1 -0
- data/website/assets/minimal-E2VATXXS.css +1 -0
- data/website/assets/previews-FMHOX7UX.js +1 -0
- data/website/assets/search-BH73JZDU.js +1 -0
- data/website/assets/search-worker-SM4IM4J7.js +1 -0
- data/website/exe/jekyll-obsidian +5 -0
- data/website/lib/jekyll_obsidian/adapter.rb +733 -0
- data/website/lib/jekyll_obsidian/built_in_themes.rb +855 -0
- data/website/lib/jekyll_obsidian/cli.rb +84 -0
- data/website/lib/jekyll_obsidian/content_policy.rb +194 -0
- data/website/lib/jekyll_obsidian/destination_registry.rb +39 -0
- data/website/lib/jekyll_obsidian/directory_indexes.rb +30 -0
- data/website/lib/jekyll_obsidian/external_media.rb +371 -0
- data/website/lib/jekyll_obsidian/front_matter.rb +453 -0
- data/website/lib/jekyll_obsidian/github_markdown.rb +664 -0
- data/website/lib/jekyll_obsidian/html_publication.rb +206 -0
- data/website/lib/jekyll_obsidian/initializer.rb +68 -0
- data/website/lib/jekyll_obsidian/localized_compiler.rb +1044 -0
- data/website/lib/jekyll_obsidian/media_policy.rb +41 -0
- data/website/lib/jekyll_obsidian/ofm_scanner.rb +493 -0
- data/website/lib/jekyll_obsidian/output_text.rb +13 -0
- data/website/lib/jekyll_obsidian/preview.rb +93 -0
- data/website/lib/jekyll_obsidian/published_markdown.rb +32 -0
- data/website/lib/jekyll_obsidian/raw_html_manifest.rb +66 -0
- data/website/lib/jekyll_obsidian/runtime.rb +197 -0
- data/website/lib/jekyll_obsidian/site_compilation.rb +147 -0
- data/website/lib/jekyll_obsidian/site_navigation.rb +517 -0
- data/website/lib/jekyll_obsidian/url_builder.rb +163 -0
- data/website/lib/jekyll_obsidian/value_objects.rb +198 -0
- data/website/lib/jekyll_obsidian/vault_compiler.rb +2771 -0
- data/website/lib/jekyll_obsidian/version.rb +6 -0
- data/website/lib/jekyll_obsidian/workspace_layout.rb +104 -0
- data/website/lib/jekyll_obsidian.rb +22 -0
- data/website/scripts/audit-site.rb +120 -0
- data/website/scripts/templates/pages.yml +55 -0
- data/website/scripts/verify-site-urls.rb +585 -0
- metadata +319 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "psych"
|
|
5
|
+
|
|
6
|
+
module JekyllObsidian
|
|
7
|
+
class FrontMatter
|
|
8
|
+
XML_INVALID_CHARACTER = OutputText::INVALID_CHARACTER
|
|
9
|
+
SUPPORTED = %w[
|
|
10
|
+
publish title subtitle aliases tags author categories description permalink image cssclasses created updated
|
|
11
|
+
content_type date pinned nav_order nav_exclude tab tabs comments github_markdown related
|
|
12
|
+
].freeze
|
|
13
|
+
ARRAY_PROPERTIES = %w[aliases tags author categories cssclasses].freeze
|
|
14
|
+
LINK_ARRAY_PROPERTIES = %w[author categories].freeze
|
|
15
|
+
STRING_PROPERTIES = %w[title subtitle description permalink image].freeze
|
|
16
|
+
CONTENT_TYPES = %w[post doc page].freeze
|
|
17
|
+
TAB_KEYS = %w[id label order topics].freeze
|
|
18
|
+
TAB_ID_PATTERN = /\A[a-z][a-z0-9-]*\z/.freeze
|
|
19
|
+
|
|
20
|
+
Result = Struct.new(:properties, :property_links, :body, :diagnostics, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
def self.parse(path, bytes)
|
|
23
|
+
new(path, bytes).parse
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.valid_output_text?(value)
|
|
27
|
+
OutputText.valid?(value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.parse_wiki_link(value)
|
|
31
|
+
return unless value.is_a?(String)
|
|
32
|
+
|
|
33
|
+
match = value.match(/\A\[\[([^\[\]\r\n]+)\]\]\z/u)
|
|
34
|
+
return unless match
|
|
35
|
+
|
|
36
|
+
target, display = match[1].split("|", 2).map { |part| part&.strip }
|
|
37
|
+
return if target.to_s.empty? || display == ""
|
|
38
|
+
|
|
39
|
+
[target, display]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(path, bytes)
|
|
43
|
+
@path = path
|
|
44
|
+
@bytes = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
45
|
+
@diagnostics = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse
|
|
49
|
+
unless @bytes.valid_encoding?
|
|
50
|
+
error("invalid_utf8", "note must be valid UTF-8")
|
|
51
|
+
return result({}, [], "")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
raw, body = split
|
|
55
|
+
return result({}, [], body) unless raw
|
|
56
|
+
|
|
57
|
+
parsed = Psych.safe_load(raw, permitted_classes: [Date, Time], aliases: false) || {}
|
|
58
|
+
unless parsed.is_a?(Hash) && parsed.keys.all? { |key| key.is_a?(String) }
|
|
59
|
+
error("invalid_frontmatter", "frontmatter must be a YAML mapping with string keys")
|
|
60
|
+
return result({}, [], body)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
properties, property_links = validate(parsed, raw)
|
|
64
|
+
result(properties, property_links, body)
|
|
65
|
+
rescue Psych::Exception => exception
|
|
66
|
+
error("invalid_frontmatter", "invalid YAML frontmatter: #{exception.problem || exception.message}")
|
|
67
|
+
result({}, [], body || "")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def split
|
|
73
|
+
return [nil, @bytes] unless @bytes.start_with?("---\n", "---\r\n")
|
|
74
|
+
|
|
75
|
+
lines = @bytes.lines
|
|
76
|
+
closing = lines.each_index.drop(1).find { |index| %W[---\n ---\r\n ...\n ...\r\n].include?(lines[index]) }
|
|
77
|
+
unless closing
|
|
78
|
+
error("invalid_frontmatter", "frontmatter is missing a closing delimiter")
|
|
79
|
+
return ["", ""]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
[lines[1...closing].join, lines[(closing + 1)..].to_a.join]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate(parsed, raw)
|
|
86
|
+
unquoted_wiki_links = unquoted_wiki_link_properties(raw)
|
|
87
|
+
property_nodes, property_key_nodes = frontmatter_nodes(raw)
|
|
88
|
+
properties = {}
|
|
89
|
+
property_links = []
|
|
90
|
+
parsed.each do |key, value|
|
|
91
|
+
unless SUPPORTED.include?(key)
|
|
92
|
+
unless valid_custom_property_name?(key)
|
|
93
|
+
error(
|
|
94
|
+
"invalid_property",
|
|
95
|
+
"custom property names must be non-empty NFC text without leading, trailing, or control characters",
|
|
96
|
+
span: frontmatter_source_span(property_key_nodes[key])
|
|
97
|
+
)
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
valid, normalized = normalize_custom_property(key, value, property_nodes[key])
|
|
102
|
+
next unless valid
|
|
103
|
+
|
|
104
|
+
properties[key] = normalized
|
|
105
|
+
unless key == "alias"
|
|
106
|
+
property_links.concat(custom_property_links(key, normalized, property_nodes[key]))
|
|
107
|
+
end
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
case key
|
|
112
|
+
when "publish", "comments", "pinned"
|
|
113
|
+
if value == true || value == false
|
|
114
|
+
properties[key] = value
|
|
115
|
+
else
|
|
116
|
+
code = case key
|
|
117
|
+
when "publish" then "invalid_publish"
|
|
118
|
+
when "comments" then "invalid_comments"
|
|
119
|
+
else "invalid_property"
|
|
120
|
+
end
|
|
121
|
+
error(code, "#{key} must be a YAML boolean")
|
|
122
|
+
end
|
|
123
|
+
when *ARRAY_PROPERTIES
|
|
124
|
+
if value.is_a?(Array) && value.all? { |item| self.class.valid_output_text?(item) }
|
|
125
|
+
if LINK_ARRAY_PROPERTIES.include?(key) && value.any? { |item| wiki_link_candidate?(item) && !self.class.parse_wiki_link(item) }
|
|
126
|
+
error("invalid_property", "#{key} wiki links must use [[target]] or [[target|label]] syntax")
|
|
127
|
+
elsif unquoted_wiki_links.include?(key)
|
|
128
|
+
error("invalid_property", "#{key} wiki link entries must use double-quoted YAML strings")
|
|
129
|
+
elsif key == "cssclasses" && !value.all? { |item| item.match?(/\A[-_a-zA-Z][-_a-zA-Z0-9]*\z/) }
|
|
130
|
+
error("invalid_property", "cssclasses entries must be valid CSS class tokens")
|
|
131
|
+
else
|
|
132
|
+
properties[key] = value.dup
|
|
133
|
+
end
|
|
134
|
+
else
|
|
135
|
+
error("invalid_property", "#{key} must be an array of strings")
|
|
136
|
+
end
|
|
137
|
+
when "related"
|
|
138
|
+
normalized, links = validate_related(value, property_nodes[key])
|
|
139
|
+
if normalized
|
|
140
|
+
properties[key] = normalized
|
|
141
|
+
property_links.concat(links)
|
|
142
|
+
end
|
|
143
|
+
when *STRING_PROPERTIES
|
|
144
|
+
if self.class.valid_output_text?(value)
|
|
145
|
+
properties[key] = value
|
|
146
|
+
else
|
|
147
|
+
error("invalid_property", "#{key} must be a string containing only output-safe Unicode characters")
|
|
148
|
+
end
|
|
149
|
+
when "content_type"
|
|
150
|
+
if CONTENT_TYPES.include?(value)
|
|
151
|
+
properties[key] = value
|
|
152
|
+
else
|
|
153
|
+
error("invalid_property", "content_type must be one of: #{CONTENT_TYPES.join(', ')}")
|
|
154
|
+
end
|
|
155
|
+
when "created", "updated", "date"
|
|
156
|
+
normalized = normalize_time(value)
|
|
157
|
+
normalized ? properties[key] = normalized : error("invalid_property", "#{key} must be an ISO-8601 date or date-time")
|
|
158
|
+
when "nav_order"
|
|
159
|
+
if value.is_a?(Integer)
|
|
160
|
+
properties[key] = value
|
|
161
|
+
else
|
|
162
|
+
error("invalid_property", "nav_order must be an integer")
|
|
163
|
+
end
|
|
164
|
+
when "nav_exclude"
|
|
165
|
+
if value == true || value == false
|
|
166
|
+
properties[key] = value
|
|
167
|
+
else
|
|
168
|
+
error("invalid_property", "nav_exclude must be a YAML boolean")
|
|
169
|
+
end
|
|
170
|
+
when "tab"
|
|
171
|
+
normalized = validate_tab(value)
|
|
172
|
+
properties[key] = normalized if normalized
|
|
173
|
+
when "tabs"
|
|
174
|
+
normalized = validate_tabs(value)
|
|
175
|
+
properties[key] = normalized if normalized
|
|
176
|
+
when "github_markdown"
|
|
177
|
+
begin
|
|
178
|
+
reference = GitHubMarkdown.normalize(value)
|
|
179
|
+
properties[key] = {
|
|
180
|
+
"repository" => reference.repository,
|
|
181
|
+
"ref" => reference.ref,
|
|
182
|
+
"path" => reference.path
|
|
183
|
+
}
|
|
184
|
+
rescue GitHubMarkdown::Invalid => exception
|
|
185
|
+
error("invalid_property", exception.message)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
[properties, property_links]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def valid_custom_property_name?(value)
|
|
193
|
+
self.class.valid_output_text?(value) &&
|
|
194
|
+
!value.empty? &&
|
|
195
|
+
!value.match?(/\A\p{Space}|\p{Space}\z/u) &&
|
|
196
|
+
value == value.unicode_normalize(:nfc) &&
|
|
197
|
+
!value.match?(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u)
|
|
198
|
+
rescue EncodingError
|
|
199
|
+
false
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def normalize_custom_property(key, value, property_node)
|
|
203
|
+
if value.is_a?(Array)
|
|
204
|
+
if value.all? { |item| custom_scalar?(item) }
|
|
205
|
+
return [true, value.map { |item| normalize_custom_scalar(item) }]
|
|
206
|
+
end
|
|
207
|
+
elsif custom_scalar?(value)
|
|
208
|
+
return [true, normalize_custom_scalar(value)]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
if unquoted_wiki_link_node?(property_node)
|
|
212
|
+
error("invalid_property", "#{key} wiki links must use double-quoted YAML strings")
|
|
213
|
+
else
|
|
214
|
+
error("invalid_property", "#{key} must be a scalar or a flat array of scalars")
|
|
215
|
+
end
|
|
216
|
+
[false, nil]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def validate_related(value, property_node)
|
|
220
|
+
unless value.is_a?(Array)
|
|
221
|
+
error("invalid_property", "related must be an array of double-quoted wiki links")
|
|
222
|
+
return [nil, []]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
nodes = property_node.is_a?(Psych::Nodes::Sequence) ? property_node.children : []
|
|
226
|
+
links = []
|
|
227
|
+
invalid = false
|
|
228
|
+
value.each_with_index do |item, index|
|
|
229
|
+
if item.is_a?(String) && !self.class.valid_output_text?(item)
|
|
230
|
+
error("invalid_property", "related entries must contain only output-safe Unicode characters")
|
|
231
|
+
invalid = true
|
|
232
|
+
next
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
parsed = self.class.parse_wiki_link(item)
|
|
236
|
+
node = nodes[index]
|
|
237
|
+
unless parsed
|
|
238
|
+
message = if unquoted_wiki_link_node?(node)
|
|
239
|
+
"related wiki links must use double-quoted YAML strings"
|
|
240
|
+
else
|
|
241
|
+
"related entries must use [[target]] or [[target|label]] syntax"
|
|
242
|
+
end
|
|
243
|
+
error("invalid_property", message)
|
|
244
|
+
invalid = true
|
|
245
|
+
next
|
|
246
|
+
end
|
|
247
|
+
unless node.is_a?(Psych::Nodes::Scalar) && node.style == Psych::Nodes::Scalar::DOUBLE_QUOTED
|
|
248
|
+
error("invalid_property", "related wiki links must use double-quoted YAML strings")
|
|
249
|
+
invalid = true
|
|
250
|
+
next
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
target, display = parsed
|
|
254
|
+
links << FrontMatterLink.new(
|
|
255
|
+
property: "related",
|
|
256
|
+
index: index,
|
|
257
|
+
target: target,
|
|
258
|
+
display: display,
|
|
259
|
+
source_span: frontmatter_source_span(node)
|
|
260
|
+
)
|
|
261
|
+
end
|
|
262
|
+
invalid ? [nil, []] : [value.dup, links]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def unquoted_wiki_link_node?(node)
|
|
266
|
+
return false unless node.is_a?(Psych::Nodes::Sequence)
|
|
267
|
+
|
|
268
|
+
node.children.any? do |child|
|
|
269
|
+
child.is_a?(Psych::Nodes::Sequence) &&
|
|
270
|
+
(child.children.all? { |nested| nested.is_a?(Psych::Nodes::Scalar) } || unquoted_wiki_link_node?(child))
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def custom_scalar?(value)
|
|
275
|
+
return true if value.nil? || value == true || value == false || value.is_a?(Integer)
|
|
276
|
+
return value.finite? if value.is_a?(Float)
|
|
277
|
+
return true if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
|
278
|
+
|
|
279
|
+
self.class.valid_output_text?(value)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def normalize_custom_scalar(value)
|
|
283
|
+
return normalize_time(value) if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
|
284
|
+
|
|
285
|
+
value
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def custom_property_links(property, value, property_node)
|
|
289
|
+
values = value.is_a?(Array) ? value : [value]
|
|
290
|
+
nodes = if property_node.is_a?(Psych::Nodes::Sequence)
|
|
291
|
+
property_node.children
|
|
292
|
+
else
|
|
293
|
+
[property_node]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
values.each_with_index.filter_map do |item, index|
|
|
297
|
+
next unless item.is_a?(String)
|
|
298
|
+
|
|
299
|
+
parsed = self.class.parse_wiki_link(item)
|
|
300
|
+
if parsed
|
|
301
|
+
node = nodes[index]
|
|
302
|
+
unless node.is_a?(Psych::Nodes::Scalar) && node.style == Psych::Nodes::Scalar::DOUBLE_QUOTED
|
|
303
|
+
error("invalid_property", "#{property} wiki links must use double-quoted YAML strings")
|
|
304
|
+
next
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
target, display = parsed
|
|
308
|
+
FrontMatterLink.new(
|
|
309
|
+
property: property,
|
|
310
|
+
index: index,
|
|
311
|
+
target: target,
|
|
312
|
+
display: display,
|
|
313
|
+
source_span: frontmatter_source_span(node)
|
|
314
|
+
)
|
|
315
|
+
elsif item.start_with?("[[")
|
|
316
|
+
error("invalid_property", "#{property} wiki links must use [[target]] or [[target|label]] syntax")
|
|
317
|
+
nil
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def frontmatter_nodes(raw)
|
|
323
|
+
mapping = Psych.parse(raw)&.root
|
|
324
|
+
return [{}, {}] unless mapping.is_a?(Psych::Nodes::Mapping)
|
|
325
|
+
|
|
326
|
+
property_nodes = {}
|
|
327
|
+
property_key_nodes = {}
|
|
328
|
+
mapping.children.each_slice(2) do |key_node, value_node|
|
|
329
|
+
next unless key_node.is_a?(Psych::Nodes::Scalar)
|
|
330
|
+
|
|
331
|
+
property_nodes[key_node.value] = value_node
|
|
332
|
+
property_key_nodes[key_node.value] = key_node
|
|
333
|
+
end
|
|
334
|
+
[property_nodes, property_key_nodes]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def frontmatter_source_span(node)
|
|
338
|
+
SourceSpan.new(
|
|
339
|
+
start_line: node.start_line + 2,
|
|
340
|
+
start_column: node.start_column + 1,
|
|
341
|
+
end_line: node.end_line + 2,
|
|
342
|
+
end_column: node.end_column + 1
|
|
343
|
+
)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def validate_tab(value)
|
|
347
|
+
unless value.is_a?(Hash) && value.keys.all? { |key| key.is_a?(String) }
|
|
348
|
+
error("invalid_property", "tab must be a YAML mapping with string keys")
|
|
349
|
+
return nil
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
unknown = value.keys - TAB_KEYS
|
|
353
|
+
unknown.sort.each { |key| error("invalid_property", "unknown tab setting #{key.inspect}") }
|
|
354
|
+
normalized = {}
|
|
355
|
+
if value.key?("id")
|
|
356
|
+
id = value["id"]
|
|
357
|
+
if valid_tab_id?(id)
|
|
358
|
+
normalized["id"] = id
|
|
359
|
+
else
|
|
360
|
+
error("invalid_property", "tab.id must be a lowercase ASCII identifier")
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
if value.key?("label")
|
|
364
|
+
label = value["label"]
|
|
365
|
+
if self.class.valid_output_text?(label) && !label.strip.empty?
|
|
366
|
+
normalized["label"] = label
|
|
367
|
+
else
|
|
368
|
+
error("invalid_property", "tab.label must be a non-empty string containing only output-safe Unicode characters")
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
if value.key?("order")
|
|
372
|
+
order = value["order"]
|
|
373
|
+
if order.is_a?(Integer)
|
|
374
|
+
normalized["order"] = order
|
|
375
|
+
else
|
|
376
|
+
error("invalid_property", "tab.order must be an integer")
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
if value.key?("topics")
|
|
380
|
+
topics = value["topics"]
|
|
381
|
+
if topics.is_a?(Array) && topics.all? { |topic| valid_tab_topic?(topic) }
|
|
382
|
+
normalized["topics"] = topics.map(&:strip)
|
|
383
|
+
else
|
|
384
|
+
error("invalid_property", "tab.topics must be an array of non-empty strings")
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
normalized
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def validate_tabs(value)
|
|
391
|
+
unless value.is_a?(Array) && value.all? { |id| valid_tab_id?(id) } && value.uniq.length == value.length
|
|
392
|
+
error("invalid_property", "tabs entries must be unique lowercase ASCII identifiers")
|
|
393
|
+
return nil
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
value.dup
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def valid_tab_id?(value)
|
|
400
|
+
value.is_a?(String) && value.match?(TAB_ID_PATTERN)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def valid_tab_topic?(value)
|
|
404
|
+
self.class.valid_output_text?(value) && !value.strip.empty? && value == value.unicode_normalize(:nfc)
|
|
405
|
+
rescue EncodingError
|
|
406
|
+
false
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def wiki_link_candidate?(value)
|
|
410
|
+
value.start_with?("[[") || value.end_with?("]]")
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def unquoted_wiki_link_properties(raw)
|
|
414
|
+
mapping = Psych.parse(raw)&.root
|
|
415
|
+
return [] unless mapping.is_a?(Psych::Nodes::Mapping)
|
|
416
|
+
|
|
417
|
+
mapping.children.each_slice(2).filter_map do |key_node, value_node|
|
|
418
|
+
next unless key_node.is_a?(Psych::Nodes::Scalar) && LINK_ARRAY_PROPERTIES.include?(key_node.value)
|
|
419
|
+
next unless value_node.is_a?(Psych::Nodes::Sequence)
|
|
420
|
+
|
|
421
|
+
invalid = value_node.children.any? do |item|
|
|
422
|
+
item.is_a?(Psych::Nodes::Scalar) &&
|
|
423
|
+
self.class.parse_wiki_link(item.value) &&
|
|
424
|
+
item.style != Psych::Nodes::Scalar::DOUBLE_QUOTED
|
|
425
|
+
end
|
|
426
|
+
key_node.value if invalid
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def normalize_time(value)
|
|
431
|
+
case value
|
|
432
|
+
when DateTime, Time
|
|
433
|
+
value.iso8601
|
|
434
|
+
when Date
|
|
435
|
+
"#{value.iso8601}T00:00:00Z"
|
|
436
|
+
when String
|
|
437
|
+
return "#{Date.iso8601(value).iso8601}T00:00:00Z" if value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
438
|
+
|
|
439
|
+
DateTime.iso8601(value).iso8601
|
|
440
|
+
end
|
|
441
|
+
rescue Date::Error
|
|
442
|
+
nil
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def error(code, message, span: nil)
|
|
446
|
+
@diagnostics << Diagnostic.new(severity: :error, code: code, message: message, path: @path, span: span)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def result(properties, property_links, body)
|
|
450
|
+
Result.new(properties: properties, property_links: property_links, body: body, diagnostics: @diagnostics)
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|