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.
Files changed (191) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +133 -0
  4. data/website/_config.yml +21 -0
  5. data/website/_includes/website/archive-timeline.html +8 -0
  6. data/website/_includes/website/blog-ledger.html +19 -0
  7. data/website/_includes/website/blog-navigation.html +7 -0
  8. data/website/_includes/website/comments.html +22 -0
  9. data/website/_includes/website/contact-icon.html +14 -0
  10. data/website/_includes/website/context-tags.html +7 -0
  11. data/website/_includes/website/context.html +3 -0
  12. data/website/_includes/website/dialogs.html +71 -0
  13. data/website/_includes/website/docs-navigation.html +7 -0
  14. data/website/_includes/website/docs-tree.html +8 -0
  15. data/website/_includes/website/footer.html +14 -0
  16. data/website/_includes/website/github-icon.html +3 -0
  17. data/website/_includes/website/head.html +41 -0
  18. data/website/_includes/website/header.html +35 -0
  19. data/website/_includes/website/language-switcher.html +16 -0
  20. data/website/_includes/website/local-graph.html +25 -0
  21. data/website/_includes/website/note-header.html +35 -0
  22. data/website/_includes/website/outline.html +9 -0
  23. data/website/_includes/website/page-actions.html +34 -0
  24. data/website/_includes/website/portfolio-grid.html +36 -0
  25. data/website/_includes/website/portfolio-topics.html +4 -0
  26. data/website/_includes/website/post-cards.html +16 -0
  27. data/website/_includes/website/related-articles.html +7 -0
  28. data/website/_includes/website/relations.html +16 -0
  29. data/website/_includes/website/source-actions.html +28 -0
  30. data/website/_includes/website/system-page.html +31 -0
  31. data/website/_includes/website/tab-collection.html +28 -0
  32. data/website/_includes/website/tag-filter.html +12 -0
  33. data/website/_layouts/website-docs.html +20 -0
  34. data/website/_layouts/website-minimal.html +93 -0
  35. data/website/_layouts/website-redirect.html +17 -0
  36. data/website/assets/chunks/abnfDiagram-N423BO3Z-TSORAEI2.js +1 -0
  37. data/website/assets/chunks/architecture-TIHT7OUA-OUC5PEC7.js +1 -0
  38. data/website/assets/chunks/architectureDiagram-T3A2C74G-6P5MB2DJ.js +36 -0
  39. data/website/assets/chunks/blockDiagram-VBNYF7ZC-ZWINYROZ.js +132 -0
  40. data/website/assets/chunks/c4Diagram-5PPSVZJV-R5RMEAOT.js +10 -0
  41. data/website/assets/chunks/chunk-22DRK7VS.js +1 -0
  42. data/website/assets/chunks/chunk-25REBZ7M.js +2 -0
  43. data/website/assets/chunks/chunk-2RQCUDTG.js +1 -0
  44. data/website/assets/chunks/chunk-34FHPXU4.js +1 -0
  45. data/website/assets/chunks/chunk-57XXJOQN.js +10 -0
  46. data/website/assets/chunks/chunk-6DCNDVBX.js +2 -0
  47. data/website/assets/chunks/chunk-6DHFEWGX.js +1 -0
  48. data/website/assets/chunks/chunk-6VQ47JJS.js +2 -0
  49. data/website/assets/chunks/chunk-7CSIRG7W.js +127 -0
  50. data/website/assets/chunks/chunk-A43YELH5.js +1 -0
  51. data/website/assets/chunks/chunk-AIX6IH4O.js +1 -0
  52. data/website/assets/chunks/chunk-B3K7RH5O.js +88 -0
  53. data/website/assets/chunks/chunk-CBGUD2NP.js +1 -0
  54. data/website/assets/chunks/chunk-D3WOCELE.js +1 -0
  55. data/website/assets/chunks/chunk-D6FURU6X.js +1 -0
  56. data/website/assets/chunks/chunk-DGEGT77F.js +1 -0
  57. data/website/assets/chunks/chunk-DN52R6U7.js +1 -0
  58. data/website/assets/chunks/chunk-E3T3IFAC.js +1 -0
  59. data/website/assets/chunks/chunk-EIBFAW37.js +1 -0
  60. data/website/assets/chunks/chunk-ERWOH47R.js +1 -0
  61. data/website/assets/chunks/chunk-ETOVTYWF.js +1 -0
  62. data/website/assets/chunks/chunk-GHD3PFWA.js +231 -0
  63. data/website/assets/chunks/chunk-GMBKQ7NZ.js +1 -0
  64. data/website/assets/chunks/chunk-GQM25EPP.js +156 -0
  65. data/website/assets/chunks/chunk-GSOU4UTT.js +1 -0
  66. data/website/assets/chunks/chunk-HEMELP7J.js +1 -0
  67. data/website/assets/chunks/chunk-HZ5I5NJO.js +1 -0
  68. data/website/assets/chunks/chunk-IP56FOSI.js +70 -0
  69. data/website/assets/chunks/chunk-L7BMR7WL.js +1 -0
  70. data/website/assets/chunks/chunk-LGUG3SQB.js +1 -0
  71. data/website/assets/chunks/chunk-LOMHZYS2.js +1 -0
  72. data/website/assets/chunks/chunk-MII4VZEB.js +3 -0
  73. data/website/assets/chunks/chunk-MYMJLHQL.js +62 -0
  74. data/website/assets/chunks/chunk-NBH7YOBD.js +15 -0
  75. data/website/assets/chunks/chunk-NCOVM3YF.js +1 -0
  76. data/website/assets/chunks/chunk-OAGTSILX.js +1 -0
  77. data/website/assets/chunks/chunk-OCX5OCIU.js +321 -0
  78. data/website/assets/chunks/chunk-OE3NOVIH.js +1 -0
  79. data/website/assets/chunks/chunk-PLP5XUTJ.js +1 -0
  80. data/website/assets/chunks/chunk-PNOERP6G.js +1 -0
  81. data/website/assets/chunks/chunk-QBPQ7DPE.js +1 -0
  82. data/website/assets/chunks/chunk-RWFFDL53.js +1 -0
  83. data/website/assets/chunks/chunk-SEGIW3MP.js +1 -0
  84. data/website/assets/chunks/chunk-SZDMZNUS.js +1 -0
  85. data/website/assets/chunks/chunk-T2QBAPVT.js +1 -0
  86. data/website/assets/chunks/chunk-UE533OZ7.js +1 -0
  87. data/website/assets/chunks/chunk-UTWOJE2A.js +1 -0
  88. data/website/assets/chunks/chunk-VOCDYA3H.js +1 -0
  89. data/website/assets/chunks/chunk-VYZZPKGK.js +161 -0
  90. data/website/assets/chunks/chunk-WWLZIRS6.js +1 -0
  91. data/website/assets/chunks/chunk-XNXYDBKX.js +32 -0
  92. data/website/assets/chunks/chunk-XQUIO562.js +1 -0
  93. data/website/assets/chunks/chunk-Y2XVFW7T.js +2 -0
  94. data/website/assets/chunks/chunk-YORIST7Z.js +1 -0
  95. data/website/assets/chunks/chunk-Z2WBQPVY.js +206 -0
  96. data/website/assets/chunks/classDiagram-JCYQIIEL-OFDVDDIM.js +1 -0
  97. data/website/assets/chunks/classDiagram-v2-OCEON4UE-E36B6BGQ.js +1 -0
  98. data/website/assets/chunks/cose-bilkent-JH36ORCC-CBAW64AB.js +1 -0
  99. data/website/assets/chunks/cynefin-VYW2F7L2-CBO6DLNV.js +1 -0
  100. data/website/assets/chunks/cynefinDiagram-MW4NZA55-DFINLOUY.js +62 -0
  101. data/website/assets/chunks/dagre-VZM6K2ZE-BZFBSZAH.js +4 -0
  102. data/website/assets/chunks/diagram-7IWD3JNH-XQHTVOU2.js +30 -0
  103. data/website/assets/chunks/diagram-B4RE2ZJO-FTSXBZJR.js +3 -0
  104. data/website/assets/chunks/diagram-LBJQPF4R-ZSZW4OLH.js +24 -0
  105. data/website/assets/chunks/diagram-Q27KOJAE-2IV5TVCJ.js +24 -0
  106. data/website/assets/chunks/diagram-UB23O5K3-D6PDUTFL.js +41 -0
  107. data/website/assets/chunks/ebnfDiagram-BXEA7PRR-C6MAER3M.js +1 -0
  108. data/website/assets/chunks/erDiagram-JOGREHBK-EDAD2H5M.js +85 -0
  109. data/website/assets/chunks/eventmodeling-45OFAUF4-4K577DMF.js +1 -0
  110. data/website/assets/chunks/flowDiagram-UKHOOZJN-545F4YX3.js +1 -0
  111. data/website/assets/chunks/ganttDiagram-PKOTCBZU-7A7EKMY3.js +292 -0
  112. data/website/assets/chunks/gitGraph-TEB2WS4Q-3WUB7WYQ.js +1 -0
  113. data/website/assets/chunks/gitGraphDiagram-DS77QQ5N-A5UWV5FF.js +106 -0
  114. data/website/assets/chunks/info-DKCQHKI2-EOOHI7XS.js +1 -0
  115. data/website/assets/chunks/infoDiagram-6WML65LV-CWIOOWJP.js +2 -0
  116. data/website/assets/chunks/ishikawaDiagram-WSZJBQD7-CEASQVDL.js +70 -0
  117. data/website/assets/chunks/journeyDiagram-NVQOT4AX-LYG3S2AW.js +139 -0
  118. data/website/assets/chunks/kanban-definition-27J2QSJJ-HCC6KAYI.js +89 -0
  119. data/website/assets/chunks/katex-F4MQ6FB3.js +257 -0
  120. data/website/assets/chunks/mindmap-definition-FAOFIHXS-JMGSM3RE.js +96 -0
  121. data/website/assets/chunks/packet-7NZHBO7P-W3SJA3MY.js +1 -0
  122. data/website/assets/chunks/pegDiagram-VL7TDLO6-4JO3Y3Z3.js +1 -0
  123. data/website/assets/chunks/pie-RZYD4A2V-YOIQBTY7.js +1 -0
  124. data/website/assets/chunks/pieDiagram-7S7Q4E2Y-VFQ7SE46.js +39 -0
  125. data/website/assets/chunks/quadrantDiagram-CIZ2JOQS-JISDI2S6.js +7 -0
  126. data/website/assets/chunks/radar-I7S5WNFK-NMKP5AZO.js +1 -0
  127. data/website/assets/chunks/railroad-3IZDKUUU-7MOIV7CI.js +1 -0
  128. data/website/assets/chunks/railroad-abnf-AHOZXSZD-HXNH5SLO.js +1 -0
  129. data/website/assets/chunks/railroad-ebnf-EBAXGLYW-OWDJEWMK.js +1 -0
  130. data/website/assets/chunks/railroad-peg-LSFZ7HO6-ASSINGP5.js +1 -0
  131. data/website/assets/chunks/railroadDiagram-AXF67PYL-AFV5VP5F.js +1 -0
  132. data/website/assets/chunks/requirementDiagram-LRYGKXZP-RNRPEHHN.js +84 -0
  133. data/website/assets/chunks/sankeyDiagram-W5VNT64P-O54R2L3E.js +40 -0
  134. data/website/assets/chunks/sequenceDiagram-SI44F4Z6-42WISKJ7.js +162 -0
  135. data/website/assets/chunks/sizeCapture-X5ZJPWSS-LRN2Q37T.js +1 -0
  136. data/website/assets/chunks/stateDiagram-OKZ733FA-BWIBJXIR.js +1 -0
  137. data/website/assets/chunks/stateDiagram-v2-UEYNNEHI-L73LHL3L.js +1 -0
  138. data/website/assets/chunks/swimlanes-SLNWSIFB-ECOAE7LQ.js +1 -0
  139. data/website/assets/chunks/swimlanesDiagram-ULZ7WXOC-3CKKKFZZ.js +8 -0
  140. data/website/assets/chunks/timeline-definition-Z64GVDOM-HR2OL5G7.js +120 -0
  141. data/website/assets/chunks/treeView-QDETBFTQ-C6D5WFJA.js +1 -0
  142. data/website/assets/chunks/treemap-6X3UGDF4-JFCDMSCA.js +1 -0
  143. data/website/assets/chunks/vennDiagram-T6HMQDX7-62WAAPTF.js +34 -0
  144. data/website/assets/chunks/wardley-OPB4EBWU-RQIOII7I.js +1 -0
  145. data/website/assets/chunks/wardleyDiagram-T6FBY63Y-KUXO3NHV.js +78 -0
  146. data/website/assets/chunks/xychartDiagram-ELKLHX3M-I54W4HJ7.js +7 -0
  147. data/website/assets/color-scheme-bootstrap-4H4ZR4BW.js +1 -0
  148. data/website/assets/docs-3WBBQT6K.css +1 -0
  149. data/website/assets/docs-W3D5DWQF.js +1 -0
  150. data/website/assets/docs-navigation-75ALFYBA.js +1 -0
  151. data/website/assets/graph-QWL54LWD.js +1 -0
  152. data/website/assets/manifest.json +314 -0
  153. data/website/assets/math-7JTH4OWZ.js +21 -0
  154. data/website/assets/mermaid-RXC6URG4.js +10 -0
  155. data/website/assets/minimal-3NPTDWQX.js +1 -0
  156. data/website/assets/minimal-E2VATXXS.css +1 -0
  157. data/website/assets/previews-FMHOX7UX.js +1 -0
  158. data/website/assets/search-BH73JZDU.js +1 -0
  159. data/website/assets/search-worker-SM4IM4J7.js +1 -0
  160. data/website/exe/jekyll-obsidian +5 -0
  161. data/website/lib/jekyll_obsidian/adapter.rb +733 -0
  162. data/website/lib/jekyll_obsidian/built_in_themes.rb +855 -0
  163. data/website/lib/jekyll_obsidian/cli.rb +84 -0
  164. data/website/lib/jekyll_obsidian/content_policy.rb +194 -0
  165. data/website/lib/jekyll_obsidian/destination_registry.rb +39 -0
  166. data/website/lib/jekyll_obsidian/directory_indexes.rb +30 -0
  167. data/website/lib/jekyll_obsidian/external_media.rb +371 -0
  168. data/website/lib/jekyll_obsidian/front_matter.rb +453 -0
  169. data/website/lib/jekyll_obsidian/github_markdown.rb +664 -0
  170. data/website/lib/jekyll_obsidian/html_publication.rb +206 -0
  171. data/website/lib/jekyll_obsidian/initializer.rb +68 -0
  172. data/website/lib/jekyll_obsidian/localized_compiler.rb +1044 -0
  173. data/website/lib/jekyll_obsidian/media_policy.rb +41 -0
  174. data/website/lib/jekyll_obsidian/ofm_scanner.rb +493 -0
  175. data/website/lib/jekyll_obsidian/output_text.rb +13 -0
  176. data/website/lib/jekyll_obsidian/preview.rb +93 -0
  177. data/website/lib/jekyll_obsidian/published_markdown.rb +32 -0
  178. data/website/lib/jekyll_obsidian/raw_html_manifest.rb +66 -0
  179. data/website/lib/jekyll_obsidian/runtime.rb +197 -0
  180. data/website/lib/jekyll_obsidian/site_compilation.rb +147 -0
  181. data/website/lib/jekyll_obsidian/site_navigation.rb +517 -0
  182. data/website/lib/jekyll_obsidian/url_builder.rb +163 -0
  183. data/website/lib/jekyll_obsidian/value_objects.rb +198 -0
  184. data/website/lib/jekyll_obsidian/vault_compiler.rb +2771 -0
  185. data/website/lib/jekyll_obsidian/version.rb +6 -0
  186. data/website/lib/jekyll_obsidian/workspace_layout.rb +104 -0
  187. data/website/lib/jekyll_obsidian.rb +22 -0
  188. data/website/scripts/audit-site.rb +120 -0
  189. data/website/scripts/templates/pages.yml +55 -0
  190. data/website/scripts/verify-site-urls.rb +585 -0
  191. metadata +319 -0
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllObsidian
4
+ VERSION = "0.2.0"
5
+ GEM_NAME = "jekyll-obsidian-site"
6
+ end
@@ -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