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,733 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "find"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "pathname"
|
|
8
|
+
require "tempfile"
|
|
9
|
+
require "tmpdir"
|
|
10
|
+
require "jekyll"
|
|
11
|
+
require_relative "../jekyll_obsidian"
|
|
12
|
+
require_relative "workspace_layout"
|
|
13
|
+
|
|
14
|
+
module JekyllObsidian
|
|
15
|
+
module Adapter
|
|
16
|
+
BUNDLED_FEATURE_IDS = %w[search graph previews math mermaid].freeze
|
|
17
|
+
CONFIG_KEYS = %w[source syntax_profile theme repository edit_branch content features html i18n comments analytics contacts navigation].freeze
|
|
18
|
+
GIT_FIRST_COMMIT_CACHE_VERSION = 1
|
|
19
|
+
GITHUB_MARKDOWN_MANIFEST_INPUT = "JEKYLL_OBSIDIAN_GITHUB_MARKDOWN_MANIFEST_IN"
|
|
20
|
+
GITHUB_MARKDOWN_MANIFEST_OUTPUT = "JEKYLL_OBSIDIAN_GITHUB_MARKDOWN_MANIFEST_OUT"
|
|
21
|
+
IGNORED_CONTENT_DIRECTORIES = %w[.obsidian .trash].freeze
|
|
22
|
+
STAGING_BASENAME_PATTERN = /\Aprojected-files\.[A-Za-z0-9.-]+\z/
|
|
23
|
+
StagingLease = Struct.new(:parent, :path, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
module SiteProcessCleanup
|
|
26
|
+
def process(...)
|
|
27
|
+
super
|
|
28
|
+
ensure
|
|
29
|
+
Adapter.cleanup_staging(self)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class GeneratedPage < Jekyll::PageWithoutAFile
|
|
34
|
+
attr_reader :website_route
|
|
35
|
+
|
|
36
|
+
def initialize(site, output, generated: false)
|
|
37
|
+
@compiler_generated_file = generated
|
|
38
|
+
@website_route = output.route
|
|
39
|
+
directory, filename = self.class.route_parts(output.route)
|
|
40
|
+
super(site, site.source, directory, filename)
|
|
41
|
+
self.content = output.content
|
|
42
|
+
self.data = generated ? {} : output.data.dup
|
|
43
|
+
data["layout"] = nil if generated
|
|
44
|
+
data["permalink"] = output.route
|
|
45
|
+
data["render_with_liquid"] = false
|
|
46
|
+
data["website_generated"] = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Compiler-owned artifacts are already final bytes. Present an inert
|
|
50
|
+
# source extension to Jekyll's renderer while the permalink retains the
|
|
51
|
+
# public extension (including `.md`).
|
|
52
|
+
def extname
|
|
53
|
+
@compiler_generated_file ? ".website-generated" : super
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.route_parts(route)
|
|
57
|
+
return ["", "index.html"] if route == "/"
|
|
58
|
+
|
|
59
|
+
clean = route.delete_prefix("/")
|
|
60
|
+
if clean.end_with?("/")
|
|
61
|
+
[clean.delete_suffix("/"), "index.html"]
|
|
62
|
+
else
|
|
63
|
+
directory = File.dirname(clean)
|
|
64
|
+
directory = "" if directory == "."
|
|
65
|
+
[directory, File.basename(clean)]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class ProjectedStaticFile < Jekyll::StaticFile
|
|
71
|
+
attr_reader :website_route, :source_path, :vault_relative_path
|
|
72
|
+
|
|
73
|
+
def initialize(site, source_root, source_relative_path, route)
|
|
74
|
+
@vault_relative_path = source_relative_path
|
|
75
|
+
@source_path = File.join(source_root, source_relative_path)
|
|
76
|
+
@website_route = route
|
|
77
|
+
super(site, source_root, File.dirname(source_relative_path), File.basename(source_relative_path))
|
|
78
|
+
@relative_path = route
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def path
|
|
82
|
+
@source_path
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def url
|
|
86
|
+
@website_route
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def destination(destination_root)
|
|
90
|
+
@website_destinations ||= {}
|
|
91
|
+
@website_destinations[destination_root] ||= @site.in_dest_dir(
|
|
92
|
+
destination_root,
|
|
93
|
+
Jekyll::URL.unescape_path(@website_route.delete_prefix("/"))
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def destination_rel_dir
|
|
98
|
+
File.dirname(@website_route)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
class << self
|
|
103
|
+
|
|
104
|
+
def prepare_site(site)
|
|
105
|
+
site.singleton_class.prepend(SiteProcessCleanup) unless site.singleton_class < SiteProcessCleanup
|
|
106
|
+
website, _layout = normalize_website_configuration(site)
|
|
107
|
+
site.config["website"] = website
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def generate(site)
|
|
111
|
+
source = site.config.fetch("website").fetch("source")
|
|
112
|
+
layout = resolve_workspace_layout(site, source)
|
|
113
|
+
assert_vault_was_not_read(site, layout)
|
|
114
|
+
result = compile(site, layout)
|
|
115
|
+
log_diagnostics(result)
|
|
116
|
+
unless result.success?
|
|
117
|
+
summary = result.diagnostics.select { |item| item.severity == :error }
|
|
118
|
+
.map { |item| "#{[item.path, item.code].compact.join(":")} (#{item.message})" }
|
|
119
|
+
.join(", ")
|
|
120
|
+
fatal("website compilation failed: #{summary}")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
staging_root = stage_projected_files(site, layout, result)
|
|
124
|
+
begin
|
|
125
|
+
site.data["website_feed_available"] = result.generated_files.any? { |output| output.route == "/feed.xml" }
|
|
126
|
+
site.data.merge!(result.site_data)
|
|
127
|
+
pages, projected_files = generated_objects(site, result, staging_root)
|
|
128
|
+
app_assets = app_asset_objects(site, layout:, theme: result.theme, features: result.features)
|
|
129
|
+
preflight_collisions(site, pages, projected_files + app_assets)
|
|
130
|
+
site.pages.concat(pages)
|
|
131
|
+
site.static_files.concat(projected_files).concat(app_assets)
|
|
132
|
+
rescue StandardError
|
|
133
|
+
cleanup_staging(site)
|
|
134
|
+
raise
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def cleanup_staging(site)
|
|
139
|
+
lease = site.remove_instance_variable(:@jekyll_website_staging_lease)
|
|
140
|
+
return unless lease.is_a?(StagingLease)
|
|
141
|
+
return unless File.dirname(lease.path) == lease.parent
|
|
142
|
+
return unless File.basename(lease.path).match?(STAGING_BASENAME_PATTERN)
|
|
143
|
+
|
|
144
|
+
FileUtils.remove_entry(lease.path) if File.exist?(lease.path) || File.symlink?(lease.path)
|
|
145
|
+
rescue NameError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def normalize_website_configuration(site)
|
|
152
|
+
if site.config.key?("obsidian")
|
|
153
|
+
fatal("obsidian configuration is no longer supported; rename the root mapping to website")
|
|
154
|
+
end
|
|
155
|
+
raw = site.config["website"]
|
|
156
|
+
fatal("website must be a mapping") unless raw.nil? || raw.is_a?(Hash)
|
|
157
|
+
configured = raw || {}
|
|
158
|
+
unknown = configured.keys.map(&:to_s) - CONFIG_KEYS
|
|
159
|
+
fatal("website contains unsupported key: #{unknown.sort.first}") unless unknown.empty?
|
|
160
|
+
|
|
161
|
+
source = configured.key?("source") ? configured["source"] : WorkspaceLayout::DEFAULT_SOURCE
|
|
162
|
+
layout = resolve_workspace_layout(site, source)
|
|
163
|
+
website = {
|
|
164
|
+
"source" => layout.source,
|
|
165
|
+
"syntax_profile" => configured.fetch("syntax_profile", "ofm@1"),
|
|
166
|
+
"theme" => configured.fetch("theme", "minimal"),
|
|
167
|
+
"repository" => configured.fetch("repository", ""),
|
|
168
|
+
"edit_branch" => configured.fetch("edit_branch", "main"),
|
|
169
|
+
"content" => configured["content"],
|
|
170
|
+
"features" => configured["features"],
|
|
171
|
+
"html" => configured["html"],
|
|
172
|
+
"i18n" => configured["i18n"],
|
|
173
|
+
"comments" => configured["comments"],
|
|
174
|
+
"analytics" => configured["analytics"],
|
|
175
|
+
"contacts" => configured["contacts"],
|
|
176
|
+
"navigation" => configured["navigation"]
|
|
177
|
+
}
|
|
178
|
+
[website, layout]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def resolve_workspace_layout(site, source)
|
|
182
|
+
WorkspaceLayout.resolve(site:, source:)
|
|
183
|
+
rescue WorkspaceLayout::Invalid => exception
|
|
184
|
+
fatal(exception.message)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def assert_vault_was_not_read(site, layout)
|
|
188
|
+
root = layout.source_root
|
|
189
|
+
leaked = []
|
|
190
|
+
site.pages.each { |page| leaked << page.path if path_inside?(page.path, root, site.source) }
|
|
191
|
+
site.static_files.each { |file| leaked << file.path if path_inside?(file.path, root, site.source) }
|
|
192
|
+
site.collections.each_value do |collection|
|
|
193
|
+
collection.docs.each { |document| leaked << document.path if path_inside?(document.path, root, site.source) }
|
|
194
|
+
end
|
|
195
|
+
return if leaked.empty?
|
|
196
|
+
|
|
197
|
+
fatal("website.source entered Jekyll Reader despite exclusion: #{leaked.sort.first}")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def path_inside?(candidate, root, site_source)
|
|
201
|
+
return false if candidate.nil? || candidate.to_s.empty?
|
|
202
|
+
expanded = File.expand_path(candidate.to_s, site_source)
|
|
203
|
+
expanded_root = File.expand_path(root)
|
|
204
|
+
return true if path_descendant?(expanded, expanded_root)
|
|
205
|
+
return false unless File.exist?(expanded) || File.symlink?(expanded)
|
|
206
|
+
|
|
207
|
+
resolved = File.realpath(expanded)
|
|
208
|
+
resolved_root = File.realpath(expanded_root)
|
|
209
|
+
path_descendant?(resolved, resolved_root)
|
|
210
|
+
rescue SystemCallError => exception
|
|
211
|
+
fatal("cannot resolve Jekyll Reader input #{candidate}: #{exception.message}")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def path_descendant?(candidate, root)
|
|
215
|
+
candidate == root || candidate.start_with?("#{root}#{File::SEPARATOR}")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def build_snapshot(layout)
|
|
219
|
+
root = layout.source_root
|
|
220
|
+
git_first_commit_times = git_first_commit_time_map(layout)
|
|
221
|
+
entries = []
|
|
222
|
+
Find.find(root) do |absolute|
|
|
223
|
+
relative = Pathname.new(absolute).relative_path_from(Pathname.new(root)).to_s
|
|
224
|
+
next if relative == "."
|
|
225
|
+
|
|
226
|
+
stat = File.lstat(absolute)
|
|
227
|
+
fatal("vault symlink rejected: #{relative}") if stat.symlink?
|
|
228
|
+
ignored = IGNORED_CONTENT_DIRECTORIES.any? do |directory|
|
|
229
|
+
relative == directory || relative.start_with?("#{directory}#{File::SEPARATOR}")
|
|
230
|
+
end
|
|
231
|
+
if ignored
|
|
232
|
+
Find.prune if stat.directory?
|
|
233
|
+
next
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
next if stat.directory?
|
|
237
|
+
fatal("vault contains a non-regular file: #{relative}") unless stat.file?
|
|
238
|
+
|
|
239
|
+
normalized = relative.tr(File::SEPARATOR, "/")
|
|
240
|
+
kind = if File.extname(normalized).downcase == ".md"
|
|
241
|
+
:note
|
|
242
|
+
elsif File.basename(normalized) == LocalizedCompiler::LOCALE_MANIFEST
|
|
243
|
+
:locale_manifest
|
|
244
|
+
else
|
|
245
|
+
:attachment
|
|
246
|
+
end
|
|
247
|
+
flags = File::RDONLY | (File.const_defined?(:NOFOLLOW) ? File::NOFOLLOW : 0)
|
|
248
|
+
File.open(absolute, flags) do |file|
|
|
249
|
+
pinned = file.stat
|
|
250
|
+
fatal("vault file changed during snapshot: #{relative}") unless pinned.file? && pinned.dev == stat.dev && pinned.ino == stat.ino
|
|
251
|
+
entries << SnapshotEntry.new(
|
|
252
|
+
path: normalized,
|
|
253
|
+
bytes: %i[note locale_manifest].include?(kind) ? file.read : nil,
|
|
254
|
+
kind: kind,
|
|
255
|
+
media_type: kind == :note ? "text/markdown" : MediaPolicy.media_type(normalized),
|
|
256
|
+
size: pinned.size,
|
|
257
|
+
device: pinned.dev,
|
|
258
|
+
inode: pinned.ino,
|
|
259
|
+
mtime_ns: pinned.mtime.to_i * 1_000_000_000 + pinned.mtime.nsec,
|
|
260
|
+
first_committed_at: git_first_commit_times[normalized]
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
Snapshot.new(entries: entries.sort_by(&:path))
|
|
265
|
+
rescue Errno::ENOENT => exception
|
|
266
|
+
fatal("vault changed during snapshot: #{exception.message}")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def git_first_commit_time_map(layout)
|
|
270
|
+
source = layout.source
|
|
271
|
+
head, _head_error, head_status = Open3.capture3("git", "-C", layout.workspace_root, "rev-parse", "HEAD")
|
|
272
|
+
return {} unless head_status.success?
|
|
273
|
+
|
|
274
|
+
head = head.strip
|
|
275
|
+
cache_path = File.join(layout.application_cache_root, "git-times.json")
|
|
276
|
+
ensure_runtime_directory!(layout.application_cache_root, "application cache")
|
|
277
|
+
cached_bytes = read_regular_cache_file(cache_path, "Git first-commit cache")
|
|
278
|
+
if cached_bytes
|
|
279
|
+
cached = JSON.parse(cached_bytes)
|
|
280
|
+
cached_times = cached["first_committed_at"]
|
|
281
|
+
if cached["version"] == GIT_FIRST_COMMIT_CACHE_VERSION && cached["head"] == head &&
|
|
282
|
+
cached["source"] == source && cached_times.is_a?(Hash) &&
|
|
283
|
+
cached_times.all? { |path, time| path.is_a?(String) && time.is_a?(String) }
|
|
284
|
+
return cached_times
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
command = [
|
|
289
|
+
"git", "-C", layout.workspace_root, "-c", "core.quotePath=false",
|
|
290
|
+
"log", "--format=%x1e%aI", "--name-only", "--", source
|
|
291
|
+
]
|
|
292
|
+
stdout, _stderr, status = Open3.capture3(*command)
|
|
293
|
+
return {} unless status.success?
|
|
294
|
+
|
|
295
|
+
result = {}
|
|
296
|
+
current_time = nil
|
|
297
|
+
stdout.each_line do |line|
|
|
298
|
+
stripped = line.strip
|
|
299
|
+
next if stripped.empty?
|
|
300
|
+
if stripped.start_with?("\x1e")
|
|
301
|
+
# Git 2.45+ emits Z for UTC while older versions emit +00:00.
|
|
302
|
+
current_time = stripped.delete_prefix("\x1e").sub(/\+00:00\z/, "Z")
|
|
303
|
+
next
|
|
304
|
+
end
|
|
305
|
+
next unless current_time
|
|
306
|
+
prefix = "#{source}/"
|
|
307
|
+
next unless stripped.start_with?(prefix)
|
|
308
|
+
|
|
309
|
+
relative = stripped.delete_prefix(prefix).unicode_normalize(:nfc)
|
|
310
|
+
result[relative] = current_time
|
|
311
|
+
end
|
|
312
|
+
FileUtils.mkdir_p(File.dirname(cache_path))
|
|
313
|
+
temporary = Tempfile.create(["jekyll-obsidian-git-times.", ".tmp"], File.dirname(cache_path))
|
|
314
|
+
temporary_path = temporary.path
|
|
315
|
+
temporary.write(JSON.generate(
|
|
316
|
+
"version" => GIT_FIRST_COMMIT_CACHE_VERSION,
|
|
317
|
+
"head" => head,
|
|
318
|
+
"source" => source,
|
|
319
|
+
"first_committed_at" => result
|
|
320
|
+
))
|
|
321
|
+
temporary.flush
|
|
322
|
+
temporary.fsync
|
|
323
|
+
temporary.close
|
|
324
|
+
File.rename(temporary_path, cache_path)
|
|
325
|
+
result
|
|
326
|
+
rescue JSON::ParserError, SystemCallError
|
|
327
|
+
{}
|
|
328
|
+
ensure
|
|
329
|
+
temporary.close if defined?(temporary) && temporary && !temporary.closed?
|
|
330
|
+
FileUtils.rm_f(temporary_path) if defined?(temporary_path) && temporary_path
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def ensure_runtime_directory!(root, label)
|
|
334
|
+
FileUtils.mkdir_p(root)
|
|
335
|
+
stat = File.lstat(root)
|
|
336
|
+
unless stat.directory? && !stat.symlink? && File.realpath(root) == root
|
|
337
|
+
fatal("#{label} must be a non-symlink directory")
|
|
338
|
+
end
|
|
339
|
+
rescue SystemCallError => exception
|
|
340
|
+
fatal("cannot validate #{label}: #{exception.message}")
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def read_regular_cache_file(path, label)
|
|
344
|
+
return nil unless File.exist?(path) || File.symlink?(path)
|
|
345
|
+
|
|
346
|
+
before = File.lstat(path)
|
|
347
|
+
fatal("#{label} must be a non-symlink regular file") unless before.file? && !before.symlink?
|
|
348
|
+
flags = File::RDONLY | (File.const_defined?(:NOFOLLOW) ? File::NOFOLLOW : 0)
|
|
349
|
+
File.open(path, flags) do |file|
|
|
350
|
+
opened = file.stat
|
|
351
|
+
unless opened.file? && opened.dev == before.dev && opened.ino == before.ino
|
|
352
|
+
fatal("#{label} changed while opening")
|
|
353
|
+
end
|
|
354
|
+
file.read
|
|
355
|
+
end
|
|
356
|
+
rescue SystemCallError => exception
|
|
357
|
+
fatal("cannot read #{label}: #{exception.message}")
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def repository_identity(site, layout)
|
|
361
|
+
explicit = site.config.dig("website", "repository").to_s.strip
|
|
362
|
+
return explicit unless explicit.empty?
|
|
363
|
+
environment = ENV.fetch("GITHUB_REPOSITORY", "").strip
|
|
364
|
+
return environment unless environment.empty?
|
|
365
|
+
|
|
366
|
+
stdout, _stderr, status = Open3.capture3(
|
|
367
|
+
"git", "-C", layout.workspace_root, "config", "--get", "remote.origin.url"
|
|
368
|
+
)
|
|
369
|
+
return nil unless status.success?
|
|
370
|
+
remote = stdout.strip
|
|
371
|
+
match = remote.match(%r{(?:github\.com[:/])([^/\s]+/[^/\s]+?)(?:\.git)?\z}i)
|
|
372
|
+
match && match[1]
|
|
373
|
+
rescue SystemCallError
|
|
374
|
+
nil
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def compile(site, layout)
|
|
378
|
+
repository = repository_identity(site, layout)
|
|
379
|
+
Jekyll.logger.warn("Website:", "repository identity unavailable; GitHub collaboration links are hidden") unless repository
|
|
380
|
+
website = site.config.fetch("website")
|
|
381
|
+
request = BuildRequest.new(
|
|
382
|
+
snapshot: build_snapshot(layout),
|
|
383
|
+
config: BuildConfig.new(
|
|
384
|
+
title: site.config["title"].to_s,
|
|
385
|
+
description: site.config["description"].to_s,
|
|
386
|
+
lang: site.config.fetch("lang", "en").to_s,
|
|
387
|
+
url: site.config["url"].to_s,
|
|
388
|
+
baseurl: site.config["baseurl"].to_s,
|
|
389
|
+
source: layout.source,
|
|
390
|
+
syntax_profile: website.fetch("syntax_profile"),
|
|
391
|
+
theme: website.fetch("theme"),
|
|
392
|
+
content: website.fetch("content"),
|
|
393
|
+
features: website.fetch("features"),
|
|
394
|
+
html: website.fetch("html"),
|
|
395
|
+
i18n: website.fetch("i18n"),
|
|
396
|
+
comments: website.fetch("comments"),
|
|
397
|
+
analytics: website.fetch("analytics"),
|
|
398
|
+
contacts: website.fetch("contacts"),
|
|
399
|
+
navigation: website.fetch("navigation"),
|
|
400
|
+
repository: repository.to_s,
|
|
401
|
+
edit_branch: website.fetch("edit_branch"),
|
|
402
|
+
environment: ENV.fetch("JEKYLL_ENV", "development")
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
ensure_runtime_directory!(layout.application_cache_root, "Application cache")
|
|
406
|
+
manifest_input = github_markdown_manifest_path(layout, GITHUB_MARKDOWN_MANIFEST_INPUT)
|
|
407
|
+
manifest = manifest_input && read_regular_cache_file(manifest_input, "GitHub Markdown manifest")
|
|
408
|
+
fatal("GitHub Markdown manifest does not exist") if manifest_input && manifest.nil?
|
|
409
|
+
result = SiteCompilation.compile(
|
|
410
|
+
request,
|
|
411
|
+
transport: GitHubMarkdown::HttpTransport.new,
|
|
412
|
+
cache_root: layout.application_cache_root,
|
|
413
|
+
manifest: manifest
|
|
414
|
+
)
|
|
415
|
+
manifest_output = github_markdown_manifest_path(layout, GITHUB_MARKDOWN_MANIFEST_OUTPUT)
|
|
416
|
+
if result.success? && manifest_output
|
|
417
|
+
content = result.github_markdown_manifest || GitHubMarkdown.dump_manifest([])
|
|
418
|
+
write_github_markdown_manifest(manifest_output, content)
|
|
419
|
+
end
|
|
420
|
+
result
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def github_markdown_manifest_path(layout, environment_key)
|
|
424
|
+
raw = ENV.fetch(environment_key, "").to_s
|
|
425
|
+
return nil if raw.empty?
|
|
426
|
+
|
|
427
|
+
path = File.expand_path(raw, layout.workspace_root)
|
|
428
|
+
unless File.dirname(path) == layout.application_cache_root &&
|
|
429
|
+
File.basename(path).match?(/\A[A-Za-z0-9][A-Za-z0-9._-]*\.json\z/)
|
|
430
|
+
fatal("#{environment_key} must name a JSON file directly inside the application cache")
|
|
431
|
+
end
|
|
432
|
+
path
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def write_github_markdown_manifest(path, content)
|
|
436
|
+
if File.exist?(path) || File.symlink?(path)
|
|
437
|
+
stat = File.lstat(path)
|
|
438
|
+
fatal("GitHub Markdown manifest output must be a non-symlink regular file") unless stat.file? && !stat.symlink?
|
|
439
|
+
end
|
|
440
|
+
temporary = Tempfile.create(["github-markdown-manifest.", ".tmp"], File.dirname(path))
|
|
441
|
+
temporary_path = temporary.path
|
|
442
|
+
temporary.binmode
|
|
443
|
+
temporary.write(content)
|
|
444
|
+
temporary.flush
|
|
445
|
+
temporary.fsync
|
|
446
|
+
temporary.close
|
|
447
|
+
File.rename(temporary_path, path)
|
|
448
|
+
rescue SystemCallError => exception
|
|
449
|
+
fatal("cannot write GitHub Markdown manifest: #{exception.message}")
|
|
450
|
+
ensure
|
|
451
|
+
temporary.close if defined?(temporary) && temporary && !temporary.closed?
|
|
452
|
+
FileUtils.rm_f(temporary_path) if defined?(temporary_path) && temporary_path
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def stage_projected_files(site, layout, result)
|
|
456
|
+
cleanup_staging(site)
|
|
457
|
+
FileUtils.mkdir_p(layout.application_cache_root)
|
|
458
|
+
validate_application_cache_root!(layout)
|
|
459
|
+
staging_root = Dir.mktmpdir("projected-files.", layout.application_cache_root)
|
|
460
|
+
vault_root = layout.source_root
|
|
461
|
+
result.projected_files.each do |output|
|
|
462
|
+
source_path = File.join(vault_root, output.source_path)
|
|
463
|
+
destination = File.join(staging_root, output.source_path)
|
|
464
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
465
|
+
flags = File::RDONLY | (File.const_defined?(:NOFOLLOW) ? File::NOFOLLOW : 0)
|
|
466
|
+
File.open(source_path, flags) do |file|
|
|
467
|
+
stat = file.stat
|
|
468
|
+
actual_mtime_ns = stat.mtime.to_i * 1_000_000_000 + stat.mtime.nsec
|
|
469
|
+
expected = [output.device, output.inode, output.size, output.mtime_ns]
|
|
470
|
+
actual = [stat.dev, stat.ino, stat.size, actual_mtime_ns]
|
|
471
|
+
fatal("projected file changed after compilation: #{output.source_path}") unless stat.file? && actual == expected
|
|
472
|
+
File.open(destination, File::WRONLY | File::CREAT | File::EXCL, 0o644) { |target| IO.copy_stream(file, target) }
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
site.instance_variable_set(
|
|
476
|
+
:@jekyll_website_staging_lease,
|
|
477
|
+
StagingLease.new(parent: layout.application_cache_root, path: staging_root).freeze
|
|
478
|
+
)
|
|
479
|
+
staging_root
|
|
480
|
+
rescue StandardError
|
|
481
|
+
FileUtils.remove_entry(staging_root) if staging_root && File.exist?(staging_root)
|
|
482
|
+
raise
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def generated_objects(site, result, staging_root)
|
|
486
|
+
pages = result.pages.map { |output| GeneratedPage.new(site, output) }
|
|
487
|
+
generated = result.generated_files.map do |output|
|
|
488
|
+
page_output = PageOutput.new(route: output.route, content: output.content, data: {})
|
|
489
|
+
GeneratedPage.new(site, page_output, generated: true)
|
|
490
|
+
end
|
|
491
|
+
assets = result.projected_files.map do |output|
|
|
492
|
+
ProjectedStaticFile.new(site, staging_root, output.source_path, output.route)
|
|
493
|
+
end
|
|
494
|
+
[pages + generated, assets]
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def app_asset_objects(site, layout:, theme:, features:)
|
|
498
|
+
root = layout.application_assets_root
|
|
499
|
+
validate_application_asset_root!(layout, root)
|
|
500
|
+
manifest_path = File.join(root, "manifest.json")
|
|
501
|
+
unless File.file?(manifest_path)
|
|
502
|
+
fatal("application asset manifest is missing: #{manifest_path}") unless Jekyll.env == "development"
|
|
503
|
+
|
|
504
|
+
Jekyll.logger.warn("Website:", "application asset manifest is missing; frontend assets are unavailable")
|
|
505
|
+
return []
|
|
506
|
+
end
|
|
507
|
+
validate_application_asset_file!(
|
|
508
|
+
manifest_path,
|
|
509
|
+
cache_root: root,
|
|
510
|
+
label: "application asset manifest"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
manifest = JSON.parse(File.read(manifest_path, encoding: "UTF-8"))
|
|
514
|
+
fatal("asset manifest schema is unsupported") unless manifest["schema_version"] == 1
|
|
515
|
+
entries = manifest["entries"]
|
|
516
|
+
fatal("asset manifest entries must be a mapping") unless entries.is_a?(Hash)
|
|
517
|
+
active_entry = entries[theme]
|
|
518
|
+
fatal("asset manifest has no entry for active theme: #{theme}") unless active_entry.is_a?(Hash)
|
|
519
|
+
validate_manifest_entry!(active_entry, "entries.#{theme}")
|
|
520
|
+
|
|
521
|
+
manifest_features = manifest.fetch("features", {})
|
|
522
|
+
fatal("asset manifest features must be a mapping") unless manifest_features.is_a?(Hash)
|
|
523
|
+
files = active_entry.fetch("files").dup
|
|
524
|
+
features.each do |feature, enabled|
|
|
525
|
+
next unless enabled && BUNDLED_FEATURE_IDS.include?(feature)
|
|
526
|
+
|
|
527
|
+
entry = manifest_features[feature]
|
|
528
|
+
fatal("asset manifest is missing enabled bundle feature: features.#{feature}") unless entry
|
|
529
|
+
validate_manifest_file_list!(entry, "features.#{feature}")
|
|
530
|
+
files.concat(entry.fetch("files"))
|
|
531
|
+
end
|
|
532
|
+
files = files.uniq.sort
|
|
533
|
+
allowlist = manifest["files"]
|
|
534
|
+
if allowlist
|
|
535
|
+
fatal("asset manifest files must be an array") unless allowlist.is_a?(Array)
|
|
536
|
+
allowlist.each { |relative| validate_manifest_path!(relative) }
|
|
537
|
+
outside_allowlist = files - allowlist
|
|
538
|
+
fatal("active asset is absent from manifest files: #{outside_allowlist.first}") unless outside_allowlist.empty?
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
site.data["website_assets"] = manifest
|
|
542
|
+
files.map do |relative|
|
|
543
|
+
validate_manifest_path!(relative)
|
|
544
|
+
absolute = File.join(root, relative)
|
|
545
|
+
validate_application_asset_file!(
|
|
546
|
+
absolute,
|
|
547
|
+
cache_root: root,
|
|
548
|
+
label: "application asset #{relative}"
|
|
549
|
+
)
|
|
550
|
+
ProjectedStaticFile.new(site, root, relative, "/assets/website/#{relative}")
|
|
551
|
+
end
|
|
552
|
+
rescue JSON::ParserError => exception
|
|
553
|
+
fatal("invalid asset manifest: #{exception.message}")
|
|
554
|
+
rescue SystemCallError => exception
|
|
555
|
+
fatal("cannot load application assets: #{exception.message}")
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def validate_application_cache_root!(layout)
|
|
559
|
+
root = layout.application_cache_root
|
|
560
|
+
stat = File.lstat(root)
|
|
561
|
+
fatal("application cache must be a non-symlink directory") unless stat.directory? && !stat.symlink?
|
|
562
|
+
rescue SystemCallError => exception
|
|
563
|
+
fatal("cannot validate application cache: #{exception.message}")
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def validate_application_asset_root!(layout, root)
|
|
567
|
+
expanded_root = File.expand_path(root)
|
|
568
|
+
fatal("unexpected application asset root") unless expanded_root == layout.application_assets_root
|
|
569
|
+
|
|
570
|
+
return unless File.exist?(expanded_root) || File.symlink?(expanded_root)
|
|
571
|
+
|
|
572
|
+
stat = File.lstat(expanded_root)
|
|
573
|
+
if stat.symlink? || File.realpath(expanded_root) != expanded_root
|
|
574
|
+
fatal("application assets path contains a symbolic link")
|
|
575
|
+
end
|
|
576
|
+
fatal("application assets must be a directory") unless stat.directory?
|
|
577
|
+
rescue SystemCallError => exception
|
|
578
|
+
fatal("cannot validate application asset cache: #{exception.message}")
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def validate_application_asset_file!(path, cache_root:, label:)
|
|
582
|
+
expanded = File.expand_path(path)
|
|
583
|
+
expanded_cache_root = File.expand_path(cache_root)
|
|
584
|
+
unless path_descendant?(expanded, expanded_cache_root) && expanded != expanded_cache_root
|
|
585
|
+
fatal("#{label} escapes the application asset cache")
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
relative = Pathname.new(expanded).relative_path_from(Pathname.new(expanded_cache_root))
|
|
589
|
+
cursor = expanded_cache_root
|
|
590
|
+
parts = relative.each_filename.to_a
|
|
591
|
+
parts.each_with_index do |part, index|
|
|
592
|
+
cursor = File.join(cursor, part)
|
|
593
|
+
stat = File.lstat(cursor)
|
|
594
|
+
fatal("#{label} path contains a symbolic link: #{cursor}") if stat.symlink?
|
|
595
|
+
if index == parts.length - 1
|
|
596
|
+
fatal("#{label} is not a regular file") unless stat.file?
|
|
597
|
+
else
|
|
598
|
+
fatal("#{label} path contains a non-directory: #{cursor}") unless stat.directory?
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
rescue ArgumentError
|
|
602
|
+
fatal("#{label} escapes the application asset cache")
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def validate_manifest_entry!(entry, location)
|
|
606
|
+
fatal("asset manifest #{location} must be a mapping") unless entry.is_a?(Hash)
|
|
607
|
+
js = entry["js"]
|
|
608
|
+
fatal("asset manifest #{location}.js is invalid") unless js.is_a?(String)
|
|
609
|
+
validate_manifest_path!(js)
|
|
610
|
+
color_scheme = entry["color_scheme"]
|
|
611
|
+
fatal("asset manifest #{location}.color_scheme is invalid") unless color_scheme.is_a?(String)
|
|
612
|
+
validate_manifest_path!(color_scheme)
|
|
613
|
+
if entry.key?("css")
|
|
614
|
+
fatal("asset manifest #{location}.css is invalid") unless entry["css"].is_a?(String)
|
|
615
|
+
validate_manifest_path!(entry["css"])
|
|
616
|
+
end
|
|
617
|
+
validate_manifest_file_list!(entry, location)
|
|
618
|
+
fatal("asset manifest #{location}.files must contain its js") unless entry.fetch("files").include?(js)
|
|
619
|
+
unless entry.fetch("files").include?(color_scheme)
|
|
620
|
+
fatal("asset manifest #{location}.files must contain its color scheme bootstrap")
|
|
621
|
+
end
|
|
622
|
+
if entry.key?("css") && !entry.fetch("files").include?(entry["css"])
|
|
623
|
+
fatal("asset manifest #{location}.files must contain its css")
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def validate_manifest_file_list!(entry, location)
|
|
628
|
+
fatal("asset manifest #{location} must be a mapping") unless entry.is_a?(Hash)
|
|
629
|
+
files = entry["files"]
|
|
630
|
+
fatal("asset manifest #{location}.files must be an array") unless files.is_a?(Array)
|
|
631
|
+
files.each { |relative| validate_manifest_path!(relative) }
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def validate_manifest_path!(path)
|
|
635
|
+
fatal("asset manifest contains an unsafe path") unless safe_relative_path?(path)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def safe_relative_path?(path)
|
|
639
|
+
path.is_a?(String) && !path.empty? && !path.include?("\\") && !path.include?("\0") &&
|
|
640
|
+
!Pathname.new(path).absolute? && Pathname.new(path).cleanpath.to_s == path &&
|
|
641
|
+
path.split("/").none? { |part| part.empty? || part == "." || part == ".." }
|
|
642
|
+
rescue ArgumentError
|
|
643
|
+
false
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def preflight_collisions(site, pages, static_files)
|
|
647
|
+
registry = {}
|
|
648
|
+
site.pages.each { |page| register_output!(registry, site, page, page.url, "Jekyll page #{page.path}") }
|
|
649
|
+
site.static_files.each { |file| register_output!(registry, site, file, file.url, "Jekyll static file #{file.path}") }
|
|
650
|
+
site.collections.each_value do |collection|
|
|
651
|
+
collection.docs.each do |document|
|
|
652
|
+
next unless document.write?
|
|
653
|
+
|
|
654
|
+
register_output!(registry, site, document, document.url, "Jekyll document #{document.path}")
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
pages.each { |page| register_output!(registry, site, page, page.website_route, "website output") }
|
|
658
|
+
static_files.each do |file|
|
|
659
|
+
owner = if file.is_a?(ProjectedStaticFile)
|
|
660
|
+
"website projected source #{file.vault_relative_path}"
|
|
661
|
+
else
|
|
662
|
+
"website asset"
|
|
663
|
+
end
|
|
664
|
+
register_output!(registry, site, file, file.website_route, owner)
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def register_output!(registry, site, output, route, owner)
|
|
669
|
+
validate_output_route!(route)
|
|
670
|
+
key = destination_collision_key(site, output.destination(site.dest))
|
|
671
|
+
if registry.key?(key)
|
|
672
|
+
fatal("output route collision at #{route} (#{owner}; already owned by #{registry[key]})")
|
|
673
|
+
end
|
|
674
|
+
registry[key] = owner
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
def validate_output_route!(route)
|
|
678
|
+
UrlBuilder.new(origin: "", baseurl: "").collision_key(route.to_s)
|
|
679
|
+
rescue ArgumentError => exception
|
|
680
|
+
fatal("unsafe output route #{route.inspect}: #{exception.message}")
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def destination_collision_key(site, destination)
|
|
684
|
+
root = File.expand_path(site.dest)
|
|
685
|
+
expanded = File.expand_path(destination.to_s)
|
|
686
|
+
unless path_descendant?(expanded, root) && expanded != root
|
|
687
|
+
fatal("output destination escapes the configured destination: #{destination.inspect}")
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
Pathname.new(expanded).relative_path_from(Pathname.new(root)).to_s
|
|
691
|
+
.tr(File::SEPARATOR, "/")
|
|
692
|
+
.unicode_normalize(:nfc)
|
|
693
|
+
.downcase(:fold)
|
|
694
|
+
rescue ArgumentError => exception
|
|
695
|
+
fatal("unsafe output destination #{destination.inspect}: #{exception.message}")
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def log_diagnostics(result)
|
|
699
|
+
result.diagnostics.each do |diagnostic|
|
|
700
|
+
label = [diagnostic.path, diagnostic.code].compact.join(":")
|
|
701
|
+
if diagnostic.severity == :error
|
|
702
|
+
Jekyll.logger.error("Website #{label}:", diagnostic.message)
|
|
703
|
+
else
|
|
704
|
+
Jekyll.logger.warn("Website #{label}:", diagnostic.message)
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def fatal(message)
|
|
710
|
+
raise Jekyll::Errors::FatalException, message
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
class Generator < Jekyll::Generator
|
|
715
|
+
safe false
|
|
716
|
+
priority :highest
|
|
717
|
+
|
|
718
|
+
def generate(site)
|
|
719
|
+
return unless site.config[WorkspaceLayout::CONTEXT_KEY]
|
|
720
|
+
|
|
721
|
+
Adapter.generate(site)
|
|
722
|
+
end
|
|
723
|
+
end
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
Jekyll::Hooks.register :site, :after_init do |site|
|
|
728
|
+
JekyllObsidian::Adapter.prepare_site(site) if site.config[JekyllObsidian::WorkspaceLayout::CONTEXT_KEY]
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
Jekyll::Hooks.register :site, :post_write do |site|
|
|
732
|
+
JekyllObsidian::Adapter.cleanup_staging(site)
|
|
733
|
+
end
|