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,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module JekyllObsidian
|
|
6
|
+
# Owns the serialized contract shared by the compiler, production audit, and
|
|
7
|
+
# URL verifier. Filesystem safety remains the responsibility of each
|
|
8
|
+
# consumer after this structural validation succeeds.
|
|
9
|
+
module RawHtmlManifest
|
|
10
|
+
PATH = "assets/website/raw-html.v1.json"
|
|
11
|
+
ROUTE = "/#{PATH}"
|
|
12
|
+
SCHEMA_VERSION = 1
|
|
13
|
+
KEYS = %w[documents files schema_version].freeze
|
|
14
|
+
DOCUMENT_KEYS = %w[output route].freeze
|
|
15
|
+
|
|
16
|
+
class Invalid < StandardError; end
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def generate(documents:, files:)
|
|
21
|
+
# Destination preflight owns duplicate-route diagnostics and runs after
|
|
22
|
+
# the manifest object is assembled. Keep generation total so collisions
|
|
23
|
+
# remain structured compiler failures; readers still validate strictly.
|
|
24
|
+
payload = {
|
|
25
|
+
"schema_version" => SCHEMA_VERSION,
|
|
26
|
+
"documents" => documents,
|
|
27
|
+
"files" => files
|
|
28
|
+
}
|
|
29
|
+
"#{JSON.generate(payload)}\n"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def parse(content)
|
|
33
|
+
payload = JSON.parse(content)
|
|
34
|
+
validate!(payload)
|
|
35
|
+
payload
|
|
36
|
+
rescue JSON::ParserError => exception
|
|
37
|
+
raise Invalid, "invalid JSON: #{exception.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate!(payload)
|
|
41
|
+
unless payload.is_a?(Hash) && payload.keys.sort == KEYS && payload["schema_version"] == SCHEMA_VERSION
|
|
42
|
+
raise Invalid, "expected the exact v#{SCHEMA_VERSION} object"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
documents = payload["documents"]
|
|
46
|
+
files = payload["files"]
|
|
47
|
+
unless documents.is_a?(Array) && files.is_a?(Array) &&
|
|
48
|
+
documents.uniq == documents && files.uniq == files && files.all? { |route| route.is_a?(String) }
|
|
49
|
+
raise Invalid, "documents and files must be unique arrays"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
documents.each do |document|
|
|
53
|
+
unless document.is_a?(Hash) && document.keys.sort == DOCUMENT_KEYS &&
|
|
54
|
+
document.values.all? { |value| value.is_a?(String) }
|
|
55
|
+
raise Invalid, "document entries must contain string route and output values"
|
|
56
|
+
end
|
|
57
|
+
unless files.include?(document.fetch("output"))
|
|
58
|
+
raise Invalid, "document output is not listed as a file: #{document.fetch('output').inspect}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
payload
|
|
63
|
+
end
|
|
64
|
+
private_class_method :validate!
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "open3"
|
|
6
|
+
require "pathname"
|
|
7
|
+
require "rbconfig"
|
|
8
|
+
require "tmpdir"
|
|
9
|
+
require "yaml"
|
|
10
|
+
require "jekyll"
|
|
11
|
+
require_relative "version"
|
|
12
|
+
require_relative "workspace_layout"
|
|
13
|
+
require_relative "adapter"
|
|
14
|
+
|
|
15
|
+
module JekyllObsidian
|
|
16
|
+
class Runtime
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
|
|
19
|
+
PACKAGE_ROOT = File.realpath(File.expand_path("../..", __dir__))
|
|
20
|
+
CONFIG_PATH = ".github/jekyll-obsidian.yml"
|
|
21
|
+
PUBLIC_KEYS = %w[title description lang url baseurl website].freeze
|
|
22
|
+
SITE_OPTIONS = {
|
|
23
|
+
"safe" => false, "incremental" => false, "disable_disk_cache" => true,
|
|
24
|
+
"strict_front_matter" => true, "permalink" => "pretty", "encoding" => "UTF-8",
|
|
25
|
+
"markdown" => "kramdown", "highlighter" => "none", "plugins" => [], "plugins_dir" => [],
|
|
26
|
+
"liquid" => { "error_mode" => "strict", "strict_filters" => true, "strict_variables" => false }
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :root, :config_path, :cache_root, :destination, :assets_root
|
|
30
|
+
|
|
31
|
+
def self.discover_root(start = Dir.pwd)
|
|
32
|
+
Pathname.new(File.expand_path(start)).ascend do |path|
|
|
33
|
+
return path.to_s if File.exist?(path.join(CONFIG_PATH)) || File.symlink?(path.join(CONFIG_PATH))
|
|
34
|
+
end
|
|
35
|
+
raise Error, "Missing #{CONFIG_PATH}. Run jekyll-obsidian init from your repository root."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(root:, config_path: nil, assets_root: nil, destination_name: "site")
|
|
39
|
+
@root = File.realpath(root)
|
|
40
|
+
@config_path = config_path || File.join(@root, CONFIG_PATH)
|
|
41
|
+
@assets_root = assets_root || File.join(PACKAGE_ROOT, "assets")
|
|
42
|
+
@cache_root = File.join(@root, WorkspaceLayout::CACHE_DIRECTORY)
|
|
43
|
+
unless destination_name.match?(/\Asite(?:-[A-Za-z0-9][A-Za-z0-9._-]*)?\z/)
|
|
44
|
+
raise Error, "destination name must be site or site-NAME"
|
|
45
|
+
end
|
|
46
|
+
@destination = File.join(@cache_root, destination_name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def configuration
|
|
50
|
+
WorkspaceLayout.assert_path!(@root, @config_path, "configuration")
|
|
51
|
+
raise Error, "configuration must be a regular file" unless File.file?(@config_path)
|
|
52
|
+
|
|
53
|
+
user = YAML.safe_load_file(@config_path, permitted_classes: [Date, Time], aliases: true)
|
|
54
|
+
raise Error, "#{CONFIG_PATH} must be a mapping" unless user.is_a?(Hash)
|
|
55
|
+
unknown = user.keys - PUBLIC_KEYS
|
|
56
|
+
raise Error, "unsupported site configuration: #{unknown.first}" unless unknown.empty?
|
|
57
|
+
if user.key?("website") && !user["website"].is_a?(Hash)
|
|
58
|
+
raise Error, "website must be a mapping"
|
|
59
|
+
end
|
|
60
|
+
defaults = YAML.safe_load_file(File.join(PACKAGE_ROOT, "_config.yml"), aliases: false)
|
|
61
|
+
Jekyll::Utils.deep_merge_hashes(defaults, user)
|
|
62
|
+
rescue Psych::Exception => exception
|
|
63
|
+
raise Error, "Cannot read #{CONFIG_PATH}: #{exception.message}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def source_root(config = configuration)
|
|
67
|
+
source = WorkspaceLayout.relative_source(config.fetch("website").fetch("source"))
|
|
68
|
+
path = File.join(@root, source)
|
|
69
|
+
WorkspaceLayout.assert_path!(@root, path, "website.source")
|
|
70
|
+
raise Error, "website.source must be a directory: #{source}" unless File.directory?(path)
|
|
71
|
+
path
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build(url: nil, baseurl: nil, theme: nil, environment: "production", quiet: false)
|
|
75
|
+
config = configuration
|
|
76
|
+
config["website"]["theme"] = theme if theme
|
|
77
|
+
config["url"] = url unless url.nil?
|
|
78
|
+
config["baseurl"] = baseurl unless baseurl.nil?
|
|
79
|
+
validate_urls!(config, environment)
|
|
80
|
+
# Validate content before creating or changing any build state.
|
|
81
|
+
source_root(config)
|
|
82
|
+
with_lock do
|
|
83
|
+
runtime_root = prepare_runtime
|
|
84
|
+
staging = Dir.mktmpdir("site-build.", @cache_root)
|
|
85
|
+
previous_environment = ENV["JEKYLL_ENV"]
|
|
86
|
+
begin
|
|
87
|
+
ENV["JEKYLL_ENV"] = environment
|
|
88
|
+
site_config = Jekyll.configuration(config.merge(SITE_OPTIONS).merge(
|
|
89
|
+
"config" => [], "source" => runtime_root, "destination" => staging,
|
|
90
|
+
"cache_dir" => ".jekyll-cache", "quiet" => quiet,
|
|
91
|
+
"layouts_dir" => "_layouts", "includes_dir" => "_includes",
|
|
92
|
+
"data_dir" => "_data", "collections_dir" => "",
|
|
93
|
+
WorkspaceLayout::CONTEXT_KEY => { "workspace_root" => @root, "assets_root" => @assets_root }
|
|
94
|
+
))
|
|
95
|
+
Jekyll::Site.new(site_config).process
|
|
96
|
+
audit!(staging, config, quiet:) if environment == "production"
|
|
97
|
+
publish!(staging)
|
|
98
|
+
ensure
|
|
99
|
+
previous_environment.nil? ? ENV.delete("JEKYLL_ENV") : ENV["JEKYLL_ENV"] = previous_environment
|
|
100
|
+
FileUtils.remove_entry(staging) if File.exist?(staging)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
@destination
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def clean
|
|
107
|
+
return unless File.exist?(@cache_root) || File.symlink?(@cache_root)
|
|
108
|
+
|
|
109
|
+
with_lock do
|
|
110
|
+
Dir.children(@cache_root).each do |entry|
|
|
111
|
+
next if %w[.lock .gitignore].include?(entry)
|
|
112
|
+
|
|
113
|
+
FileUtils.remove_entry(File.join(@cache_root, entry))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def validate_urls!(config, environment)
|
|
121
|
+
origin = config.fetch("url", "").to_s.delete_suffix("/")
|
|
122
|
+
baseurl = config.fetch("baseurl", "").to_s.delete_suffix("/")
|
|
123
|
+
if environment == "production" && origin.empty?
|
|
124
|
+
raise Error, "production builds require --url or url in #{CONFIG_PATH}"
|
|
125
|
+
end
|
|
126
|
+
unless origin.empty? || origin.match?(%r{\Ahttps?://(?:[A-Za-z0-9][A-Za-z0-9.-]*|\[[0-9A-Fa-f:]+\])(?::[0-9]+)?\z})
|
|
127
|
+
raise Error, "url must be an HTTP or HTTPS origin without a path, query, or fragment"
|
|
128
|
+
end
|
|
129
|
+
unless baseurl.empty? || (baseurl.match?(%r{\A(/[A-Za-z0-9._~!$&()*+,;=:@%-]+)+\z}) &&
|
|
130
|
+
baseurl.split("/").none? { |part| %w[. ..].include?(part) })
|
|
131
|
+
raise Error, "baseurl must be empty or a normalized site path such as /project"
|
|
132
|
+
end
|
|
133
|
+
config["url"] = origin
|
|
134
|
+
config["baseurl"] = baseurl
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def with_lock
|
|
138
|
+
WorkspaceLayout.assert_path!(@root, @cache_root, "application cache", missing: true)
|
|
139
|
+
FileUtils.mkdir_p(@cache_root)
|
|
140
|
+
ignore = File.join(@cache_root, ".gitignore")
|
|
141
|
+
WorkspaceLayout.assert_path!(@root, ignore, "cache ignore file", missing: true)
|
|
142
|
+
File.write(ignore, "*\n") unless File.exist?(ignore)
|
|
143
|
+
lock_path = File.join(@cache_root, ".lock")
|
|
144
|
+
WorkspaceLayout.assert_path!(@root, lock_path, "build lock", missing: true)
|
|
145
|
+
File.open(lock_path, File::RDWR | File::CREAT, 0o600) do |lock|
|
|
146
|
+
raise Error, "another jekyll-obsidian command is using this workspace" unless lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
147
|
+
yield
|
|
148
|
+
ensure
|
|
149
|
+
lock.flock(File::LOCK_UN)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def prepare_runtime
|
|
154
|
+
runtime_root = File.join(@cache_root, "runtime")
|
|
155
|
+
WorkspaceLayout.assert_path!(@root, runtime_root, "Jekyll source", missing: true)
|
|
156
|
+
FileUtils.remove_entry(runtime_root) if File.exist?(runtime_root)
|
|
157
|
+
FileUtils.mkdir_p(runtime_root)
|
|
158
|
+
%w[_layouts _includes].each do |directory|
|
|
159
|
+
FileUtils.cp_r(File.join(PACKAGE_ROOT, directory), File.join(runtime_root, directory))
|
|
160
|
+
end
|
|
161
|
+
runtime_root
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def audit!(staging, config, quiet:)
|
|
165
|
+
[
|
|
166
|
+
["audit-site.rb", staging],
|
|
167
|
+
["verify-site-urls.rb", staging, config.fetch("url"), config.fetch("baseurl")]
|
|
168
|
+
].each do |script, *arguments|
|
|
169
|
+
output, status = Open3.capture2e(RbConfig.ruby, File.join(PACKAGE_ROOT, "scripts", script), *arguments)
|
|
170
|
+
raise Error, output.strip unless status.success?
|
|
171
|
+
puts output unless quiet
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def publish!(staging)
|
|
176
|
+
WorkspaceLayout.assert_path!(@root, @destination, "destination", missing: true)
|
|
177
|
+
if File.exist?(@destination) && !File.directory?(@destination)
|
|
178
|
+
raise Error, "destination must be a directory"
|
|
179
|
+
end
|
|
180
|
+
backup = File.join(@cache_root, "site-backup")
|
|
181
|
+
raise Error, "stale site backup exists: #{backup}" if File.exist?(backup) || File.symlink?(backup)
|
|
182
|
+
|
|
183
|
+
moved_previous = false
|
|
184
|
+
begin
|
|
185
|
+
if File.exist?(@destination)
|
|
186
|
+
File.rename(@destination, backup)
|
|
187
|
+
moved_previous = true
|
|
188
|
+
end
|
|
189
|
+
File.rename(staging, @destination)
|
|
190
|
+
rescue SystemCallError
|
|
191
|
+
File.rename(backup, @destination) if moved_previous && !File.exist?(@destination)
|
|
192
|
+
raise
|
|
193
|
+
end
|
|
194
|
+
FileUtils.remove_entry(backup) if moved_previous
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module JekyllObsidian
|
|
6
|
+
# Owns the impure boundary for remote build inputs. VaultCompiler remains a
|
|
7
|
+
# deterministic projection over one immutable snapshot.
|
|
8
|
+
class SiteCompilation
|
|
9
|
+
TRANSLATION_PREFIX = "_translations/"
|
|
10
|
+
|
|
11
|
+
PlannedReference = ImmutableRecord.define(:physical_path, :logical_path, :reference)
|
|
12
|
+
|
|
13
|
+
def self.compile(request, transport: nil, cache_root: nil, manifest: nil)
|
|
14
|
+
new(request, transport:, cache_root:, manifest:).compile
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(request, transport:, cache_root:, manifest:)
|
|
18
|
+
@request = request
|
|
19
|
+
@transport = transport
|
|
20
|
+
@cache_root = cache_root
|
|
21
|
+
@manifest = manifest
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def compile
|
|
25
|
+
baseline = VaultCompiler.compile(@request)
|
|
26
|
+
return baseline unless baseline.success?
|
|
27
|
+
|
|
28
|
+
plan, diagnostics = build_plan(baseline)
|
|
29
|
+
return failure(baseline.diagnostics + diagnostics) unless diagnostics.empty?
|
|
30
|
+
if plan.empty?
|
|
31
|
+
GitHubMarkdown.materialize([], manifest: @manifest) unless @manifest.nil?
|
|
32
|
+
return baseline
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
documents = GitHubMarkdown.materialize(
|
|
36
|
+
plan.map(&:reference),
|
|
37
|
+
transport: @transport,
|
|
38
|
+
cache_root: @cache_root,
|
|
39
|
+
manifest: @manifest
|
|
40
|
+
)
|
|
41
|
+
documents_by_path = plan.each_with_index.to_h do |item, index|
|
|
42
|
+
[item.physical_path, documents.fetch(index)]
|
|
43
|
+
end
|
|
44
|
+
snapshot = Snapshot.new(entries: @request.snapshot.entries.map do |entry|
|
|
45
|
+
document = documents_by_path[entry.path.to_s]
|
|
46
|
+
document ? SnapshotEntry.new(**entry.to_h.merge(external_document: document)) : entry
|
|
47
|
+
end)
|
|
48
|
+
final_request = BuildRequest.new(snapshot: snapshot, config: @request.config)
|
|
49
|
+
result = VaultCompiler.compile(final_request)
|
|
50
|
+
return result unless result.success?
|
|
51
|
+
|
|
52
|
+
BuildSuccess.new(**result.to_h.merge(
|
|
53
|
+
github_markdown_manifest: GitHubMarkdown.dump_manifest(documents)
|
|
54
|
+
))
|
|
55
|
+
rescue GitHubMarkdown::ManifestError => exception
|
|
56
|
+
failure([diagnostic("github_markdown_manifest_invalid", exception.message)])
|
|
57
|
+
rescue GitHubMarkdown::Error => exception
|
|
58
|
+
failure([diagnostic("github_markdown_fetch_failed", exception.message)])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def build_plan(result)
|
|
64
|
+
published = result.notes.map(&:id).to_set
|
|
65
|
+
portfolio_path = configured_portfolio_path
|
|
66
|
+
physical_note_paths = @request.snapshot.entries.filter_map do |entry|
|
|
67
|
+
path = entry.path.to_s
|
|
68
|
+
path if entry.kind.to_sym == :note && !path.start_with?(TRANSLATION_PREFIX)
|
|
69
|
+
end
|
|
70
|
+
portfolio_index_path = DirectoryIndexes.resolve(physical_note_paths)[portfolio_path]
|
|
71
|
+
locales = Array(result.site_data.dig("website_i18n", "locales")).map { |item| item.fetch("code") }
|
|
72
|
+
default_locale = @request.config.lang.to_s
|
|
73
|
+
plan = []
|
|
74
|
+
diagnostics = []
|
|
75
|
+
|
|
76
|
+
@request.snapshot.entries.sort_by { |entry| entry.path.to_s.b }.each do |entry|
|
|
77
|
+
next unless entry.kind.to_sym == :note
|
|
78
|
+
|
|
79
|
+
physical_path = entry.path.to_s
|
|
80
|
+
locale, logical_path = translated_path(physical_path, locales, default_locale)
|
|
81
|
+
next if physical_path.start_with?(TRANSLATION_PREFIX) && locale.nil?
|
|
82
|
+
|
|
83
|
+
parsed = FrontMatter.parse(physical_path, entry.bytes.to_s)
|
|
84
|
+
reference = parsed.properties["github_markdown"]
|
|
85
|
+
next unless reference
|
|
86
|
+
next if parsed.properties["publish"] == false
|
|
87
|
+
next unless published_note?(published, logical_path, locale)
|
|
88
|
+
|
|
89
|
+
unless logical_path.start_with?("#{portfolio_path}/") && logical_path != portfolio_index_path
|
|
90
|
+
diagnostics << diagnostic(
|
|
91
|
+
"github_markdown_scope",
|
|
92
|
+
"github_markdown is only supported by project wrappers below the configured portfolio path",
|
|
93
|
+
physical_path
|
|
94
|
+
)
|
|
95
|
+
next
|
|
96
|
+
end
|
|
97
|
+
unless parsed.body.strip.empty?
|
|
98
|
+
diagnostics << diagnostic(
|
|
99
|
+
"github_markdown_body_conflict",
|
|
100
|
+
"a github_markdown project wrapper must have an empty local body",
|
|
101
|
+
physical_path
|
|
102
|
+
)
|
|
103
|
+
next
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
plan << PlannedReference.new(
|
|
107
|
+
physical_path: physical_path,
|
|
108
|
+
logical_path: logical_path,
|
|
109
|
+
reference: reference
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
[plan.freeze, diagnostics.freeze]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def configured_portfolio_path
|
|
116
|
+
navigation = @request.config.navigation
|
|
117
|
+
value = navigation.is_a?(Hash) ? navigation.dig("portfolio", "path") : nil
|
|
118
|
+
(value || "portfolio").to_s.unicode_normalize(:nfc)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def translated_path(path, locales, default_locale)
|
|
122
|
+
return [nil, path] unless path.start_with?(TRANSLATION_PREFIX)
|
|
123
|
+
|
|
124
|
+
suffix = path.delete_prefix(TRANSLATION_PREFIX)
|
|
125
|
+
locale, logical = suffix.split("/", 2)
|
|
126
|
+
return [nil, nil] unless logical && locale != default_locale && locales.include?(locale)
|
|
127
|
+
|
|
128
|
+
[locale, logical]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def published_note?(published, logical_path, locale)
|
|
132
|
+
return published.include?(logical_path) unless locale
|
|
133
|
+
|
|
134
|
+
published.include?(logical_path) && published.include?("#{locale}:#{logical_path}")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def diagnostic(code, message, path = nil)
|
|
138
|
+
Diagnostic.new(severity: :error, code: code, message: message, path: path, span: nil)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def failure(diagnostics)
|
|
142
|
+
BuildFailure.new(diagnostics: diagnostics.sort_by do |item|
|
|
143
|
+
[item.path.to_s, item.code.to_s, item.message.to_s]
|
|
144
|
+
end.freeze)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|