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,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "runtime"
|
|
5
|
+
require_relative "initializer"
|
|
6
|
+
|
|
7
|
+
module JekyllObsidian
|
|
8
|
+
class CLI
|
|
9
|
+
def self.run(arguments = ARGV)
|
|
10
|
+
new.run(arguments.dup)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(arguments)
|
|
14
|
+
command = arguments.shift || "--help"
|
|
15
|
+
if %w[--version -v].include?(command)
|
|
16
|
+
puts "jekyll-obsidian #{VERSION} (#{GEM_NAME})"
|
|
17
|
+
return 0
|
|
18
|
+
end
|
|
19
|
+
if %w[--help -h help].include?(command)
|
|
20
|
+
puts "Usage: jekyll-obsidian <init|build|dev|clean> [options]"
|
|
21
|
+
puts "Configuration: .github/jekyll-obsidian.yml"
|
|
22
|
+
puts "Run jekyll-obsidian COMMAND --help for command options."
|
|
23
|
+
return 0
|
|
24
|
+
end
|
|
25
|
+
raise ArgumentError, "unknown command: #{command}" unless %w[init build dev clean].include?(command)
|
|
26
|
+
|
|
27
|
+
options = {}
|
|
28
|
+
parser = OptionParser.new do |parser|
|
|
29
|
+
parser.banner = "Usage: jekyll-obsidian #{command} [options]"
|
|
30
|
+
if command == "init"
|
|
31
|
+
parser.on("--source PATH", "Content directory relative to the repository (default: docs)") { |value| options[:source] = value }
|
|
32
|
+
end
|
|
33
|
+
if %w[init build dev].include?(command)
|
|
34
|
+
parser.on("--theme minimal|docs", %w[minimal docs], "Site theme (default: configuration, or minimal)") { |value| options[:theme] = value }
|
|
35
|
+
end
|
|
36
|
+
if %w[build dev].include?(command)
|
|
37
|
+
parser.on("--baseurl PATH", "Site path, such as /project") { |value| options[:baseurl] = value }
|
|
38
|
+
end
|
|
39
|
+
if command == "build"
|
|
40
|
+
parser.on("--url ORIGIN", "Production origin, such as https://example.com") { |value| options[:url] = value }
|
|
41
|
+
end
|
|
42
|
+
if command == "dev"
|
|
43
|
+
parser.on("--host HOST", "Preview host (default: 127.0.0.1)") { |value| options[:host] = value }
|
|
44
|
+
parser.on("--port PORT", Integer, "Preview port (default: 58000)") do |value|
|
|
45
|
+
raise ArgumentError, "port must be between 1 and 65535" unless (1..65_535).cover?(value)
|
|
46
|
+
options[:port] = value
|
|
47
|
+
end
|
|
48
|
+
parser.separator "Watching is always enabled. Builds are complete and atomic."
|
|
49
|
+
parser.separator "Jekyll --watch and --incremental are not supported."
|
|
50
|
+
end
|
|
51
|
+
parser.on("-h", "--help", "Show this help") { puts parser; return 0 }
|
|
52
|
+
end
|
|
53
|
+
parser.parse!(arguments)
|
|
54
|
+
raise ArgumentError, "unexpected argument: #{arguments.first}" unless arguments.empty?
|
|
55
|
+
|
|
56
|
+
if command == "init"
|
|
57
|
+
created = Initializer.run(root: Dir.pwd, **options)
|
|
58
|
+
puts(created.empty? ? "Site files already exist." : "Created #{created.join(", ")}")
|
|
59
|
+
puts "Edit .github/jekyll-obsidian.yml, then push to GitHub or run jekyll-obsidian dev."
|
|
60
|
+
puts "For GitHub Pages, select Settings > Pages > Source > GitHub Actions."
|
|
61
|
+
return 0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
runtime = Runtime.new(root: Runtime.discover_root)
|
|
65
|
+
case command
|
|
66
|
+
when "build"
|
|
67
|
+
puts "Built #{runtime.build(**options)}"
|
|
68
|
+
when "dev"
|
|
69
|
+
require_relative "preview"
|
|
70
|
+
Preview.new(runtime, **options).run
|
|
71
|
+
when "clean"
|
|
72
|
+
runtime.clean
|
|
73
|
+
puts "Build output is clean."
|
|
74
|
+
end
|
|
75
|
+
0
|
|
76
|
+
rescue OptionParser::ParseError, ArgumentError, Runtime::Error, WorkspaceLayout::Invalid,
|
|
77
|
+
Jekyll::Errors::FatalException, SystemCallError => exception
|
|
78
|
+
warn "Error: #{exception.message}"
|
|
79
|
+
1
|
|
80
|
+
rescue Interrupt
|
|
81
|
+
130
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllObsidian
|
|
4
|
+
# Owns the publication and content-classification rules for authored notes.
|
|
5
|
+
# Callers resolve configuration once, then use the same policy while selecting
|
|
6
|
+
# default-language notes, applying locale overlays, and classifying output.
|
|
7
|
+
class ContentPolicy
|
|
8
|
+
ROOT_DIRECTORY = "."
|
|
9
|
+
DEFAULT_SETTINGS = DeepFreeze.call(
|
|
10
|
+
"publish_by_default" => [],
|
|
11
|
+
"default_type" => "page",
|
|
12
|
+
"directories" => { "post" => ["blog"], "doc" => ["docs"] }
|
|
13
|
+
)
|
|
14
|
+
Resolution = ImmutableRecord.define(:policy, :diagnostics)
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def resolve(raw)
|
|
18
|
+
diagnostics = []
|
|
19
|
+
settings = normalize(raw, diagnostics)
|
|
20
|
+
Resolution.new(policy: new(settings), diagnostics: diagnostics)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def normalize(raw, diagnostics)
|
|
26
|
+
unless raw.nil? || raw.is_a?(Hash)
|
|
27
|
+
diagnostics << diagnostic("invalid_content_config", "content must be a mapping")
|
|
28
|
+
return DEFAULT_SETTINGS
|
|
29
|
+
end
|
|
30
|
+
raw ||= {}
|
|
31
|
+
unknown = raw.keys.map(&:to_s) - %w[publish_by_default default_type directories]
|
|
32
|
+
unknown.sort.each do |key|
|
|
33
|
+
diagnostics << diagnostic("invalid_content_config", "unknown content setting #{key.inspect}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
default_type = fetch(raw, "default_type") || DEFAULT_SETTINGS.fetch("default_type")
|
|
37
|
+
unless FrontMatter::CONTENT_TYPES.include?(default_type)
|
|
38
|
+
diagnostics << diagnostic(
|
|
39
|
+
"invalid_content_config",
|
|
40
|
+
"content.default_type must be one of: #{FrontMatter::CONTENT_TYPES.join(', ')}"
|
|
41
|
+
)
|
|
42
|
+
default_type = DEFAULT_SETTINGS.fetch("default_type")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
publish_by_default = normalize_publish_directories(raw, diagnostics)
|
|
46
|
+
directories = normalize_content_directories(raw, diagnostics)
|
|
47
|
+
directories.fetch("post").product(directories.fetch("doc")).each do |post_directory, doc_directory|
|
|
48
|
+
next unless directory_overlaps?(post_directory, doc_directory)
|
|
49
|
+
|
|
50
|
+
diagnostics << diagnostic(
|
|
51
|
+
"overlapping_content_directories",
|
|
52
|
+
"post and doc directories overlap: #{post_directory.inspect} and #{doc_directory.inspect}"
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
DeepFreeze.call(
|
|
57
|
+
"publish_by_default" => publish_by_default,
|
|
58
|
+
"default_type" => default_type,
|
|
59
|
+
"directories" => directories
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def normalize_publish_directories(raw, diagnostics)
|
|
64
|
+
values = if raw.key?("publish_by_default") || raw.key?(:publish_by_default)
|
|
65
|
+
fetch(raw, "publish_by_default")
|
|
66
|
+
else
|
|
67
|
+
DEFAULT_SETTINGS.fetch("publish_by_default")
|
|
68
|
+
end
|
|
69
|
+
unless values.is_a?(Array)
|
|
70
|
+
diagnostics << diagnostic(
|
|
71
|
+
"invalid_publish_by_default",
|
|
72
|
+
"content.publish_by_default must be an array"
|
|
73
|
+
)
|
|
74
|
+
values = []
|
|
75
|
+
end
|
|
76
|
+
values.filter_map do |value|
|
|
77
|
+
normalize_directory(value, "content.publish_by_default", diagnostics, allow_root: true)
|
|
78
|
+
end.uniq.sort
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def normalize_content_directories(raw, diagnostics)
|
|
82
|
+
directories = if raw.key?("directories") || raw.key?(:directories)
|
|
83
|
+
fetch(raw, "directories")
|
|
84
|
+
else
|
|
85
|
+
DEFAULT_SETTINGS.fetch("directories")
|
|
86
|
+
end
|
|
87
|
+
unless directories.is_a?(Hash)
|
|
88
|
+
diagnostics << diagnostic("invalid_content_directories", "content.directories must be a mapping")
|
|
89
|
+
directories = {}
|
|
90
|
+
end
|
|
91
|
+
unknown = directories.keys.map(&:to_s) - %w[post doc]
|
|
92
|
+
unknown.sort.each do |key|
|
|
93
|
+
diagnostics << diagnostic(
|
|
94
|
+
"invalid_content_directories",
|
|
95
|
+
"unknown content directory type #{key.inspect}"
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
%w[post doc].to_h do |type|
|
|
100
|
+
values = fetch(directories, type) || []
|
|
101
|
+
unless values.is_a?(Array)
|
|
102
|
+
diagnostics << diagnostic(
|
|
103
|
+
"invalid_content_directories",
|
|
104
|
+
"content.directories.#{type} must be an array"
|
|
105
|
+
)
|
|
106
|
+
values = []
|
|
107
|
+
end
|
|
108
|
+
normalized = values.filter_map do |value|
|
|
109
|
+
normalize_directory(value, "content.directories.#{type}", diagnostics, allow_root: false)
|
|
110
|
+
end.uniq.sort
|
|
111
|
+
[type, normalized]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def normalize_directory(value, setting, diagnostics, allow_root:)
|
|
116
|
+
unless FrontMatter.valid_output_text?(value)
|
|
117
|
+
diagnostics << diagnostic("invalid_content_directory", "#{setting} entries must be strings")
|
|
118
|
+
return nil
|
|
119
|
+
end
|
|
120
|
+
return ROOT_DIRECTORY if allow_root && value == ROOT_DIRECTORY
|
|
121
|
+
|
|
122
|
+
if value.empty? || value.start_with?("/", "\\") || value.include?("\\") || value != value.unicode_normalize(:nfc)
|
|
123
|
+
diagnostics << diagnostic(
|
|
124
|
+
"invalid_content_directory",
|
|
125
|
+
"#{setting} entries must be normalized vault-relative POSIX directories"
|
|
126
|
+
)
|
|
127
|
+
return nil
|
|
128
|
+
end
|
|
129
|
+
segments = value.split("/", -1)
|
|
130
|
+
if segments.any? { |segment| segment.empty? || segment == "." || segment == ".." }
|
|
131
|
+
diagnostics << diagnostic(
|
|
132
|
+
"invalid_content_directory",
|
|
133
|
+
"#{setting} entries must not contain empty or traversal segments"
|
|
134
|
+
)
|
|
135
|
+
return nil
|
|
136
|
+
end
|
|
137
|
+
value
|
|
138
|
+
rescue EncodingError
|
|
139
|
+
diagnostics << diagnostic("invalid_content_directory", "#{setting} entries must contain valid Unicode")
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def directory_overlaps?(first, second)
|
|
144
|
+
left = first.downcase(:fold)
|
|
145
|
+
right = second.downcase(:fold)
|
|
146
|
+
left == right || left.start_with?("#{right}/") || right.start_with?("#{left}/")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def fetch(hash, key)
|
|
150
|
+
hash.key?(key) ? hash[key] : hash[key.to_sym]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def diagnostic(code, message)
|
|
154
|
+
Diagnostic.new(severity: :error, code: code, message: message, path: nil, span: nil)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private_class_method :new
|
|
159
|
+
|
|
160
|
+
attr_reader :settings
|
|
161
|
+
|
|
162
|
+
def initialize(settings)
|
|
163
|
+
@settings = settings
|
|
164
|
+
@publish_by_default = settings.fetch("publish_by_default")
|
|
165
|
+
@directories = settings.fetch("directories")
|
|
166
|
+
@default_type = settings.fetch("default_type")
|
|
167
|
+
freeze
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def publish?(path, properties)
|
|
171
|
+
return false if properties["publish"] == false
|
|
172
|
+
return true if properties["publish"] == true
|
|
173
|
+
|
|
174
|
+
@publish_by_default.any? { |directory| inside_directory?(path, directory) }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def classify(path, properties)
|
|
178
|
+
return "page" if path == "index.md"
|
|
179
|
+
return properties["content_type"] if properties["content_type"]
|
|
180
|
+
|
|
181
|
+
directory = File.dirname(path)
|
|
182
|
+
@directories.each do |type, configured|
|
|
183
|
+
return type if configured.any? { |prefix| directory == prefix || directory.start_with?("#{prefix}/") }
|
|
184
|
+
end
|
|
185
|
+
@default_type
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
def inside_directory?(path, directory)
|
|
191
|
+
directory == ROOT_DIRECTORY || path.start_with?("#{directory}/")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllObsidian
|
|
4
|
+
# Tracks normalized output destinations while treating a file and any path
|
|
5
|
+
# below that file as a collision. Each insertion visits only its path
|
|
6
|
+
# segments, so large sites do not compare every output with every other one.
|
|
7
|
+
class DestinationRegistry
|
|
8
|
+
Node = Struct.new(:owner, :descendant_owner, :children, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@root = node
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def add(destination, owner)
|
|
15
|
+
current = @root
|
|
16
|
+
lineage = [current]
|
|
17
|
+
destination.split("/").reject(&:empty?).each do |segment|
|
|
18
|
+
return current.owner if current.owner
|
|
19
|
+
|
|
20
|
+
current.children[segment] ||= node
|
|
21
|
+
current = current.children.fetch(segment)
|
|
22
|
+
lineage << current
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
return current.owner if current.owner
|
|
26
|
+
return current.descendant_owner if current.descendant_owner
|
|
27
|
+
|
|
28
|
+
current.owner = owner
|
|
29
|
+
lineage[0...-1].each { |ancestor| ancestor.descendant_owner ||= owner }
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def node
|
|
36
|
+
Node.new(owner: nil, descendant_owner: nil, children: {})
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllObsidian
|
|
4
|
+
# Selects the one physical note path that may act as each directory's index.
|
|
5
|
+
# Publication is deliberately applied later: a private index.md still owns
|
|
6
|
+
# the physical slot and therefore prevents README.md from taking it over.
|
|
7
|
+
module DirectoryIndexes
|
|
8
|
+
PRIMARY_BASENAME = "index.md"
|
|
9
|
+
FALLBACK_BASENAME = "README.md"
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def resolve(note_paths)
|
|
14
|
+
paths = Array(note_paths).map(&:to_s)
|
|
15
|
+
selected = {}
|
|
16
|
+
|
|
17
|
+
paths.each do |path|
|
|
18
|
+
selected[File.dirname(path)] = path if File.basename(path) == PRIMARY_BASENAME
|
|
19
|
+
end
|
|
20
|
+
paths.each do |path|
|
|
21
|
+
directory = File.dirname(path)
|
|
22
|
+
if File.basename(path) == FALLBACK_BASENAME && !selected.key?(directory)
|
|
23
|
+
selected[directory] = path
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
DeepFreeze.call(selected)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|