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,855 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/escape"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module JekyllObsidian
|
|
7
|
+
# The only internal theme seam. Each built-in adapter consumes the same
|
|
8
|
+
# immutable PublishedSiteModel and returns a complete ThemeOutput through a
|
|
9
|
+
# single render interface.
|
|
10
|
+
module BuiltInThemes
|
|
11
|
+
IDS = %w[minimal docs].freeze
|
|
12
|
+
ALWAYS_RESERVED_NAMESPACES = %w[
|
|
13
|
+
/404.html /sitemap.xml /assets/website /assets/vault
|
|
14
|
+
].freeze
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def resolve(id)
|
|
18
|
+
case id
|
|
19
|
+
when "minimal" then Minimal.new
|
|
20
|
+
when "docs" then Docs.new
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "unknown built-in theme #{id.inspect}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Presenter
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def project(model:, config:, landing_note:, note_theme_data:, theme_pages:, system_theme_data:, taxonomy:, feed_notes:, shared_files: [], suppressed_note_routes: [], ¬e_page_transform)
|
|
30
|
+
topic_anchors = taxonomy.fetch("anchors")
|
|
31
|
+
tag_groups = taxonomy.fetch("groups")
|
|
32
|
+
topic_summaries = taxonomy.fetch("summaries")
|
|
33
|
+
local_graphs = config.features.fetch("graph") ? local_graphs(model, config) : {}
|
|
34
|
+
home_route = landing_note ? landing_note.route : "/"
|
|
35
|
+
home_url = config.url_builder.href(home_route)
|
|
36
|
+
pages = model.notes.reject { |note| suppressed_note_routes.include?(note.route) }.map do |note|
|
|
37
|
+
output = note_page(
|
|
38
|
+
note,
|
|
39
|
+
config,
|
|
40
|
+
note_theme_data.fetch(note.id),
|
|
41
|
+
topic_anchors,
|
|
42
|
+
topic_summaries,
|
|
43
|
+
local_graphs[note.id],
|
|
44
|
+
home_route,
|
|
45
|
+
home_url
|
|
46
|
+
)
|
|
47
|
+
note_page_transform ? note_page_transform.call(note, output) : output
|
|
48
|
+
end
|
|
49
|
+
pages.concat(theme_pages)
|
|
50
|
+
if config.theme == "docs" && config.features.fetch("tags")
|
|
51
|
+
pages << tags_page(tag_groups, config, system_theme_data, home_route, home_url)
|
|
52
|
+
end
|
|
53
|
+
pages << not_found_page(config, system_theme_data, home_route, home_url)
|
|
54
|
+
pages << redirect_page(landing_note, config, home_route, home_url) if landing_note && pages.none? { |page| page.route == "/" }
|
|
55
|
+
|
|
56
|
+
artifacts = []
|
|
57
|
+
artifacts << "catalog" if config.features.fetch("previews")
|
|
58
|
+
artifacts << "graph" if config.features.fetch("graph")
|
|
59
|
+
artifacts << "search" if config.features.fetch("search")
|
|
60
|
+
artifacts << "sitemap"
|
|
61
|
+
artifacts << "feed" if config.features.fetch("feed")
|
|
62
|
+
|
|
63
|
+
namespaces = ALWAYS_RESERVED_NAMESPACES.dup
|
|
64
|
+
namespaces << "/feed.xml" if config.features.fetch("feed")
|
|
65
|
+
pages = pages.map { |page| attach_navigation(page, config) }
|
|
66
|
+
|
|
67
|
+
ThemeOutput.new(
|
|
68
|
+
pages: pages,
|
|
69
|
+
artifacts: artifacts,
|
|
70
|
+
shared_files: shared_files,
|
|
71
|
+
site_data: {},
|
|
72
|
+
feed_note_ids: feed_notes.sort_by(&:id).map(&:id),
|
|
73
|
+
reserved_namespaces: namespaces
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def note_page(note, config, theme_data, topic_anchors, topic_summaries, local_graph, home_route, home_url)
|
|
78
|
+
properties = note.properties
|
|
79
|
+
comments = page_comments(note, config)
|
|
80
|
+
website = {
|
|
81
|
+
"kind" => "note",
|
|
82
|
+
"id" => note.id,
|
|
83
|
+
"content_type" => note.content_type,
|
|
84
|
+
"published_at" => note.published_at,
|
|
85
|
+
"route" => note.route,
|
|
86
|
+
"href" => config.url_builder.href(note.route),
|
|
87
|
+
"absolute_url" => config.url_builder.absolute_url(note.route),
|
|
88
|
+
"markdown_url" => config.url_builder.href(PublishedMarkdown.route(note.route)),
|
|
89
|
+
"aliases" => Array(properties["aliases"]),
|
|
90
|
+
"subtitle" => properties["subtitle"],
|
|
91
|
+
"tags" => Array(properties["tags"]),
|
|
92
|
+
"authors" => note.topics.select { |topic| topic.fetch("kind") == "author" },
|
|
93
|
+
"categories" => Array(properties["categories"]),
|
|
94
|
+
"topic_links" => blog_topic_links(note, topic_anchors),
|
|
95
|
+
"cssclasses" => Array(properties["cssclasses"]),
|
|
96
|
+
"created" => note.created,
|
|
97
|
+
"updated" => note.updated,
|
|
98
|
+
"directory_index" => note.directory_index,
|
|
99
|
+
"has_h1" => note.has_h1,
|
|
100
|
+
"source_links" => note.source_links,
|
|
101
|
+
"home_route" => home_route,
|
|
102
|
+
"home_url" => home_url,
|
|
103
|
+
"routes" => { "home" => home_route },
|
|
104
|
+
"theme" => config.theme,
|
|
105
|
+
"features" => config.features.merge(note.feature_flags),
|
|
106
|
+
"content_security" => content_security_data(note.content_security, config),
|
|
107
|
+
"theme_data" => theme_data,
|
|
108
|
+
"tag_links" => Array(note.properties["tags"]).filter_map do |tag|
|
|
109
|
+
anchor = topic_anchors[topic_identity("name" => tag)]
|
|
110
|
+
{ "name" => tag, "anchor" => anchor } if anchor
|
|
111
|
+
end,
|
|
112
|
+
"outline" => note.outline,
|
|
113
|
+
"related_articles" => theme_data.fetch("related_articles", note.related || []),
|
|
114
|
+
"links" => config.features.fetch("relations") ? note.links : [],
|
|
115
|
+
"backlinks" => config.features.fetch("relations") ? note.backlinks : [],
|
|
116
|
+
"embedded_by" => config.features.fetch("relations") ? note.embedded_by : []
|
|
117
|
+
}
|
|
118
|
+
website["local_graph"] = local_graph if local_graph
|
|
119
|
+
website["has_context"] = context_present?(website)
|
|
120
|
+
website["comments"] = comments if comments
|
|
121
|
+
website["analytics"] = analytics_data(config.analytics) unless config.analytics.provider.empty?
|
|
122
|
+
data = {
|
|
123
|
+
"title" => note.title,
|
|
124
|
+
"description" => properties["description"] || note.preview,
|
|
125
|
+
"image" => note.image_url,
|
|
126
|
+
"layout" => "website-#{config.theme}",
|
|
127
|
+
"website" => website
|
|
128
|
+
}.compact
|
|
129
|
+
PageOutput.new(route: note.route, content: note.content, data: data)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def system_page(
|
|
133
|
+
config,
|
|
134
|
+
route,
|
|
135
|
+
title,
|
|
136
|
+
kind,
|
|
137
|
+
system_theme_data,
|
|
138
|
+
theme_data = {},
|
|
139
|
+
home_route: "/",
|
|
140
|
+
home_url: config.url_builder.href(home_route),
|
|
141
|
+
content: "",
|
|
142
|
+
description: config.site.description.to_s,
|
|
143
|
+
website_data: {}
|
|
144
|
+
)
|
|
145
|
+
PageOutput.new(
|
|
146
|
+
route: route,
|
|
147
|
+
content: content,
|
|
148
|
+
data: {
|
|
149
|
+
"layout" => "website-#{config.theme}",
|
|
150
|
+
"title" => title,
|
|
151
|
+
"description" => description,
|
|
152
|
+
"website" => {
|
|
153
|
+
"kind" => kind,
|
|
154
|
+
"theme" => config.theme,
|
|
155
|
+
"features" => config.features,
|
|
156
|
+
"content_security" => content_security_data(nil, config),
|
|
157
|
+
"theme_data" => system_theme_data.merge(theme_data),
|
|
158
|
+
"route" => route,
|
|
159
|
+
"href" => config.url_builder.href(route),
|
|
160
|
+
"absolute_url" => config.url_builder.absolute_url(route),
|
|
161
|
+
"home_route" => home_route,
|
|
162
|
+
"home_url" => home_url,
|
|
163
|
+
"routes" => { "home" => home_route }
|
|
164
|
+
}.merge(website_data).tap do |website|
|
|
165
|
+
website["analytics"] = analytics_data(config.analytics) unless config.analytics.provider.empty?
|
|
166
|
+
end
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def tags_page(tag_groups, config, system_theme_data, home_route, home_url)
|
|
172
|
+
system_page(
|
|
173
|
+
config,
|
|
174
|
+
"/tags/",
|
|
175
|
+
"Tags",
|
|
176
|
+
"tags",
|
|
177
|
+
system_theme_data,
|
|
178
|
+
{ "tag_groups" => tag_groups },
|
|
179
|
+
home_route: home_route,
|
|
180
|
+
home_url: home_url
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def analytics_data(analytics)
|
|
185
|
+
{
|
|
186
|
+
"provider" => analytics.provider,
|
|
187
|
+
"identifier" => analytics.identifier,
|
|
188
|
+
"load" => analytics.load
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def content_security_data(content_security, config)
|
|
193
|
+
media_sources = Array(content_security&.media_sources).dup
|
|
194
|
+
frame_sources = Array(content_security&.frame_sources).dup
|
|
195
|
+
script_sources = Array(content_security&.script_sources).dup
|
|
196
|
+
connect_sources = Array(content_security&.connect_sources).dup
|
|
197
|
+
if config.analytics.load
|
|
198
|
+
case config.analytics.provider
|
|
199
|
+
when "cloudflare"
|
|
200
|
+
script_sources << "https://static.cloudflareinsights.com"
|
|
201
|
+
connect_sources << "https://cloudflareinsights.com"
|
|
202
|
+
when "google"
|
|
203
|
+
script_sources << "https://www.googletagmanager.com"
|
|
204
|
+
connect_sources.concat(%w[
|
|
205
|
+
https://*.analytics.google.com
|
|
206
|
+
https://*.google-analytics.com
|
|
207
|
+
https://www.googletagmanager.com
|
|
208
|
+
])
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
{
|
|
212
|
+
"media_sources" => media_sources.uniq.sort,
|
|
213
|
+
"frame_sources" => frame_sources.uniq.sort,
|
|
214
|
+
"script_sources" => script_sources.uniq.sort,
|
|
215
|
+
"connect_sources" => connect_sources.uniq.sort
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def build_tag_groups(notes, anchors, config)
|
|
220
|
+
groups = Hash.new { |hash, key| hash[key] = [] }
|
|
221
|
+
notes.each { |note| Array(note.properties["tags"]).each { |tag| groups[tag] << note } }
|
|
222
|
+
groups.keys.sort.map do |tag|
|
|
223
|
+
{
|
|
224
|
+
"name" => tag,
|
|
225
|
+
"anchor" => anchors.fetch(topic_identity("name" => tag)),
|
|
226
|
+
"notes" => groups.fetch(tag).sort_by(&:id).map { |note| system_note_card(note, config) }
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def build_topic_groups(notes, anchors, config)
|
|
232
|
+
groups = {}
|
|
233
|
+
notes.each do |note|
|
|
234
|
+
topics_for(note, config).uniq { |topic| topic_identity(topic) }.each do |topic|
|
|
235
|
+
identity = topic_identity(topic)
|
|
236
|
+
groups[identity] ||= { "topic" => topic, "notes" => [] }
|
|
237
|
+
groups.fetch(identity).fetch("notes") << note
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
groups.map do |identity, group|
|
|
242
|
+
topic = group.fetch("topic")
|
|
243
|
+
{
|
|
244
|
+
"name" => topic.fetch("name"),
|
|
245
|
+
"anchor" => anchors.fetch(identity),
|
|
246
|
+
"url" => topic["url"],
|
|
247
|
+
"notes" => group.fetch("notes").sort_by(&:id).map { |note| system_note_card(note, config) }
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def summarize_topics(topic_groups)
|
|
253
|
+
topic_groups.map do |group|
|
|
254
|
+
group.slice("name", "anchor", "url").merge("count" => group.fetch("notes").length).compact
|
|
255
|
+
end.sort_by { |topic| [-topic.fetch("count"), topic.fetch("name").downcase, topic.fetch("name")] }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def taxonomy_for(notes, config)
|
|
259
|
+
return { "anchors" => {}, "groups" => [], "summaries" => [] } unless config.features.fetch("tags")
|
|
260
|
+
|
|
261
|
+
anchors = topic_anchor_map(notes, config)
|
|
262
|
+
groups = build_topic_groups(notes, anchors, config)
|
|
263
|
+
{
|
|
264
|
+
"anchors" => anchors,
|
|
265
|
+
"groups" => build_tag_groups(notes, anchors, config),
|
|
266
|
+
"summaries" => summarize_topics(groups)
|
|
267
|
+
}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def timeline_for(notes)
|
|
271
|
+
month_counts = Hash.new(0)
|
|
272
|
+
notes.each do |note|
|
|
273
|
+
month = month_key(yield(note))
|
|
274
|
+
month_counts[month] += 1 if month
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
month_counts.keys.group_by { |month| month[0, 4] }.sort.reverse.map do |year, months|
|
|
278
|
+
{
|
|
279
|
+
"year" => year,
|
|
280
|
+
"count" => months.sum { |month| month_counts.fetch(month) },
|
|
281
|
+
"months" => months.sort.map do |month|
|
|
282
|
+
{ "key" => month, "label" => month[5, 2], "count" => month_counts.fetch(month) }
|
|
283
|
+
end
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def month_key(value)
|
|
289
|
+
match = value.to_s.match(/\A(\d{4})-(0[1-9]|1[0-2])/)
|
|
290
|
+
match && "#{match[1]}-#{match[2]}"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def redirect_page(landing_note, config, home_route, home_url)
|
|
294
|
+
PageOutput.new(
|
|
295
|
+
route: "/",
|
|
296
|
+
content: "",
|
|
297
|
+
data: {
|
|
298
|
+
"layout" => "website-redirect",
|
|
299
|
+
"title" => landing_note.title,
|
|
300
|
+
"description" => landing_note.preview,
|
|
301
|
+
"website" => {
|
|
302
|
+
"kind" => "redirect",
|
|
303
|
+
"theme" => config.theme,
|
|
304
|
+
"features" => config.features,
|
|
305
|
+
"route" => "/",
|
|
306
|
+
"href" => config.url_builder.href("/"),
|
|
307
|
+
"absolute_url" => config.url_builder.absolute_url("/"),
|
|
308
|
+
"canonical_url" => config.url_builder.absolute_url(landing_note.route),
|
|
309
|
+
"redirect_url" => home_url,
|
|
310
|
+
"home_route" => home_route,
|
|
311
|
+
"home_url" => home_url,
|
|
312
|
+
"routes" => { "home" => home_route },
|
|
313
|
+
"robots" => "noindex"
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def local_graphs(model, config)
|
|
320
|
+
edges_by_note = Hash.new { |hash, id| hash[id] = [] }
|
|
321
|
+
neighbours_by_note = Hash.new { |hash, id| hash[id] = {} }
|
|
322
|
+
model.graph_edges.each do |edge|
|
|
323
|
+
source = edge.fetch("source")
|
|
324
|
+
target = edge.fetch("target")
|
|
325
|
+
edges_by_note[source] << edge
|
|
326
|
+
edges_by_note[target] << edge unless source == target
|
|
327
|
+
neighbours_by_note[source][target] = true
|
|
328
|
+
neighbours_by_note[target][source] = true
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
model.notes.each_with_object({}) do |note, graphs|
|
|
332
|
+
neighbour_ids = neighbours_by_note[note.id].keys.reject { |id| id == note.id }
|
|
333
|
+
next if neighbour_ids.empty?
|
|
334
|
+
|
|
335
|
+
node_ids = [note.id, *neighbour_ids].sort
|
|
336
|
+
nodes = node_ids.map do |id|
|
|
337
|
+
target = model.notes_by_id.fetch(id)
|
|
338
|
+
{
|
|
339
|
+
"id" => id,
|
|
340
|
+
"title" => target.title,
|
|
341
|
+
"url" => config.url_builder.href(target.route),
|
|
342
|
+
"degree" => model.graph_degrees.fetch(id)
|
|
343
|
+
}
|
|
344
|
+
end
|
|
345
|
+
edges = edges_by_note[note.id].sort_by do |edge|
|
|
346
|
+
[edge.fetch("source"), edge.fetch("target"), edge.fetch("kind")]
|
|
347
|
+
end
|
|
348
|
+
graphs[note.id] = { "current_id" => note.id, "nodes" => nodes, "edges" => edges }
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def context_present?(website)
|
|
353
|
+
return true if website["local_graph"]
|
|
354
|
+
return true if website.fetch("features").fetch("outline") && !website.fetch("outline").empty?
|
|
355
|
+
return true unless Array(website["tag_summaries"]).empty?
|
|
356
|
+
|
|
357
|
+
website.fetch("features").fetch("relations") &&
|
|
358
|
+
%w[links backlinks embedded_by].any? { |key| !website.fetch(key).empty? }
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def page_comments(note, config)
|
|
362
|
+
comments = config.comments
|
|
363
|
+
return unless comments.enabled && note.content_type == "post"
|
|
364
|
+
return if note.properties["comments"] == false
|
|
365
|
+
|
|
366
|
+
{
|
|
367
|
+
"configured" => comments.configured,
|
|
368
|
+
"repository" => comments.repository,
|
|
369
|
+
"repository_id" => comments.repository_id,
|
|
370
|
+
"category" => comments.category,
|
|
371
|
+
"category_id" => comments.category_id,
|
|
372
|
+
"term" => "website:post:#{note.id.delete_suffix('.md')}",
|
|
373
|
+
"language" => comments.language,
|
|
374
|
+
"load" => comments.load,
|
|
375
|
+
"repository_url" => "https://github.com/#{comments.repository}",
|
|
376
|
+
"discussion_url" => "https://github.com/#{comments.repository}/discussions"
|
|
377
|
+
}
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def not_found_page(config, system_theme_data, home_route, home_url)
|
|
381
|
+
system_page(
|
|
382
|
+
config,
|
|
383
|
+
"/404.html",
|
|
384
|
+
"Page not found",
|
|
385
|
+
"404",
|
|
386
|
+
system_theme_data,
|
|
387
|
+
{ "home_url" => home_url },
|
|
388
|
+
home_route: home_route,
|
|
389
|
+
home_url: home_url
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def system_note_card(note, config)
|
|
394
|
+
{ "id" => note.id, "title" => note.title, "url" => config.url_builder.href(note.route) }
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def topic_anchors_for(note, taxonomy, config)
|
|
398
|
+
anchors = taxonomy.fetch("anchors")
|
|
399
|
+
topics_for(note, config).filter_map { |topic| anchors[topic_identity(topic)] }.uniq
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def topic_anchor_map(notes, config)
|
|
403
|
+
used = Hash.new(0)
|
|
404
|
+
topics = notes.flat_map { |note| topics_for(note, config) }.uniq { |topic| topic_identity(topic) }
|
|
405
|
+
topics.sort_by { |topic| [topic.fetch("name").downcase, topic.fetch("name"), topic["url"].to_s] }.to_h do |topic|
|
|
406
|
+
base = config.url_builder.slug(topic.fetch("name"))
|
|
407
|
+
used[base] += 1
|
|
408
|
+
[topic_identity(topic), used[base] == 1 ? base : "#{base}-#{used[base]}"]
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def topics_for(note, config)
|
|
413
|
+
topics = note.topics
|
|
414
|
+
if config.theme == "minimal"
|
|
415
|
+
seen_names = {}
|
|
416
|
+
return topics.select do |topic|
|
|
417
|
+
next true unless %w[tag category].include?(topic.fetch("kind"))
|
|
418
|
+
|
|
419
|
+
name = topic_name_identity(topic)
|
|
420
|
+
next false if seen_names[name]
|
|
421
|
+
|
|
422
|
+
seen_names[name] = true
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
topics.select { |topic| topic.fetch("kind") == "tag" }
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def blog_topic_links(note, topic_anchors)
|
|
430
|
+
note.topics
|
|
431
|
+
.select { |topic| %w[tag category].include?(topic.fetch("kind")) }
|
|
432
|
+
.uniq { |topic| topic_name_identity(topic) }
|
|
433
|
+
.filter_map do |topic|
|
|
434
|
+
anchor = topic_anchors[topic_identity(topic)]
|
|
435
|
+
{ "name" => topic.fetch("name"), "anchor" => anchor } if anchor
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def topic_name_identity(topic)
|
|
440
|
+
topic.fetch("name").unicode_normalize(:nfc).downcase(:fold)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def content_card(note, config)
|
|
444
|
+
{
|
|
445
|
+
"id" => note.id,
|
|
446
|
+
"title" => note.title,
|
|
447
|
+
"url" => config.url_builder.href(note.route),
|
|
448
|
+
"published_at" => note.published_at,
|
|
449
|
+
"summary" => note.properties["description"] || note.preview,
|
|
450
|
+
"subtitle" => note.properties["subtitle"],
|
|
451
|
+
"image" => note.image_url,
|
|
452
|
+
"authors" => note.topics.select { |topic| topic.fetch("kind") == "author" },
|
|
453
|
+
"tags" => Array(note.properties["tags"])
|
|
454
|
+
}
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def tab_member_card(note, config)
|
|
458
|
+
topics = note.topics
|
|
459
|
+
.select { |topic| %w[tag category].include?(topic.fetch("kind")) }
|
|
460
|
+
.uniq { |topic| topic_name_identity(topic) }
|
|
461
|
+
.map { |topic| topic.slice("name", "url") }
|
|
462
|
+
content_card(note, config).merge(
|
|
463
|
+
"content_type" => note.content_type,
|
|
464
|
+
"topics" => topics
|
|
465
|
+
)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def attach_custom_tab_theme_data(theme_data, model, config)
|
|
469
|
+
config.navigation.custom_tabs.each do |tab|
|
|
470
|
+
cards = tab.member_note_ids.filter_map do |id|
|
|
471
|
+
note = model.notes_by_id[id]
|
|
472
|
+
tab_member_card(note, config) if note
|
|
473
|
+
end
|
|
474
|
+
theme_data[tab.index_note_id] = theme_data.fetch(tab.index_note_id).merge(
|
|
475
|
+
"tab_id" => tab.id,
|
|
476
|
+
"tab_member_ids" => tab.member_note_ids,
|
|
477
|
+
"tab_members" => cards
|
|
478
|
+
)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def related_article_cards(note, model, config)
|
|
483
|
+
Array(note.related).map do |related|
|
|
484
|
+
target = model.notes_by_id.fetch(related.fetch("id"))
|
|
485
|
+
content_card(target, config).merge(
|
|
486
|
+
"title" => related.fetch("title"),
|
|
487
|
+
"url" => related.fetch("url")
|
|
488
|
+
)
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def topic_identity(topic)
|
|
493
|
+
[topic.fetch("name"), topic["url"].to_s]
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def h(value)
|
|
497
|
+
text = value.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\uFFFD")
|
|
498
|
+
CGI.escapeHTML(text.gsub(FrontMatter::XML_INVALID_CHARACTER, "\uFFFD"))
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def attach_navigation(page, config)
|
|
502
|
+
navigation = config.navigation
|
|
503
|
+
website = page.data.fetch("website").dup
|
|
504
|
+
active_id = if website["kind"] == "note"
|
|
505
|
+
navigation.active_by_note_id[website["id"]]
|
|
506
|
+
else
|
|
507
|
+
navigation.items.find do |item|
|
|
508
|
+
item.fetch("active_scope").fetch("routes").include?(page.route)
|
|
509
|
+
end&.fetch("id")
|
|
510
|
+
end
|
|
511
|
+
website["navigation"] = navigation.items.map do |item|
|
|
512
|
+
item.slice("id", "label", "url", "order")
|
|
513
|
+
end
|
|
514
|
+
website["active_navigation_id"] = active_id
|
|
515
|
+
routes = navigation.items.to_h { |item| [item.fetch("id"), item.fetch("url")] }
|
|
516
|
+
routes["home"] ||= website.fetch("home_url")
|
|
517
|
+
routes["portfolio"] = config.url_builder.href(navigation.portfolio.route) if navigation.portfolio
|
|
518
|
+
routes["tags"] = config.url_builder.href("/tags/") if config.theme == "docs" && config.features.fetch("tags")
|
|
519
|
+
routes["feed"] = config.url_builder.href("/feed.xml") if config.features.fetch("feed")
|
|
520
|
+
website["routes"] = routes
|
|
521
|
+
data = page.data.merge("website" => website)
|
|
522
|
+
PageOutput.new(route: page.route, content: page.content, data: data)
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
class Minimal < Presenter
|
|
527
|
+
def render(model:, config:)
|
|
528
|
+
posts = ordered_posts(model)
|
|
529
|
+
displayed = displayed_posts(posts)
|
|
530
|
+
taxonomy = taxonomy_for(posts, config)
|
|
531
|
+
timeline = timeline_for(displayed, &:published_at)
|
|
532
|
+
documentation = config.navigation
|
|
533
|
+
portfolio = documentation.portfolio
|
|
534
|
+
portfolio_notes = portfolio ? portfolio.project_note_ids.map { |id| model.notes_by_id.fetch(id) } : []
|
|
535
|
+
portfolio_taxonomy = taxonomy_for(portfolio_notes, config)
|
|
536
|
+
portfolio_projects = portfolio_project_cards(portfolio_notes, config, portfolio_taxonomy)
|
|
537
|
+
portfolio_theme_data = {
|
|
538
|
+
"portfolio_projects" => portfolio_projects,
|
|
539
|
+
"portfolio_topic_summaries" => portfolio_taxonomy.fetch("summaries"),
|
|
540
|
+
"portfolio_topic_filter_count" => portfolio_projects.length
|
|
541
|
+
}
|
|
542
|
+
portfolio_projects_by_id = portfolio_projects.to_h { |project| [project.fetch("id"), project] }
|
|
543
|
+
linked_docs = documentation.docs_note_ids.filter_map { |id| model.notes_by_id[id] }
|
|
544
|
+
linked_doc_positions = linked_docs.each_with_index.to_h { |note, index| [note.id, index] }
|
|
545
|
+
post_positions = posts.each_with_index.to_h { |post, index| [post.id, index] }
|
|
546
|
+
theme_data = model.notes.to_h do |note|
|
|
547
|
+
post_index = post_positions[note.id]
|
|
548
|
+
doc_index = linked_doc_positions[note.id]
|
|
549
|
+
project = portfolio_projects_by_id[note.id]
|
|
550
|
+
data = {
|
|
551
|
+
"archive_groups" => [],
|
|
552
|
+
"docs_tree" => note.content_type == "doc" ? documentation.docs_tree : [],
|
|
553
|
+
"docs_home_url" => documentation.docs_home_url,
|
|
554
|
+
"related_articles" => related_article_cards(note, model, config),
|
|
555
|
+
"previous" => sequence_card(
|
|
556
|
+
note,
|
|
557
|
+
post_index,
|
|
558
|
+
doc_index,
|
|
559
|
+
posts,
|
|
560
|
+
linked_docs,
|
|
561
|
+
-1,
|
|
562
|
+
config,
|
|
563
|
+
taxonomy
|
|
564
|
+
),
|
|
565
|
+
"next" => sequence_card(
|
|
566
|
+
note,
|
|
567
|
+
post_index,
|
|
568
|
+
doc_index,
|
|
569
|
+
posts,
|
|
570
|
+
linked_docs,
|
|
571
|
+
1,
|
|
572
|
+
config,
|
|
573
|
+
taxonomy
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
data["portfolio_topics"] = project.fetch("topics") if project
|
|
577
|
+
[
|
|
578
|
+
note.id,
|
|
579
|
+
data
|
|
580
|
+
]
|
|
581
|
+
end
|
|
582
|
+
system_theme_data = {
|
|
583
|
+
"archive_groups" => [],
|
|
584
|
+
"docs_tree" => documentation.docs_tree,
|
|
585
|
+
"docs_home_url" => documentation.docs_home_url,
|
|
586
|
+
"previous" => nil,
|
|
587
|
+
"next" => nil
|
|
588
|
+
}
|
|
589
|
+
root_path = model.directory_index_paths&.fetch(".", nil)
|
|
590
|
+
root = root_path && model.notes_by_id[root_path]
|
|
591
|
+
home_modules = home_theme_data(displayed, config, taxonomy)
|
|
592
|
+
theme_data[root.id] = theme_data.fetch(root.id).merge(home_modules) if root
|
|
593
|
+
if portfolio&.index_note_id
|
|
594
|
+
theme_data[portfolio.index_note_id] = theme_data.fetch(portfolio.index_note_id).merge(
|
|
595
|
+
portfolio_theme_data
|
|
596
|
+
)
|
|
597
|
+
end
|
|
598
|
+
attach_custom_tab_theme_data(theme_data, model, config)
|
|
599
|
+
authored_root_route = model.notes.any? { |note| note.route == "/" }
|
|
600
|
+
home = home_page(displayed, config, system_theme_data, taxonomy) if !root && !authored_root_route && displayed.any?
|
|
601
|
+
groups = archive_groups(displayed, config, taxonomy)
|
|
602
|
+
blog = if displayed.any?
|
|
603
|
+
system_page(
|
|
604
|
+
config,
|
|
605
|
+
"/blog/",
|
|
606
|
+
navigation_label(config, "blog", "Blog"),
|
|
607
|
+
"blog-index",
|
|
608
|
+
system_theme_data,
|
|
609
|
+
{
|
|
610
|
+
"archive_groups" => groups,
|
|
611
|
+
"topic_summaries" => taxonomy.fetch("summaries"),
|
|
612
|
+
"topic_filter_count" => displayed.length,
|
|
613
|
+
"timeline" => timeline
|
|
614
|
+
},
|
|
615
|
+
home_route: "/",
|
|
616
|
+
home_url: config.url_builder.href("/")
|
|
617
|
+
)
|
|
618
|
+
end
|
|
619
|
+
portfolio_page = if portfolio && !portfolio.index_note_id
|
|
620
|
+
system_page(
|
|
621
|
+
config,
|
|
622
|
+
portfolio.route,
|
|
623
|
+
navigation_label(config, "portfolio", "Portfolio"),
|
|
624
|
+
"portfolio-index",
|
|
625
|
+
system_theme_data,
|
|
626
|
+
portfolio_theme_data,
|
|
627
|
+
home_route: "/",
|
|
628
|
+
home_url: config.url_builder.href("/")
|
|
629
|
+
)
|
|
630
|
+
end
|
|
631
|
+
theme_pages = [home, blog, portfolio_page].compact
|
|
632
|
+
root_route_claimed = authored_root_route || theme_pages.any? { |page| page.route == "/" }
|
|
633
|
+
unless root || home || root_route_claimed
|
|
634
|
+
destination = config.navigation.items.first
|
|
635
|
+
if !destination && portfolio
|
|
636
|
+
destination = {
|
|
637
|
+
"id" => "portfolio",
|
|
638
|
+
"label" => navigation_label(config, "portfolio", "Portfolio"),
|
|
639
|
+
"url" => config.url_builder.href(portfolio.route)
|
|
640
|
+
}
|
|
641
|
+
end
|
|
642
|
+
theme_pages << redirect_to(destination, config) if destination
|
|
643
|
+
end
|
|
644
|
+
project(
|
|
645
|
+
model: model,
|
|
646
|
+
config: config,
|
|
647
|
+
landing_note: nil,
|
|
648
|
+
note_theme_data: theme_data,
|
|
649
|
+
theme_pages: theme_pages,
|
|
650
|
+
system_theme_data: system_theme_data,
|
|
651
|
+
taxonomy: taxonomy,
|
|
652
|
+
feed_notes: posts
|
|
653
|
+
) do |note, output|
|
|
654
|
+
note.id == root&.id ? authored_home_page(output) : output
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
private
|
|
659
|
+
|
|
660
|
+
def ordered_posts(model)
|
|
661
|
+
dated, undated = model.notes.select { |note| note.content_type == "post" }.partition(&:published_at)
|
|
662
|
+
dated.sort_by! { |note| [chronology_key(note.published_at), note.id] }
|
|
663
|
+
undated.sort_by!(&:id)
|
|
664
|
+
dated + undated
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def displayed_posts(posts)
|
|
668
|
+
dated, undated = posts.partition(&:published_at)
|
|
669
|
+
displayed = dated.reverse + undated
|
|
670
|
+
pinned, remaining = displayed.partition { |note| note.properties["pinned"] == true }
|
|
671
|
+
pinned + remaining
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def archive_groups(posts, config, taxonomy)
|
|
675
|
+
posts.group_by { |note| note.published_at ? note.published_at[0, 4] : "Undated" }.map do |label, grouped|
|
|
676
|
+
{ "label" => label, "posts" => grouped.map { |note| note_card(note, config, taxonomy) } }
|
|
677
|
+
end
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def home_theme_data(posts, config, taxonomy)
|
|
681
|
+
{
|
|
682
|
+
"recent_posts" => posts.first(6).map { |post| note_card(post, config, taxonomy) },
|
|
683
|
+
"view_all_url" => config.url_builder.href("/blog/"),
|
|
684
|
+
"contacts" => config.contacts,
|
|
685
|
+
"topic_summaries" => taxonomy.fetch("summaries")
|
|
686
|
+
}
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def home_page(posts, config, system_theme_data, taxonomy)
|
|
690
|
+
system_page(
|
|
691
|
+
config,
|
|
692
|
+
"/",
|
|
693
|
+
navigation_label(config, "home", "Home"),
|
|
694
|
+
"home",
|
|
695
|
+
system_theme_data,
|
|
696
|
+
home_theme_data(posts, config, taxonomy),
|
|
697
|
+
home_route: "/",
|
|
698
|
+
home_url: config.url_builder.href("/")
|
|
699
|
+
)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def authored_home_page(page)
|
|
703
|
+
website = page.data.fetch("website").merge("kind" => "home")
|
|
704
|
+
PageOutput.new(route: "/", content: page.content, data: page.data.merge("website" => website))
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def redirect_to(item, config)
|
|
708
|
+
url = item.fetch("url")
|
|
709
|
+
PageOutput.new(
|
|
710
|
+
route: "/",
|
|
711
|
+
content: "",
|
|
712
|
+
data: {
|
|
713
|
+
"layout" => "website-redirect",
|
|
714
|
+
"title" => item.fetch("label"),
|
|
715
|
+
"description" => config.site.description.to_s,
|
|
716
|
+
"website" => {
|
|
717
|
+
"kind" => "redirect",
|
|
718
|
+
"redirect_navigation_id" => item.fetch("id"),
|
|
719
|
+
"theme" => config.theme,
|
|
720
|
+
"features" => config.features,
|
|
721
|
+
"route" => "/",
|
|
722
|
+
"href" => config.url_builder.href("/"),
|
|
723
|
+
"absolute_url" => config.url_builder.absolute_url("/"),
|
|
724
|
+
"canonical_url" => config.url_builder.origin.empty? ? nil : "#{config.url_builder.origin}#{url}",
|
|
725
|
+
"redirect_url" => url,
|
|
726
|
+
"home_route" => "/",
|
|
727
|
+
"home_url" => config.url_builder.href("/"),
|
|
728
|
+
"routes" => { "home" => "/" },
|
|
729
|
+
"robots" => "noindex"
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def sequence_card(note, post_index, doc_index, posts, docs, offset, config, taxonomy)
|
|
736
|
+
collection, index = note.content_type == "post" ? [posts, post_index] : [docs, doc_index]
|
|
737
|
+
return unless index
|
|
738
|
+
|
|
739
|
+
target_index = index + offset
|
|
740
|
+
return if target_index.negative? || target_index >= collection.length
|
|
741
|
+
|
|
742
|
+
target = collection.fetch(target_index)
|
|
743
|
+
note.content_type == "post" ? note_card(target, config, taxonomy) : docs_card(target, config)
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def docs_card(note, config)
|
|
747
|
+
{
|
|
748
|
+
"id" => note.id,
|
|
749
|
+
"title" => note.title,
|
|
750
|
+
"url" => !note.nav_exclude ? config.url_builder.href(note.route) : nil,
|
|
751
|
+
"content_type" => note.content_type
|
|
752
|
+
}
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def navigation_label(config, id, fallback)
|
|
756
|
+
config.navigation.items.find { |item| item.fetch("id") == id }&.fetch("label") ||
|
|
757
|
+
config.site.navigation&.dig(id, "label") || fallback
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def portfolio_project_cards(notes, config, taxonomy)
|
|
761
|
+
notes.map do |note|
|
|
762
|
+
topics = portfolio_topics_for(note, config, taxonomy)
|
|
763
|
+
card = {
|
|
764
|
+
"id" => note.id,
|
|
765
|
+
"title" => note.title,
|
|
766
|
+
"url" => config.url_builder.href(note.route),
|
|
767
|
+
"image" => note.image_url,
|
|
768
|
+
"summary" => note.properties["description"] || note.preview,
|
|
769
|
+
"topics" => topics,
|
|
770
|
+
"topic_anchors" => topics.map { |topic| topic.fetch("anchor") }
|
|
771
|
+
}
|
|
772
|
+
card["repository_url"] = note.source_links.fetch("repository") if note.source_links["repository"]
|
|
773
|
+
card
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
def portfolio_topics_for(note, config, taxonomy)
|
|
778
|
+
anchors = taxonomy.fetch("anchors")
|
|
779
|
+
topics_for(note, config).uniq { |topic| topic_identity(topic) }.filter_map do |topic|
|
|
780
|
+
anchor = anchors[topic_identity(topic)]
|
|
781
|
+
next unless anchor
|
|
782
|
+
|
|
783
|
+
topic.slice("name", "url").merge("anchor" => anchor).compact
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def chronology_key(value)
|
|
788
|
+
[0, DateTime.iso8601(value.to_s).new_offset(0).ajd]
|
|
789
|
+
rescue Date::Error
|
|
790
|
+
[1, value.to_s]
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def note_card(note, config, taxonomy)
|
|
794
|
+
content_card(note, config).merge(
|
|
795
|
+
"topics" => blog_topic_links(note, taxonomy.fetch("anchors")),
|
|
796
|
+
"topic_anchors" => topic_anchors_for(note, taxonomy, config),
|
|
797
|
+
"filter_month" => month_key(note.published_at)
|
|
798
|
+
)
|
|
799
|
+
end
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
class Docs < Presenter
|
|
803
|
+
def render(model:, config:)
|
|
804
|
+
taxonomy = taxonomy_for(model.notes, config)
|
|
805
|
+
navigation = config.navigation
|
|
806
|
+
docs_home_url = navigation.docs_home_url
|
|
807
|
+
linked = navigation.docs_note_ids.filter_map { |id| model.notes_by_id[id] }
|
|
808
|
+
root_path = model.directory_index_paths&.fetch(".", nil)
|
|
809
|
+
landing = (root_path && model.notes_by_id[root_path]) || linked.first || model.notes.first
|
|
810
|
+
linked_positions = linked.each_with_index.to_h { |linked_note, index| [linked_note.id, index] }
|
|
811
|
+
theme_data = model.notes.to_h do |note|
|
|
812
|
+
index = linked_positions[note.id]
|
|
813
|
+
[
|
|
814
|
+
note.id,
|
|
815
|
+
{
|
|
816
|
+
"docs_tree" => navigation.docs_tree,
|
|
817
|
+
"docs_home_url" => docs_home_url,
|
|
818
|
+
"related_articles" => related_article_cards(note, model, config),
|
|
819
|
+
"previous" => index && index.positive? ? docs_card(linked[index - 1], config) : nil,
|
|
820
|
+
"next" => index && index < linked.length - 1 ? docs_card(linked[index + 1], config) : nil
|
|
821
|
+
}
|
|
822
|
+
]
|
|
823
|
+
end
|
|
824
|
+
attach_custom_tab_theme_data(theme_data, model, config)
|
|
825
|
+
system_theme_data = {
|
|
826
|
+
"docs_tree" => navigation.docs_tree,
|
|
827
|
+
"docs_home_url" => docs_home_url,
|
|
828
|
+
"previous" => nil,
|
|
829
|
+
"next" => nil
|
|
830
|
+
}
|
|
831
|
+
project(
|
|
832
|
+
model: model,
|
|
833
|
+
config: config,
|
|
834
|
+
landing_note: landing,
|
|
835
|
+
note_theme_data: theme_data,
|
|
836
|
+
theme_pages: [],
|
|
837
|
+
system_theme_data: system_theme_data,
|
|
838
|
+
taxonomy: taxonomy,
|
|
839
|
+
feed_notes: model.notes
|
|
840
|
+
)
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
private
|
|
844
|
+
|
|
845
|
+
def docs_card(note, config, linked: true)
|
|
846
|
+
{
|
|
847
|
+
"id" => note.id,
|
|
848
|
+
"title" => note.title,
|
|
849
|
+
"url" => linked && !note.nav_exclude ? config.url_builder.href(note.route) : nil,
|
|
850
|
+
"content_type" => note.content_type
|
|
851
|
+
}
|
|
852
|
+
end
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
end
|