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,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module JekyllObsidian
|
|
6
|
+
class WorkspaceLayout
|
|
7
|
+
class Invalid < StandardError; end
|
|
8
|
+
|
|
9
|
+
DEFAULT_SOURCE = Object.new.freeze
|
|
10
|
+
CONTEXT_KEY = "_jekyll_obsidian_runtime"
|
|
11
|
+
CACHE_DIRECTORY = ".jekyll-obsidian-cache"
|
|
12
|
+
|
|
13
|
+
attr_reader :workspace_root, :site_root, :source_root, :source,
|
|
14
|
+
:destination_root, :jekyll_cache_root, :application_cache_root, :application_assets_root
|
|
15
|
+
|
|
16
|
+
def self.resolve(site:, source: DEFAULT_SOURCE)
|
|
17
|
+
new(site:, source:).tap(&:validate!)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.relative_source(raw)
|
|
21
|
+
raise Invalid, "website.source must be a repository-relative directory" unless raw.is_a?(String)
|
|
22
|
+
|
|
23
|
+
source = raw.unicode_normalize(:nfc).tr("\\", "/")
|
|
24
|
+
invalid = source.empty? || source == "." || source.match?(/[[:cntrl:]]/) ||
|
|
25
|
+
source.match?(/\A[A-Za-z]:\//) || Pathname.new(source).absolute? ||
|
|
26
|
+
Pathname.new(source).cleanpath.to_s != source ||
|
|
27
|
+
source.split("/").any? { |part| part.empty? || part == "." || part == ".." }
|
|
28
|
+
raise Invalid, "website.source must be a normalized repository-relative directory" if invalid
|
|
29
|
+
if source == CACHE_DIRECTORY || source.start_with?("#{CACHE_DIRECTORY}/")
|
|
30
|
+
raise Invalid, "website.source must not overlap the application cache"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
source
|
|
34
|
+
rescue ArgumentError => exception
|
|
35
|
+
raise Invalid, "invalid website.source: #{exception.message}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.assert_path!(root, target, label, missing: false)
|
|
39
|
+
relative = Pathname.new(target).relative_path_from(Pathname.new(root))
|
|
40
|
+
raise Invalid, "#{label} escapes its allowed root" if relative.each_filename.any? { |part| part == ".." }
|
|
41
|
+
|
|
42
|
+
cursor = root
|
|
43
|
+
([root] + relative.each_filename.map { |part| cursor = File.join(cursor, part) }).each do |path|
|
|
44
|
+
stat = File.lstat(path)
|
|
45
|
+
raise Invalid, "#{label} path contains a symbolic link: #{path}" if stat.symlink?
|
|
46
|
+
rescue Errno::ENOENT
|
|
47
|
+
break if missing
|
|
48
|
+
raise Invalid, "#{label} does not exist: #{path}"
|
|
49
|
+
end
|
|
50
|
+
rescue ArgumentError
|
|
51
|
+
raise Invalid, "#{label} escapes its allowed root"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize(site:, source:)
|
|
55
|
+
context = site.config.fetch(CONTEXT_KEY)
|
|
56
|
+
@workspace_root = File.realpath(context.fetch("workspace_root"))
|
|
57
|
+
@site_root = File.expand_path(site.source)
|
|
58
|
+
@source = self.class.relative_source(source.equal?(DEFAULT_SOURCE) ? "docs" : source)
|
|
59
|
+
@source_root = File.expand_path(@source, @workspace_root)
|
|
60
|
+
@destination_root = File.expand_path(site.dest)
|
|
61
|
+
@jekyll_cache_root = File.expand_path(site.cache_dir)
|
|
62
|
+
@application_cache_root = File.join(@workspace_root, CACHE_DIRECTORY)
|
|
63
|
+
@application_assets_root = File.expand_path(context.fetch("assets_root"))
|
|
64
|
+
rescue KeyError => exception
|
|
65
|
+
raise Invalid, "missing runtime context; use jekyll-obsidian build or dev (#{exception.message})"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def validate!
|
|
69
|
+
self.class.assert_path!(@workspace_root, @application_cache_root, "application cache", missing: true)
|
|
70
|
+
unless @site_root == File.join(@application_cache_root, "runtime")
|
|
71
|
+
raise Invalid, "Jekyll source must use the isolated runtime directory"
|
|
72
|
+
end
|
|
73
|
+
validate_directory!(@site_root, "Jekyll source", missing: false)
|
|
74
|
+
self.class.assert_path!(@workspace_root, @source_root, "website.source")
|
|
75
|
+
raise Invalid, "website.source must be a directory: #{@source}" unless File.directory?(@source_root)
|
|
76
|
+
unless File.realpath(@source_root) == @source_root
|
|
77
|
+
raise Invalid, "website.source casing must match the repository path exactly"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
unless File.dirname(@destination_root) == @application_cache_root &&
|
|
81
|
+
File.basename(@destination_root).match?(/\Asite(?:-[A-Za-z0-9][A-Za-z0-9._-]*|\-build\.[A-Za-z0-9]+)?\z/)
|
|
82
|
+
raise Invalid, "destination must be a site directory inside the application cache"
|
|
83
|
+
end
|
|
84
|
+
validate_directory!(@destination_root, "destination")
|
|
85
|
+
unless @jekyll_cache_root == File.join(@site_root, ".jekyll-cache")
|
|
86
|
+
raise Invalid, "Jekyll cache must use the runtime-local .jekyll-cache directory"
|
|
87
|
+
end
|
|
88
|
+
validate_directory!(@jekyll_cache_root, "Jekyll cache")
|
|
89
|
+
instance_variables.each { |name| instance_variable_get(name).freeze }
|
|
90
|
+
freeze
|
|
91
|
+
rescue SystemCallError => exception
|
|
92
|
+
raise Invalid, "invalid workspace: #{exception.message}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def validate_directory!(path, label, missing: true)
|
|
98
|
+
self.class.assert_path!(@workspace_root, path, label, missing:)
|
|
99
|
+
if File.exist?(path) && !File.directory?(path)
|
|
100
|
+
raise Invalid, "#{label} must be a directory"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "jekyll_obsidian/version"
|
|
4
|
+
require_relative "jekyll_obsidian/value_objects"
|
|
5
|
+
require_relative "jekyll_obsidian/output_text"
|
|
6
|
+
require_relative "jekyll_obsidian/github_markdown"
|
|
7
|
+
require_relative "jekyll_obsidian/url_builder"
|
|
8
|
+
require_relative "jekyll_obsidian/destination_registry"
|
|
9
|
+
require_relative "jekyll_obsidian/media_policy"
|
|
10
|
+
require_relative "jekyll_obsidian/external_media"
|
|
11
|
+
require_relative "jekyll_obsidian/front_matter"
|
|
12
|
+
require_relative "jekyll_obsidian/content_policy"
|
|
13
|
+
require_relative "jekyll_obsidian/directory_indexes"
|
|
14
|
+
require_relative "jekyll_obsidian/raw_html_manifest"
|
|
15
|
+
require_relative "jekyll_obsidian/html_publication"
|
|
16
|
+
require_relative "jekyll_obsidian/ofm_scanner"
|
|
17
|
+
require_relative "jekyll_obsidian/published_markdown"
|
|
18
|
+
require_relative "jekyll_obsidian/site_navigation"
|
|
19
|
+
require_relative "jekyll_obsidian/built_in_themes"
|
|
20
|
+
require_relative "jekyll_obsidian/vault_compiler"
|
|
21
|
+
require_relative "jekyll_obsidian/localized_compiler"
|
|
22
|
+
require_relative "jekyll_obsidian/site_compilation"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "find"
|
|
5
|
+
require "json"
|
|
6
|
+
require "pathname"
|
|
7
|
+
require "set"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
require_relative "../lib/jekyll_obsidian/raw_html_manifest"
|
|
11
|
+
|
|
12
|
+
MAX_BYTES = 1_073_741_824
|
|
13
|
+
EXACT_FILES = %w[
|
|
14
|
+
index.html 404.html feed.xml sitemap.xml
|
|
15
|
+
assets/website/catalog.v1.json assets/website/graph.v1.json assets/website/search.v1.json
|
|
16
|
+
robots.txt site.webmanifest favicon.ico .nojekyll
|
|
17
|
+
].append(JekyllObsidian::RawHtmlManifest::PATH).freeze
|
|
18
|
+
RAW_HTML_MANIFEST = JekyllObsidian::RawHtmlManifest::PATH
|
|
19
|
+
ALLOWED_EXTENSIONS = %w[
|
|
20
|
+
.html .css .js .mjs .woff .woff2
|
|
21
|
+
.avif .bmp .gif .jpeg .jpg .png .svg .webp
|
|
22
|
+
.flac .m4a .mp3 .ogg .wav .webm .3gp
|
|
23
|
+
.mkv .mov .mp4 .ogv .pdf .canvas .base
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
def fail_audit(message)
|
|
27
|
+
warn "site audit: #{message}"
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def generated_markdown_resource?(site_dir, relative)
|
|
32
|
+
return false unless File.extname(relative).casecmp(".md").zero?
|
|
33
|
+
|
|
34
|
+
candidates = ["#{relative.delete_suffix('.md')}/index.html"]
|
|
35
|
+
if File.basename(relative).casecmp("index.md").zero?
|
|
36
|
+
directory = File.dirname(relative)
|
|
37
|
+
candidates << (directory == "." ? "index.html" : "#{directory}/index.html")
|
|
38
|
+
end
|
|
39
|
+
candidates.any? { |html| File.file?(File.join(site_dir, html)) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def raw_html_relative_path(value, label, allow_directory: false)
|
|
43
|
+
unless value.is_a?(String) && value.valid_encoding? && value.start_with?("/") &&
|
|
44
|
+
!value.match?(/[?#\\\x00-\x1f\x7f]/) && !value.match?(/%(?:2f|5c)/i)
|
|
45
|
+
fail_audit("unsafe #{label}: #{value.inspect}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
decoded = URI.decode_uri_component(value)
|
|
49
|
+
segments = decoded.split("/", -1)
|
|
50
|
+
segments.pop if allow_directory && decoded.end_with?("/")
|
|
51
|
+
unless decoded.valid_encoding? && segments.first == "" &&
|
|
52
|
+
segments.drop(1).none? { |segment| segment.empty? || segment == "." || segment == ".." }
|
|
53
|
+
fail_audit("unsafe #{label}: #{value.inspect}")
|
|
54
|
+
end
|
|
55
|
+
decoded.delete_prefix("/")
|
|
56
|
+
rescue ArgumentError
|
|
57
|
+
fail_audit("unsafe #{label}: #{value.inspect}")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def raw_html_output_files(site_dir)
|
|
61
|
+
manifest_path = File.join(site_dir, RAW_HTML_MANIFEST)
|
|
62
|
+
return Set.new unless File.exist?(manifest_path) || File.symlink?(manifest_path)
|
|
63
|
+
|
|
64
|
+
stat = File.lstat(manifest_path)
|
|
65
|
+
fail_audit("raw HTML manifest must be a non-symlink regular file") unless stat.file? && !stat.symlink?
|
|
66
|
+
payload = JekyllObsidian::RawHtmlManifest.parse(File.read(manifest_path, encoding: "UTF-8"))
|
|
67
|
+
documents = payload.fetch("documents")
|
|
68
|
+
files = payload.fetch("files")
|
|
69
|
+
|
|
70
|
+
relative_files = files.map { |route| raw_html_relative_path(route, "raw HTML file route") }
|
|
71
|
+
documents.each do |document|
|
|
72
|
+
raw_html_relative_path(document.fetch("route"), "raw HTML document route", allow_directory: true)
|
|
73
|
+
raw_html_relative_path(document.fetch("output"), "raw HTML document output")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
relative_files.each do |relative|
|
|
77
|
+
target = File.expand_path(relative, site_dir)
|
|
78
|
+
inside = target.start_with?("#{site_dir}#{File::SEPARATOR}")
|
|
79
|
+
unless inside && File.exist?(target) && File.lstat(target).file? && !File.lstat(target).symlink?
|
|
80
|
+
fail_audit("raw HTML manifest target is missing or unsafe: #{relative}")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
Set.new(relative_files)
|
|
84
|
+
rescue JekyllObsidian::RawHtmlManifest::Invalid => exception
|
|
85
|
+
fail_audit("raw HTML manifest has an unsupported schema: #{exception.message}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
site_dir = File.expand_path(ARGV.fetch(0, File.expand_path("../_site", __dir__)))
|
|
89
|
+
fail_audit("#{site_dir} is not a directory") unless File.directory?(site_dir)
|
|
90
|
+
fail_audit("the site root must not be a symbolic link") if File.lstat(site_dir).symlink?
|
|
91
|
+
fail_audit("index.html is missing") unless File.file?(File.join(site_dir, "index.html"))
|
|
92
|
+
raw_html_files = raw_html_output_files(site_dir)
|
|
93
|
+
|
|
94
|
+
root = Pathname.new(site_dir)
|
|
95
|
+
total = 0
|
|
96
|
+
Find.find(site_dir) do |path|
|
|
97
|
+
next if path == site_dir
|
|
98
|
+
|
|
99
|
+
stat = File.lstat(path)
|
|
100
|
+
relative = Pathname.new(path).relative_path_from(root).to_s
|
|
101
|
+
unless relative.valid_encoding? && !relative.match?(/[\x00-\x1f\x7f\\]/) &&
|
|
102
|
+
!relative.start_with?("/") && relative.split("/").none? { |segment| segment.empty? || segment == "." || segment == ".." }
|
|
103
|
+
fail_audit("unsafe output path: #{relative.inspect}")
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
fail_audit("symbolic link found: #{relative}") if stat.symlink?
|
|
107
|
+
next if stat.directory?
|
|
108
|
+
fail_audit("non-regular output found: #{relative}") unless stat.file?
|
|
109
|
+
|
|
110
|
+
localized_artifact = relative.match?(%r{\Aassets/website/i18n/[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*/(?:catalog|graph|search)\.v1\.json\z}) ||
|
|
111
|
+
relative.match?(%r{\A[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*/feed\.xml\z})
|
|
112
|
+
allowed = EXACT_FILES.include?(relative) || raw_html_files.include?(relative) || localized_artifact || relative.end_with?("/index.html") ||
|
|
113
|
+
generated_markdown_resource?(site_dir, relative) || ALLOWED_EXTENSIONS.include?(File.extname(relative).downcase)
|
|
114
|
+
fail_audit("output is not on the extension allowlist: #{relative}") unless allowed
|
|
115
|
+
|
|
116
|
+
total += stat.size
|
|
117
|
+
fail_audit("site is larger than 1 GB (#{total} bytes)") if total > MAX_BYTES
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
puts "site audit: ok (#{(total + 1023) / 1024} KiB)"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Build and deploy Pages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: pages-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
if: github.event_name != 'push' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
pages: read
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
|
24
|
+
with:
|
|
25
|
+
fetch-depth: 0
|
|
26
|
+
- name: Read Pages metadata
|
|
27
|
+
if: github.event_name != 'pull_request'
|
|
28
|
+
id: pages
|
|
29
|
+
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d
|
|
30
|
+
- name: Build the site
|
|
31
|
+
id: build
|
|
32
|
+
uses: wowfun/jekyll-obsidian@v__JEKYLL_OBSIDIAN_VERSION__
|
|
33
|
+
with:
|
|
34
|
+
url: ${{ steps.pages.outputs.origin || 'https://example.invalid' }}
|
|
35
|
+
baseurl: ${{ github.event_name == 'pull_request' && '/preview' || steps.pages.outputs.base_path }}
|
|
36
|
+
- name: Upload the site
|
|
37
|
+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
|
38
|
+
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9
|
|
39
|
+
with:
|
|
40
|
+
path: ${{ steps.build.outputs.site-path }}
|
|
41
|
+
|
|
42
|
+
deploy:
|
|
43
|
+
if: github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
|
44
|
+
needs: build
|
|
45
|
+
runs-on: ubuntu-latest
|
|
46
|
+
permissions:
|
|
47
|
+
pages: write
|
|
48
|
+
id-token: write
|
|
49
|
+
environment:
|
|
50
|
+
name: github-pages
|
|
51
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
52
|
+
steps:
|
|
53
|
+
- name: Deploy to GitHub Pages
|
|
54
|
+
id: deployment
|
|
55
|
+
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128
|