feat: unify layout slot architecture - make header/footer configurable positions, footer as array, add defaultPosition fallback, add tests, update docs
This commit is contained in:
@@ -0,0 +1,955 @@
|
|||||||
|
|
||||||
|
> @jackyzha0/quartz@5.0.0 test /home/saberzero1/Repos/dev-mode/repos/quartz
|
||||||
|
> tsx --test
|
||||||
|
|
||||||
|
TAP version 13
|
||||||
|
# Subtest: symlinkOrCopySync
|
||||||
|
# Subtest: creates a symlink on success
|
||||||
|
ok 1 - creates a symlink on success
|
||||||
|
---
|
||||||
|
duration_ms: 1.794289
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: silently succeeds when link already exists (EEXIST)
|
||||||
|
ok 2 - silently succeeds when link already exists (EEXIST)
|
||||||
|
---
|
||||||
|
duration_ms: 0.881952
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: re-throws non-EPERM errors
|
||||||
|
ok 3 - re-throws non-EPERM errors
|
||||||
|
---
|
||||||
|
duration_ms: 9.41571
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: falls back to junction on Windows EPERM
|
||||||
|
ok 4 - falls back to junction on Windows EPERM
|
||||||
|
---
|
||||||
|
duration_ms: 1.366338
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: falls back to copy when both symlink and junction fail on Windows
|
||||||
|
ok 5 - falls back to copy when both symlink and junction fail on Windows
|
||||||
|
---
|
||||||
|
duration_ms: 1.424093
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: does not fall back on EPERM when not on Windows
|
||||||
|
ok 6 - does not fall back on EPERM when not on Windows
|
||||||
|
---
|
||||||
|
duration_ms: 0.661469
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..6
|
||||||
|
ok 1 - symlinkOrCopySync
|
||||||
|
---
|
||||||
|
duration_ms: 16.577591
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: symlinkOrCopy
|
||||||
|
# Subtest: creates a symlink on success
|
||||||
|
ok 1 - creates a symlink on success
|
||||||
|
---
|
||||||
|
duration_ms: 6.646098
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: silently succeeds when link already exists (EEXIST)
|
||||||
|
ok 2 - silently succeeds when link already exists (EEXIST)
|
||||||
|
---
|
||||||
|
duration_ms: 3.596443
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: falls back to copy when both symlink and junction fail on Windows
|
||||||
|
ok 3 - falls back to copy when both symlink and junction fail on Windows
|
||||||
|
---
|
||||||
|
duration_ms: 2.929394
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: does not fall back on EPERM when not on Windows
|
||||||
|
ok 4 - does not fall back on EPERM when not on Windows
|
||||||
|
---
|
||||||
|
duration_ms: 0.814288
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..4
|
||||||
|
ok 2 - symlinkOrCopy
|
||||||
|
---
|
||||||
|
duration_ms: 14.388319
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Warning: Skipping circular transclusion: pageA -> pageA
|
||||||
|
# Warning: Skipping circular transclusion: self -> self
|
||||||
|
# Subtest: renderTranscludes
|
||||||
|
# Subtest: resolves a single page transclusion
|
||||||
|
ok 1 - resolves a single page transclusion
|
||||||
|
---
|
||||||
|
duration_ms: 1.26955
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: allows the same page to be embedded twice as siblings
|
||||||
|
ok 2 - allows the same page to be embedded twice as siblings
|
||||||
|
---
|
||||||
|
duration_ms: 0.22418
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: allows different sections of the same page to be embedded
|
||||||
|
ok 3 - allows different sections of the same page to be embedded
|
||||||
|
---
|
||||||
|
duration_ms: 0.37216
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: detects actual circular transclusion (A -> B -> A)
|
||||||
|
ok 4 - detects actual circular transclusion (A -> B -> A)
|
||||||
|
---
|
||||||
|
duration_ms: 0.965395
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: self-referencing transclusion is blocked
|
||||||
|
ok 5 - self-referencing transclusion is blocked
|
||||||
|
---
|
||||||
|
duration_ms: 0.169319
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..5
|
||||||
|
ok 3 - renderTranscludes
|
||||||
|
---
|
||||||
|
duration_ms: 3.861318
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: pageResources
|
||||||
|
# Subtest: uses baseDir prefix for resource paths in production mode
|
||||||
|
ok 1 - uses baseDir prefix for resource paths in production mode
|
||||||
|
---
|
||||||
|
duration_ms: 0.415289
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: omits subpath prefix when baseDir is empty (serve mode)
|
||||||
|
ok 2 - omits subpath prefix when baseDir is empty (serve mode)
|
||||||
|
---
|
||||||
|
duration_ms: 0.272739
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: contentIndex path reflects baseDir
|
||||||
|
ok 3 - contentIndex path reflects baseDir
|
||||||
|
---
|
||||||
|
duration_ms: 0.296542
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..3
|
||||||
|
ok 4 - pageResources
|
||||||
|
---
|
||||||
|
duration_ms: 1.250977
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: showPopover on cache-hit with hash
|
||||||
|
# Subtest: does not reference any lexical popoverInner from an outer scope
|
||||||
|
ok 1 - does not reference any lexical popoverInner from an outer scope
|
||||||
|
---
|
||||||
|
duration_ms: 1.304254
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: skips scroll when hash is empty
|
||||||
|
ok 2 - skips scroll when hash is empty
|
||||||
|
---
|
||||||
|
duration_ms: 0.276135
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: skips scroll when heading is not found
|
||||||
|
ok 3 - skips scroll when heading is not found
|
||||||
|
---
|
||||||
|
duration_ms: 0.200547
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: decodes percent-encoded fragments when building the selector
|
||||||
|
ok 4 - decodes percent-encoded fragments when building the selector
|
||||||
|
---
|
||||||
|
duration_ms: 0.281885
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..4
|
||||||
|
ok 5 - showPopover on cache-hit with hash
|
||||||
|
---
|
||||||
|
duration_ms: 3.103934
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: buggy showPopover (lexical-capture pattern) regression guard
|
||||||
|
# Subtest: accessing a capture-before-declaration variable throws ReferenceError (TDZ simulation)
|
||||||
|
ok 1 - accessing a capture-before-declaration variable throws ReferenceError (TDZ simulation)
|
||||||
|
---
|
||||||
|
duration_ms: 0.569761
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: same pattern does NOT throw when hash is empty (explains why first link without fragment works)
|
||||||
|
ok 2 - same pattern does NOT throw when hash is empty (explains why first link without fragment works)
|
||||||
|
---
|
||||||
|
duration_ms: 0.276566
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..2
|
||||||
|
ok 6 - buggy showPopover (lexical-capture pattern) regression guard
|
||||||
|
---
|
||||||
|
duration_ms: 1.093709
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: search encoder
|
||||||
|
# Subtest: English text
|
||||||
|
# Subtest: should tokenize simple English words
|
||||||
|
ok 1 - should tokenize simple English words
|
||||||
|
---
|
||||||
|
duration_ms: 1.127912
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle multiple spaces
|
||||||
|
ok 2 - should handle multiple spaces
|
||||||
|
---
|
||||||
|
duration_ms: 0.411602
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle tabs and newlines
|
||||||
|
ok 3 - should handle tabs and newlines
|
||||||
|
---
|
||||||
|
duration_ms: 0.155003
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should lowercase all text
|
||||||
|
ok 4 - should lowercase all text
|
||||||
|
---
|
||||||
|
duration_ms: 0.091617
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..4
|
||||||
|
ok 1 - English text
|
||||||
|
---
|
||||||
|
duration_ms: 2.536476
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: CJK text
|
||||||
|
# Subtest: should tokenize Japanese Hiragana character by character
|
||||||
|
ok 1 - should tokenize Japanese Hiragana character by character
|
||||||
|
---
|
||||||
|
duration_ms: 0.233617
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should tokenize Japanese Katakana character by character
|
||||||
|
ok 2 - should tokenize Japanese Katakana character by character
|
||||||
|
---
|
||||||
|
duration_ms: 0.247863
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should tokenize Japanese Kanji character by character
|
||||||
|
ok 3 - should tokenize Japanese Kanji character by character
|
||||||
|
---
|
||||||
|
duration_ms: 0.130799
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should tokenize Korean Hangul character by character
|
||||||
|
ok 4 - should tokenize Korean Hangul character by character
|
||||||
|
---
|
||||||
|
duration_ms: 0.321528
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should tokenize Chinese characters character by character
|
||||||
|
ok 5 - should tokenize Chinese characters character by character
|
||||||
|
---
|
||||||
|
duration_ms: 0.207129
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle mixed Hiragana/Katakana/Kanji
|
||||||
|
ok 6 - should handle mixed Hiragana/Katakana/Kanji
|
||||||
|
---
|
||||||
|
duration_ms: 0.194125
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..6
|
||||||
|
ok 2 - CJK text
|
||||||
|
---
|
||||||
|
duration_ms: 1.739019
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: Mixed CJK and English
|
||||||
|
# Subtest: should handle Japanese with English words
|
||||||
|
ok 1 - should handle Japanese with English words
|
||||||
|
---
|
||||||
|
duration_ms: 0.152819
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle English with Japanese words
|
||||||
|
ok 2 - should handle English with Japanese words
|
||||||
|
---
|
||||||
|
duration_ms: 0.081549
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle complex mixed content
|
||||||
|
ok 3 - should handle complex mixed content
|
||||||
|
---
|
||||||
|
duration_ms: 0.084444
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle mixed Korean and English
|
||||||
|
ok 4 - should handle mixed Korean and English
|
||||||
|
---
|
||||||
|
duration_ms: 0.096957
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle mixed Chinese and English
|
||||||
|
ok 5 - should handle mixed Chinese and English
|
||||||
|
---
|
||||||
|
duration_ms: 0.072713
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..5
|
||||||
|
ok 3 - Mixed CJK and English
|
||||||
|
---
|
||||||
|
duration_ms: 0.613962
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: Edge cases
|
||||||
|
# Subtest: should handle empty string
|
||||||
|
ok 1 - should handle empty string
|
||||||
|
---
|
||||||
|
duration_ms: 0.191751
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle only whitespace
|
||||||
|
ok 2 - should handle only whitespace
|
||||||
|
---
|
||||||
|
duration_ms: 0.091076
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle single character
|
||||||
|
ok 3 - should handle single character
|
||||||
|
---
|
||||||
|
duration_ms: 0.073254
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle single CJK character
|
||||||
|
ok 4 - should handle single CJK character
|
||||||
|
---
|
||||||
|
duration_ms: 0.084565
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle CJK with trailing whitespace
|
||||||
|
ok 5 - should handle CJK with trailing whitespace
|
||||||
|
---
|
||||||
|
duration_ms: 0.089614
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle English with trailing whitespace
|
||||||
|
ok 6 - should handle English with trailing whitespace
|
||||||
|
---
|
||||||
|
duration_ms: 0.079265
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..6
|
||||||
|
ok 4 - Edge cases
|
||||||
|
---
|
||||||
|
duration_ms: 0.83129
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
1..4
|
||||||
|
ok 7 - search encoder
|
||||||
|
---
|
||||||
|
duration_ms: 6.293504
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: resolveLayout
|
||||||
|
# Subtest: footer defaults to [] when sharedDefaults omits footer
|
||||||
|
ok 1 - footer defaults to [] when sharedDefaults omits footer
|
||||||
|
---
|
||||||
|
duration_ms: 0.775438
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: header defaults to [] when sharedDefaults omits header
|
||||||
|
ok 2 - header defaults to [] when sharedDefaults omits header
|
||||||
|
---
|
||||||
|
duration_ms: 0.088121
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: footer from sharedDefaults is used when no override
|
||||||
|
ok 3 - footer from sharedDefaults is used when no override
|
||||||
|
---
|
||||||
|
duration_ms: 0.137291
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: byPageType override replaces footer
|
||||||
|
ok 4 - byPageType override replaces footer
|
||||||
|
---
|
||||||
|
duration_ms: 0.135939
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: byPageType override clears footer with []
|
||||||
|
ok 5 - byPageType override clears footer with []
|
||||||
|
---
|
||||||
|
duration_ms: 0.078884
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: byPageType override clears header with []
|
||||||
|
ok 6 - byPageType override clears header with []
|
||||||
|
---
|
||||||
|
duration_ms: 0.081689
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..6
|
||||||
|
ok 8 - resolveLayout
|
||||||
|
---
|
||||||
|
duration_ms: 1.943983
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: resolveLayout frame resolution
|
||||||
|
# Subtest: config override frame wins over page type frame
|
||||||
|
ok 1 - config override frame wins over page type frame
|
||||||
|
---
|
||||||
|
duration_ms: 0.33387
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: page type frame wins when no config override
|
||||||
|
ok 2 - page type frame wins when no config override
|
||||||
|
---
|
||||||
|
duration_ms: 0.19159
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: defaults to 'default' when no frame specified
|
||||||
|
ok 3 - defaults to 'default' when no frame specified
|
||||||
|
---
|
||||||
|
duration_ms: 0.216105
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..3
|
||||||
|
ok 9 - resolveLayout frame resolution
|
||||||
|
---
|
||||||
|
duration_ms: 1.03427
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: FileTrie
|
||||||
|
# Subtest: constructor
|
||||||
|
# Subtest: should create an empty trie
|
||||||
|
ok 1 - should create an empty trie
|
||||||
|
---
|
||||||
|
duration_ms: 1.219169
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should set displayName from data title
|
||||||
|
ok 2 - should set displayName from data title
|
||||||
|
---
|
||||||
|
duration_ms: 0.34463
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should be able to set displayName
|
||||||
|
ok 3 - should be able to set displayName
|
||||||
|
---
|
||||||
|
duration_ms: 0.2462
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..3
|
||||||
|
ok 1 - constructor
|
||||||
|
---
|
||||||
|
duration_ms: 2.714932
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: add
|
||||||
|
# Subtest: should add a file at root level
|
||||||
|
ok 1 - should add a file at root level
|
||||||
|
---
|
||||||
|
duration_ms: 0.437219
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should handle index files
|
||||||
|
ok 2 - should handle index files
|
||||||
|
---
|
||||||
|
duration_ms: 0.315296
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should add nested files
|
||||||
|
ok 3 - should add nested files
|
||||||
|
---
|
||||||
|
duration_ms: 0.297815
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: last-insert-wins on folder-note collision (matches emitter semantics)
|
||||||
|
ok 4 - last-insert-wins on folder-note collision (matches emitter semantics)
|
||||||
|
---
|
||||||
|
duration_ms: 0.238426
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: last-insert-wins on root-level index collision
|
||||||
|
ok 5 - last-insert-wins on root-level index collision
|
||||||
|
---
|
||||||
|
duration_ms: 0.255106
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: collision does not affect sibling files in the same folder
|
||||||
|
ok 6 - collision does not affect sibling files in the same folder
|
||||||
|
---
|
||||||
|
duration_ms: 0.337747
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..6
|
||||||
|
ok 2 - add
|
||||||
|
---
|
||||||
|
duration_ms: 2.485002
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: filter
|
||||||
|
# Subtest: should filter nodes based on condition
|
||||||
|
ok 1 - should filter nodes based on condition
|
||||||
|
---
|
||||||
|
duration_ms: 0.313664
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..1
|
||||||
|
ok 3 - filter
|
||||||
|
---
|
||||||
|
duration_ms: 0.406112
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: map
|
||||||
|
# Subtest: should apply function to all nodes
|
||||||
|
ok 1 - should apply function to all nodes
|
||||||
|
---
|
||||||
|
duration_ms: 0.242464
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: map over folders should work
|
||||||
|
ok 2 - map over folders should work
|
||||||
|
---
|
||||||
|
duration_ms: 0.215254
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..2
|
||||||
|
ok 4 - map
|
||||||
|
---
|
||||||
|
duration_ms: 0.563751
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: entries
|
||||||
|
# Subtest: should return all entries
|
||||||
|
ok 1 - should return all entries
|
||||||
|
---
|
||||||
|
duration_ms: 0.53589
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..1
|
||||||
|
ok 5 - entries
|
||||||
|
---
|
||||||
|
duration_ms: 0.618851
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: fromEntries
|
||||||
|
# Subtest: nested
|
||||||
|
ok 1 - nested
|
||||||
|
---
|
||||||
|
duration_ms: 0.259985
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..1
|
||||||
|
ok 6 - fromEntries
|
||||||
|
---
|
||||||
|
duration_ms: 0.331937
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: findNode
|
||||||
|
# Subtest: should find root node with empty path
|
||||||
|
ok 1 - should find root node with empty path
|
||||||
|
---
|
||||||
|
duration_ms: 0.249265
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should find node at first level
|
||||||
|
ok 2 - should find node at first level
|
||||||
|
---
|
||||||
|
duration_ms: 0.187964
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should find nested node
|
||||||
|
ok 3 - should find nested node
|
||||||
|
---
|
||||||
|
duration_ms: 0.36677
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should return undefined for non-existent path
|
||||||
|
ok 4 - should return undefined for non-existent path
|
||||||
|
---
|
||||||
|
duration_ms: 0.413606
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should return undefined for partial path
|
||||||
|
ok 5 - should return undefined for partial path
|
||||||
|
---
|
||||||
|
duration_ms: 0.193163
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..5
|
||||||
|
ok 7 - findNode
|
||||||
|
---
|
||||||
|
duration_ms: 1.618028
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: getFolderPaths
|
||||||
|
# Subtest: should return all folder paths
|
||||||
|
ok 1 - should return all folder paths
|
||||||
|
---
|
||||||
|
duration_ms: 0.272088
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..1
|
||||||
|
ok 8 - getFolderPaths
|
||||||
|
---
|
||||||
|
duration_ms: 0.341484
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: sort
|
||||||
|
# Subtest: should sort nodes according to sort function
|
||||||
|
ok 1 - should sort nodes according to sort function
|
||||||
|
---
|
||||||
|
duration_ms: 5.333489
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..1
|
||||||
|
ok 9 - sort
|
||||||
|
---
|
||||||
|
duration_ms: 5.418013
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: pathToNode
|
||||||
|
# Subtest: should return root node for empty path
|
||||||
|
ok 1 - should return root node for empty path
|
||||||
|
---
|
||||||
|
duration_ms: 0.244226
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should return root node for index path
|
||||||
|
ok 2 - should return root node for index path
|
||||||
|
---
|
||||||
|
duration_ms: 0.111654
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should return path to first level node
|
||||||
|
ok 3 - should return path to first level node
|
||||||
|
---
|
||||||
|
duration_ms: 0.110121
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should return path to nested node
|
||||||
|
ok 4 - should return path to nested node
|
||||||
|
---
|
||||||
|
duration_ms: 0.105232
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should return undefined for non-existent path
|
||||||
|
ok 5 - should return undefined for non-existent path
|
||||||
|
---
|
||||||
|
duration_ms: 0.089062
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should return file data for intermediate folders
|
||||||
|
ok 6 - should return file data for intermediate folders
|
||||||
|
---
|
||||||
|
duration_ms: 0.130208
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: should return path for partial path
|
||||||
|
ok 7 - should return path for partial path
|
||||||
|
---
|
||||||
|
duration_ms: 0.111404
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..7
|
||||||
|
ok 10 - pathToNode
|
||||||
|
---
|
||||||
|
duration_ms: 1.075696
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
1..10
|
||||||
|
ok 10 - FileTrie
|
||||||
|
---
|
||||||
|
duration_ms: 16.524563
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: typeguards
|
||||||
|
# Subtest: isSimpleSlug
|
||||||
|
ok 1 - isSimpleSlug
|
||||||
|
---
|
||||||
|
duration_ms: 0.883505
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: isRelativeURL
|
||||||
|
ok 2 - isRelativeURL
|
||||||
|
---
|
||||||
|
duration_ms: 0.172896
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: isAbsoluteURL
|
||||||
|
ok 3 - isAbsoluteURL
|
||||||
|
---
|
||||||
|
duration_ms: 0.165823
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: isFullSlug
|
||||||
|
ok 4 - isFullSlug
|
||||||
|
---
|
||||||
|
duration_ms: 0.170051
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: isFilePath
|
||||||
|
ok 5 - isFilePath
|
||||||
|
---
|
||||||
|
duration_ms: 0.089053
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..5
|
||||||
|
ok 11 - typeguards
|
||||||
|
---
|
||||||
|
duration_ms: 2.272023
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: transforms
|
||||||
|
# Subtest: simplifySlug
|
||||||
|
ok 1 - simplifySlug
|
||||||
|
---
|
||||||
|
duration_ms: 0.26798
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: slugifyFilePath
|
||||||
|
ok 2 - slugifyFilePath
|
||||||
|
---
|
||||||
|
duration_ms: 0.697315
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: slugifyFilePath + simplifySlug end-to-end canonicalization
|
||||||
|
ok 3 - slugifyFilePath + simplifySlug end-to-end canonicalization
|
||||||
|
---
|
||||||
|
duration_ms: 0.206718
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: transformInternalLink
|
||||||
|
ok 4 - transformInternalLink
|
||||||
|
---
|
||||||
|
duration_ms: 0.893644
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: pathToRoot
|
||||||
|
ok 5 - pathToRoot
|
||||||
|
---
|
||||||
|
duration_ms: 0.282005
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: joinSegments
|
||||||
|
ok 6 - joinSegments
|
||||||
|
---
|
||||||
|
duration_ms: 0.129106
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..6
|
||||||
|
ok 12 - transforms
|
||||||
|
---
|
||||||
|
duration_ms: 2.744245
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: link strategies
|
||||||
|
# Subtest: absolute
|
||||||
|
# Subtest: from a/b/c
|
||||||
|
ok 1 - from a/b/c
|
||||||
|
---
|
||||||
|
duration_ms: 0.427531
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: from a/b/index
|
||||||
|
ok 2 - from a/b/index
|
||||||
|
---
|
||||||
|
duration_ms: 0.132623
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: from index
|
||||||
|
ok 3 - from index
|
||||||
|
---
|
||||||
|
duration_ms: 0.119779
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..3
|
||||||
|
ok 1 - absolute
|
||||||
|
---
|
||||||
|
duration_ms: 0.822824
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: shortest
|
||||||
|
# Subtest: from a/b/c
|
||||||
|
ok 1 - from a/b/c
|
||||||
|
---
|
||||||
|
duration_ms: 0.283478
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: from a/b/index
|
||||||
|
ok 2 - from a/b/index
|
||||||
|
---
|
||||||
|
duration_ms: 0.12545
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: from index
|
||||||
|
ok 3 - from index
|
||||||
|
---
|
||||||
|
duration_ms: 0.160313
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..3
|
||||||
|
ok 2 - shortest
|
||||||
|
---
|
||||||
|
duration_ms: 0.665467
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: relative
|
||||||
|
# Subtest: from a/b/c
|
||||||
|
ok 1 - from a/b/c
|
||||||
|
---
|
||||||
|
duration_ms: 0.177294
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: from a/b/index
|
||||||
|
ok 2 - from a/b/index
|
||||||
|
---
|
||||||
|
duration_ms: 0.097328
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: from index
|
||||||
|
ok 3 - from index
|
||||||
|
---
|
||||||
|
duration_ms: 0.08224
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..3
|
||||||
|
ok 3 - relative
|
||||||
|
---
|
||||||
|
duration_ms: 0.438261
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
1..3
|
||||||
|
ok 13 - link strategies
|
||||||
|
---
|
||||||
|
duration_ms: 2.063622
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: resolveRelative
|
||||||
|
# Subtest: from index
|
||||||
|
ok 1 - from index
|
||||||
|
---
|
||||||
|
duration_ms: 0.105263
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: from nested page
|
||||||
|
ok 2 - from nested page
|
||||||
|
---
|
||||||
|
duration_ms: 0.068305
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: with index paths
|
||||||
|
ok 3 - with index paths
|
||||||
|
---
|
||||||
|
duration_ms: 0.063787
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: with simple slugs
|
||||||
|
ok 4 - with simple slugs
|
||||||
|
---
|
||||||
|
duration_ms: 0.079125
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..4
|
||||||
|
ok 14 - resolveRelative
|
||||||
|
---
|
||||||
|
duration_ms: 0.411662
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: detectSlugCollisions
|
||||||
|
# Subtest: returns empty array when there are no collisions
|
||||||
|
ok 1 - returns empty array when there are no collisions
|
||||||
|
---
|
||||||
|
duration_ms: 1.516302
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: returns empty array for empty input
|
||||||
|
ok 2 - returns empty array for empty input
|
||||||
|
---
|
||||||
|
duration_ms: 0.194716
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: detects a two-file collision with winner = last file
|
||||||
|
ok 3 - detects a two-file collision with winner = last file
|
||||||
|
---
|
||||||
|
duration_ms: 0.174559
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: detects a three-file collision with all files listed, winner = last
|
||||||
|
ok 4 - detects a three-file collision with all files listed, winner = last
|
||||||
|
---
|
||||||
|
duration_ms: 0.14758
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: detects multiple separate collisions
|
||||||
|
ok 5 - detects multiple separate collisions
|
||||||
|
---
|
||||||
|
duration_ms: 0.275203
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: ignores entries without a slug
|
||||||
|
ok 6 - ignores entries without a slug
|
||||||
|
---
|
||||||
|
duration_ms: 0.161285
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: winner annotation matches fileTrie last-insert-wins semantics
|
||||||
|
ok 7 - winner annotation matches fileTrie last-insert-wins semantics
|
||||||
|
---
|
||||||
|
duration_ms: 0.133424
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..7
|
||||||
|
ok 15 - detectSlugCollisions
|
||||||
|
---
|
||||||
|
duration_ms: 3.771814
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
# Subtest: formatCollisionWarning
|
||||||
|
# Subtest: returns empty string for empty input
|
||||||
|
ok 1 - returns empty string for empty input
|
||||||
|
---
|
||||||
|
duration_ms: 0.356021
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: formats single collision with winner and shadowed markers
|
||||||
|
ok 2 - formats single collision with winner and shadowed markers
|
||||||
|
---
|
||||||
|
duration_ms: 0.479707
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: formats multiple collisions with count in header
|
||||||
|
ok 3 - formats multiple collisions with count in header
|
||||||
|
---
|
||||||
|
duration_ms: 0.334
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: output mentions Folder Notes convention as a common cause
|
||||||
|
ok 4 - output mentions Folder Notes convention as a common cause
|
||||||
|
---
|
||||||
|
duration_ms: 0.156646
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
# Subtest: falls back to filePath when relativePath is missing
|
||||||
|
ok 5 - falls back to filePath when relativePath is missing
|
||||||
|
---
|
||||||
|
duration_ms: 0.189837
|
||||||
|
type: 'test'
|
||||||
|
...
|
||||||
|
1..5
|
||||||
|
ok 16 - formatCollisionWarning
|
||||||
|
---
|
||||||
|
duration_ms: 1.94738
|
||||||
|
type: 'suite'
|
||||||
|
...
|
||||||
|
1..16
|
||||||
|
# tests 118
|
||||||
|
# suites 33
|
||||||
|
# pass 118
|
||||||
|
# fail 0
|
||||||
|
# cancelled 0
|
||||||
|
# skipped 0
|
||||||
|
# todo 0
|
||||||
|
# duration_ms 225.288364
|
||||||
@@ -412,13 +412,13 @@ export const MyFrame: PageFrame = {
|
|||||||
grid-template-areas: "center";
|
grid-template-areas: "center";
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
render({ componentData, pageBody: Content, footer: Footer }: PageFrameProps): unknown {
|
render({ componentData, pageBody: Content, footer: Footers }: PageFrameProps): unknown {
|
||||||
const renderSlot = (C: (props: typeof componentData) => unknown): ComponentChildren =>
|
const renderSlot = (C: (props: typeof componentData) => unknown): ComponentChildren =>
|
||||||
C(componentData) as ComponentChildren
|
C(componentData) as ComponentChildren
|
||||||
return (
|
return (
|
||||||
<div class="center">
|
<div class="center">
|
||||||
{(Content as any)(componentData)}
|
{(Content as any)(componentData)}
|
||||||
{(Footer as any)(componentData)}
|
{Footers.map((Footer) => (Footer as any)(componentData))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -428,7 +428,7 @@ export const MyFrame: PageFrame = {
|
|||||||
Key requirements:
|
Key requirements:
|
||||||
|
|
||||||
- `name`: A unique string identifier. This is what page types and YAML config reference.
|
- `name`: A unique string identifier. This is what page types and YAML config reference.
|
||||||
- `render()`: Receives all layout slots (header, sidebars, content, footer) and returns JSX for the inner page structure.
|
- `render()`: Receives all layout slots (header, sidebars, content, footer) and returns JSX for the inner page structure. Note that `footer` is a `QuartzComponent[]` (an array) — frames should iterate over it with `.map()` to render all footer components.
|
||||||
- `css` (optional): Frame-specific CSS. Scope it with `.page[data-frame="my-frame"]` selectors to avoid conflicts.
|
- `css` (optional): Frame-specific CSS. Scope it with `.page[data-frame="my-frame"]` selectors to avoid conflicts.
|
||||||
|
|
||||||
**2. Re-export the frame:**
|
**2. Re-export the frame:**
|
||||||
|
|||||||
@@ -28,6 +28,42 @@ npx quartz plugin install --latest
|
|||||||
|
|
||||||
See the [[upgrade|CLI reference for upgrade]] for more details on available flags.
|
See the [[upgrade|CLI reference for upgrade]] for more details on available flags.
|
||||||
|
|
||||||
|
### Layout System Changes
|
||||||
|
|
||||||
|
The `footer` layout slot is now an array of components, consistent with other layout slots like `header`, `left`, and `right`. Additionally, `header` and `footer` are now configurable layout positions — plugins can declare `layout: { position: header }` or `layout: { position: footer }` in their YAML config.
|
||||||
|
|
||||||
|
**If you override layouts in `quartz.ts`**, update any `footer` assignments to use arrays:
|
||||||
|
|
||||||
|
```ts title="quartz.ts"
|
||||||
|
// Before
|
||||||
|
export const layout = await loadQuartzLayout({
|
||||||
|
defaults: { footer: MyFooterComponent },
|
||||||
|
})
|
||||||
|
|
||||||
|
// After
|
||||||
|
export const layout = await loadQuartzLayout({
|
||||||
|
defaults: { footer: [MyFooterComponent] },
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**If you have a custom page frame**, update the `render` function to iterate the footer array:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Before
|
||||||
|
render({ footer: Footer, ...rest }: PageFrameProps) {
|
||||||
|
return <Footer {...componentData} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
render({ footer, ...rest }: PageFrameProps) {
|
||||||
|
return footer.map((F) => <F {...componentData} />)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both changes are caught by TypeScript at compile time — running `npx quartz build` will show the error.
|
||||||
|
|
||||||
|
No changes are needed for `quartz.config.yaml` — the YAML format is unchanged.
|
||||||
|
|
||||||
### Cleaning Up Unused Plugins
|
### Cleaning Up Unused Plugins
|
||||||
|
|
||||||
If you've removed plugins from your configuration during an upgrade, you can clean up the leftover files:
|
If you've removed plugins from your configuration during an upgrade, you can clean up the leftover files:
|
||||||
|
|||||||
+10
-2
@@ -20,7 +20,7 @@ export interface FullPageLayout {
|
|||||||
afterBody: QuartzComponent[] // laid out vertically
|
afterBody: QuartzComponent[] // laid out vertically
|
||||||
left: QuartzComponent[] // vertical on desktop and tablet, horizontal on mobile
|
left: QuartzComponent[] // vertical on desktop and tablet, horizontal on mobile
|
||||||
right: QuartzComponent[] // vertical on desktop, horizontal on tablet and mobile
|
right: QuartzComponent[] // vertical on desktop, horizontal on tablet and mobile
|
||||||
footer: QuartzComponent // single component
|
footer: QuartzComponent[] // laid out vertically
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ These correspond to following parts of the page:
|
|||||||
> There are two additional layout fields that are _not_ shown in the above diagram.
|
> There are two additional layout fields that are _not_ shown in the above diagram.
|
||||||
>
|
>
|
||||||
> 1. `head` is a single component that renders the `<head>` [tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) in the HTML. This doesn't appear visually on the page and is only is responsible for metadata about the document like the tab title, scripts, and styles.
|
> 1. `head` is a single component that renders the `<head>` [tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) in the HTML. This doesn't appear visually on the page and is only is responsible for metadata about the document like the tab title, scripts, and styles.
|
||||||
> 2. `header` is a set of components that are laid out horizontally and appears _before_ the `beforeBody` section. This enables you to replicate the old Quartz 3 header bar where the title, search bar, and dark mode toggle. By default, Quartz doesn't place any components in the `header`.
|
> 2. `header` is a set of components that are laid out horizontally and appears _before_ the `beforeBody` section. You can place components in the header by setting `layout.position: header` in your plugin configuration. This enables layouts similar to Quartz 3's header bar with title, search bar, and dark mode toggle.
|
||||||
|
|
||||||
Layout components are configured in the `layout` section of `quartz.config.yaml`. Plugins declare their position and priority, and the layout system arranges them automatically:
|
Layout components are configured in the `layout` section of `quartz.config.yaml`. Plugins declare their position and priority, and the layout system arranges them automatically:
|
||||||
|
|
||||||
@@ -77,12 +77,20 @@ plugins:
|
|||||||
layout:
|
layout:
|
||||||
position: beforeBody
|
position: beforeBody
|
||||||
priority: 30
|
priority: 30
|
||||||
|
- source: github:quartz-community/darkmode
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: header
|
||||||
|
priority: 10
|
||||||
- source: github:quartz-community/footer
|
- source: github:quartz-community/footer
|
||||||
enabled: true
|
enabled: true
|
||||||
options:
|
options:
|
||||||
links:
|
links:
|
||||||
GitHub: https://github.com/jackyzha0/quartz
|
GitHub: https://github.com/jackyzha0/quartz
|
||||||
Discord Community: https://discord.gg/cRFFHYye7t
|
Discord Community: https://discord.gg/cRFFHYye7t
|
||||||
|
layout:
|
||||||
|
position: footer
|
||||||
|
priority: 50
|
||||||
|
|
||||||
layout:
|
layout:
|
||||||
groups:
|
groups:
|
||||||
|
|||||||
@@ -205,6 +205,9 @@ plugins:
|
|||||||
links:
|
links:
|
||||||
GitHub: https://github.com/jackyzha0/quartz
|
GitHub: https://github.com/jackyzha0/quartz
|
||||||
Discord Community: https://discord.gg/cRFFHYye7t
|
Discord Community: https://discord.gg/cRFFHYye7t
|
||||||
|
layout:
|
||||||
|
position: footer
|
||||||
|
priority: 50
|
||||||
- source: "@quartz-community/recent-notes"
|
- source: "@quartz-community/recent-notes"
|
||||||
enabled: false
|
enabled: false
|
||||||
- source: "@quartz-community/spacer"
|
- source: "@quartz-community/spacer"
|
||||||
@@ -248,10 +251,6 @@ plugins:
|
|||||||
unlistWhenEncrypted: false
|
unlistWhenEncrypted: false
|
||||||
outputPath: static/encryptedContentIndex.json
|
outputPath: static/encryptedContentIndex.json
|
||||||
order: 900
|
order: 900
|
||||||
layout:
|
|
||||||
position: body
|
|
||||||
priority: 100
|
|
||||||
display: all
|
|
||||||
- source: "@quartz-community/stacked-pages"
|
- source: "@quartz-community/stacked-pages"
|
||||||
enabled: false
|
enabled: false
|
||||||
layout:
|
layout:
|
||||||
|
|||||||
+1
-1
@@ -97,7 +97,7 @@ export interface FullPageLayout {
|
|||||||
afterBody: QuartzComponent[]
|
afterBody: QuartzComponent[]
|
||||||
left: QuartzComponent[]
|
left: QuartzComponent[]
|
||||||
right: QuartzComponent[]
|
right: QuartzComponent[]
|
||||||
footer: QuartzComponent
|
footer: QuartzComponent[]
|
||||||
/** Page frame name (e.g. "default", "full-width", "minimal"). Defaults to "default". */
|
/** Page frame name (e.g. "default", "full-width", "minimal"). Defaults to "default". */
|
||||||
frame?: string
|
frame?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,9 @@ plugins:
|
|||||||
links:
|
links:
|
||||||
GitHub: https://github.com/jackyzha0/quartz
|
GitHub: https://github.com/jackyzha0/quartz
|
||||||
Discord Community: https://discord.gg/cRFFHYye7t
|
Discord Community: https://discord.gg/cRFFHYye7t
|
||||||
|
layout:
|
||||||
|
position: footer
|
||||||
|
priority: 50
|
||||||
- source: "@quartz-community/recent-notes"
|
- source: "@quartz-community/recent-notes"
|
||||||
enabled: true
|
enabled: true
|
||||||
options:
|
options:
|
||||||
|
|||||||
@@ -224,6 +224,9 @@ plugins:
|
|||||||
links:
|
links:
|
||||||
GitHub: https://github.com/jackyzha0/quartz
|
GitHub: https://github.com/jackyzha0/quartz
|
||||||
Discord Community: https://discord.gg/cRFFHYye7t
|
Discord Community: https://discord.gg/cRFFHYye7t
|
||||||
|
layout:
|
||||||
|
position: footer
|
||||||
|
priority: 50
|
||||||
- source: "@quartz-community/recent-notes"
|
- source: "@quartz-community/recent-notes"
|
||||||
enabled: false
|
enabled: false
|
||||||
- source: "@quartz-community/spacer"
|
- source: "@quartz-community/spacer"
|
||||||
|
|||||||
@@ -235,6 +235,9 @@ plugins:
|
|||||||
links:
|
links:
|
||||||
GitHub: https://github.com/jackyzha0/quartz
|
GitHub: https://github.com/jackyzha0/quartz
|
||||||
Discord Community: https://discord.gg/cRFFHYye7t
|
Discord Community: https://discord.gg/cRFFHYye7t
|
||||||
|
layout:
|
||||||
|
position: footer
|
||||||
|
priority: 50
|
||||||
- source: "@quartz-community/recent-notes"
|
- source: "@quartz-community/recent-notes"
|
||||||
enabled: false
|
enabled: false
|
||||||
- source: "@quartz-community/spacer"
|
- source: "@quartz-community/spacer"
|
||||||
|
|||||||
@@ -236,6 +236,9 @@ plugins:
|
|||||||
links:
|
links:
|
||||||
GitHub: https://github.com/jackyzha0/quartz
|
GitHub: https://github.com/jackyzha0/quartz
|
||||||
Discord Community: https://discord.gg/cRFFHYye7t
|
Discord Community: https://discord.gg/cRFFHYye7t
|
||||||
|
layout:
|
||||||
|
position: footer
|
||||||
|
priority: 50
|
||||||
- source: "@quartz-community/recent-notes"
|
- source: "@quartz-community/recent-notes"
|
||||||
enabled: false
|
enabled: false
|
||||||
- source: "@quartz-community/spacer"
|
- source: "@quartz-community/spacer"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const DefaultFrame: PageFrame = {
|
|||||||
afterBody,
|
afterBody,
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
footer: Footer,
|
footer,
|
||||||
}: PageFrameProps) {
|
}: PageFrameProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -54,7 +54,9 @@ export const DefaultFrame: PageFrame = {
|
|||||||
<BodyComponent {...componentData} />
|
<BodyComponent {...componentData} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Footer {...componentData} />
|
{footer.map((FooterComponent) => (
|
||||||
|
<FooterComponent {...componentData} />
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const FullWidthFrame: PageFrame = {
|
|||||||
beforeBody,
|
beforeBody,
|
||||||
pageBody: Content,
|
pageBody: Content,
|
||||||
afterBody,
|
afterBody,
|
||||||
footer: Footer,
|
footer,
|
||||||
}: PageFrameProps) {
|
}: PageFrameProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -44,7 +44,9 @@ export const FullWidthFrame: PageFrame = {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer {...componentData} />
|
{footer.map((FooterComponent) => (
|
||||||
|
<FooterComponent {...componentData} />
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import { PageFrame, PageFrameProps } from "./types"
|
|||||||
*/
|
*/
|
||||||
export const MinimalFrame: PageFrame = {
|
export const MinimalFrame: PageFrame = {
|
||||||
name: "minimal",
|
name: "minimal",
|
||||||
render({ componentData, pageBody: Content, footer: Footer }: PageFrameProps) {
|
render({ componentData, pageBody: Content, footer }: PageFrameProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="center minimal">
|
<div class="center minimal">
|
||||||
<Content {...componentData} />
|
<Content {...componentData} />
|
||||||
</div>
|
</div>
|
||||||
<Footer {...componentData} />
|
{footer.map((FooterComponent) => (
|
||||||
|
<FooterComponent {...componentData} />
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export interface PageFrameProps {
|
|||||||
left: QuartzComponent[]
|
left: QuartzComponent[]
|
||||||
/** Right sidebar components */
|
/** Right sidebar components */
|
||||||
right: QuartzComponent[]
|
right: QuartzComponent[]
|
||||||
/** Footer component */
|
/** Footer components */
|
||||||
footer: QuartzComponent
|
footer: QuartzComponent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export interface ComponentManifest {
|
|||||||
quartzVersion?: string
|
quartzVersion?: string
|
||||||
author?: string
|
author?: string
|
||||||
homepage?: string
|
homepage?: string
|
||||||
|
defaultPosition?: string
|
||||||
|
defaultPriority?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RegisteredComponent {
|
export interface RegisteredComponent {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface RenderComponents {
|
|||||||
afterBody: QuartzComponent[]
|
afterBody: QuartzComponent[]
|
||||||
left: QuartzComponent[]
|
left: QuartzComponent[]
|
||||||
right: QuartzComponent[]
|
right: QuartzComponent[]
|
||||||
footer: QuartzComponent
|
footer: QuartzComponent[]
|
||||||
frame?: string
|
frame?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +329,7 @@ export function renderPage(
|
|||||||
afterBody,
|
afterBody,
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
footer: Footer,
|
footer,
|
||||||
frame: frameName,
|
frame: frameName,
|
||||||
} = components
|
} = components
|
||||||
const Body = BodyConstructor()
|
const Body = BodyConstructor()
|
||||||
@@ -360,7 +360,7 @@ export function renderPage(
|
|||||||
afterBody,
|
afterBody,
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
footer: Footer,
|
footer,
|
||||||
}),
|
}),
|
||||||
]}
|
]}
|
||||||
</Body>
|
</Body>
|
||||||
|
|||||||
@@ -644,7 +644,7 @@ export async function loadQuartzLayout(layoutOverrides?: {
|
|||||||
return oldLayout.layout
|
return oldLayout.layout
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabledWithLayout = json.plugins.filter((e) => e.enabled && e.layout)
|
const enabledWithLayout = json.plugins.filter((e) => e.enabled)
|
||||||
const layoutConfig = json.layout ?? {}
|
const layoutConfig = json.layout ?? {}
|
||||||
|
|
||||||
// Build default layout for all page types
|
// Build default layout for all page types
|
||||||
@@ -672,7 +672,7 @@ export async function loadQuartzLayout(layoutOverrides?: {
|
|||||||
if (Array.isArray(components) && components.length === 0) {
|
if (Array.isArray(components) && components.length === 0) {
|
||||||
const key = pos as keyof Pick<
|
const key = pos as keyof Pick<
|
||||||
FullPageLayout,
|
FullPageLayout,
|
||||||
"left" | "right" | "beforeBody" | "afterBody"
|
"header" | "left" | "right" | "beforeBody" | "afterBody" | "footer"
|
||||||
>
|
>
|
||||||
if (key in ptLayout) {
|
if (key in ptLayout) {
|
||||||
;(ptLayout as Record<string, unknown>)[key] = []
|
;(ptLayout as Record<string, unknown>)[key] = []
|
||||||
@@ -690,46 +690,20 @@ export async function loadQuartzLayout(layoutOverrides?: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Head (built-in) and Footer (plugin)
|
|
||||||
const HeadModule = await import("../../components/Head")
|
const HeadModule = await import("../../components/Head")
|
||||||
const head = HeadModule.default()
|
const head = HeadModule.default()
|
||||||
|
|
||||||
// Find footer from component registry (loaded during plugin instantiation)
|
|
||||||
const footerEntry = json.plugins.find(
|
|
||||||
(e) => e.enabled && extractPluginName(e.source) === "footer",
|
|
||||||
)
|
|
||||||
let footer: QuartzComponent | undefined
|
|
||||||
if (footerEntry) {
|
|
||||||
// Try registry lookup: plugin name ("footer") or export name ("Footer")
|
|
||||||
const footerReg = componentRegistry.get("footer") ?? componentRegistry.get("Footer")
|
|
||||||
if (footerReg) {
|
|
||||||
if (typeof footerReg.component === "function" && !("displayName" in footerReg.component)) {
|
|
||||||
// It's a constructor — use registry cache for consistent instances
|
|
||||||
const footerOverrides = componentRegistry.getOptionOverrides("footer")
|
|
||||||
const opts = { ...footerEntry.options, ...footerOverrides }
|
|
||||||
footer = componentRegistry.instantiate(
|
|
||||||
footerReg.component as QuartzComponentConstructor,
|
|
||||||
Object.keys(opts).length > 0 ? opts : undefined,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
footer = footerReg.component as QuartzComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply structural defaults
|
// Apply structural defaults
|
||||||
defaultLayout.head = head
|
defaultLayout.head = head
|
||||||
defaultLayout.header = defaultLayout.header ?? []
|
defaultLayout.header = defaultLayout.header ?? []
|
||||||
if (footer) {
|
defaultLayout.footer = defaultLayout.footer ?? []
|
||||||
defaultLayout.footer = footer
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure all byPageType entries inherit structural slots
|
// Ensure all byPageType entries inherit structural slots
|
||||||
for (const pageType of Object.keys(byPageType)) {
|
for (const pageType of Object.keys(byPageType)) {
|
||||||
const pt = byPageType[pageType]
|
const pt = byPageType[pageType]
|
||||||
if (!pt.head) pt.head = head
|
if (!pt.head) pt.head = head
|
||||||
if (!pt.header) pt.header = []
|
if (!pt.header) pt.header = defaultLayout.header
|
||||||
if (footer && !pt.footer) pt.footer = footer
|
if (!pt.footer) pt.footer = defaultLayout.footer
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedDefaults = { ...defaultLayout, ...layoutOverrides?.defaults }
|
const mergedDefaults = { ...defaultLayout, ...layoutOverrides?.defaults }
|
||||||
@@ -756,10 +730,12 @@ function buildLayoutForEntries(
|
|||||||
groupOptions?: PluginLayoutDeclaration["groupOptions"]
|
groupOptions?: PluginLayoutDeclaration["groupOptions"]
|
||||||
}[]
|
}[]
|
||||||
> = {
|
> = {
|
||||||
|
header: [],
|
||||||
left: [],
|
left: [],
|
||||||
right: [],
|
right: [],
|
||||||
beforeBody: [],
|
beforeBody: [],
|
||||||
afterBody: [],
|
afterBody: [],
|
||||||
|
footer: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -828,6 +804,48 @@ function buildLayoutForEntries(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.enabled || entry.layout) continue
|
||||||
|
|
||||||
|
const name = extractPluginName(entry.source)
|
||||||
|
const registered =
|
||||||
|
componentRegistry.get(name) ??
|
||||||
|
componentRegistry.get(`${formatSourceDisplay(entry.source)}/${name}`)
|
||||||
|
const pascalName = name
|
||||||
|
.split("-")
|
||||||
|
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||||
|
.join("")
|
||||||
|
const reg = registered ?? componentRegistry.get(pascalName)
|
||||||
|
if (!reg) continue
|
||||||
|
|
||||||
|
const layoutDefaults = reg.manifest
|
||||||
|
const defaultPosition = layoutDefaults?.defaultPosition
|
||||||
|
if (!defaultPosition) continue
|
||||||
|
|
||||||
|
const posArray = positions[defaultPosition]
|
||||||
|
if (!posArray) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let component: QuartzComponent
|
||||||
|
if (typeof reg.component === "function" && !("displayName" in reg.component)) {
|
||||||
|
const tsOverrides = componentRegistry.getOptionOverrides(name)
|
||||||
|
const opts = { ...entry.options, ...tsOverrides }
|
||||||
|
const optsArg = Object.keys(opts).length > 0 ? opts : undefined
|
||||||
|
component = componentRegistry.instantiate(
|
||||||
|
reg.component as QuartzComponentConstructor,
|
||||||
|
optsArg,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
component = reg.component as QuartzComponent
|
||||||
|
}
|
||||||
|
|
||||||
|
posArray.push({
|
||||||
|
component,
|
||||||
|
priority: layoutDefaults?.defaultPriority ?? 50,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by priority and resolve groups
|
// Sort by priority and resolve groups
|
||||||
const result: Partial<FullPageLayout> = {}
|
const result: Partial<FullPageLayout> = {}
|
||||||
|
|
||||||
@@ -837,7 +855,7 @@ function buildLayoutForEntries(
|
|||||||
const resolved = resolveGroups(items, layoutConfig.groups ?? {})
|
const resolved = resolveGroups(items, layoutConfig.groups ?? {})
|
||||||
const key = position as keyof Pick<
|
const key = position as keyof Pick<
|
||||||
FullPageLayout,
|
FullPageLayout,
|
||||||
"left" | "right" | "beforeBody" | "afterBody"
|
"header" | "left" | "right" | "beforeBody" | "afterBody" | "footer"
|
||||||
>
|
>
|
||||||
;(result as Record<string, QuartzComponent[]>)[key] = resolved
|
;(result as Record<string, QuartzComponent[]>)[key] = resolved
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { BuildCtx } from "../../util/ctx"
|
|||||||
|
|
||||||
export type PluginCategory = "transformer" | "filter" | "emitter" | "pageType" | "component"
|
export type PluginCategory = "transformer" | "filter" | "emitter" | "pageType" | "component"
|
||||||
|
|
||||||
export type LayoutPosition = "left" | "right" | "beforeBody" | "afterBody"
|
export type LayoutPosition = "left" | "right" | "beforeBody" | "afterBody" | "header" | "footer"
|
||||||
|
|
||||||
export type LayoutDisplay = "all" | "mobile-only" | "desktop-only"
|
export type LayoutDisplay = "all" | "mobile-only" | "desktop-only"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import test, { describe } from "node:test"
|
||||||
|
import assert from "node:assert"
|
||||||
|
import { resolveLayout } from "./dispatcher"
|
||||||
|
import { QuartzPageTypePluginInstance } from "../types"
|
||||||
|
import { QuartzComponent } from "../../components/types"
|
||||||
|
|
||||||
|
const StubA: QuartzComponent = (() => null) as unknown as QuartzComponent
|
||||||
|
const StubB: QuartzComponent = (() => null) as unknown as QuartzComponent
|
||||||
|
const StubHead: QuartzComponent = (() => null) as unknown as QuartzComponent
|
||||||
|
|
||||||
|
function makePageType(
|
||||||
|
overrides: Partial<QuartzPageTypePluginInstance> = {},
|
||||||
|
): QuartzPageTypePluginInstance {
|
||||||
|
return {
|
||||||
|
name: "test-page-type",
|
||||||
|
layout: "content",
|
||||||
|
match: () => true,
|
||||||
|
body: () => (() => null) as unknown as QuartzComponent,
|
||||||
|
...overrides,
|
||||||
|
} as QuartzPageTypePluginInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveLayout", () => {
|
||||||
|
test("footer defaults to [] when sharedDefaults omits footer", () => {
|
||||||
|
const result = resolveLayout(makePageType(), { head: StubHead }, {})
|
||||||
|
assert.deepStrictEqual(result.footer, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("header defaults to [] when sharedDefaults omits header", () => {
|
||||||
|
const result = resolveLayout(makePageType(), { head: StubHead }, {})
|
||||||
|
assert.deepStrictEqual(result.header, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("footer from sharedDefaults is used when no override", () => {
|
||||||
|
const result = resolveLayout(makePageType(), { head: StubHead, footer: [StubA] }, {})
|
||||||
|
assert.deepStrictEqual(result.footer, [StubA])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("byPageType override replaces footer", () => {
|
||||||
|
const result = resolveLayout(
|
||||||
|
makePageType(),
|
||||||
|
{ head: StubHead, footer: [StubA] },
|
||||||
|
{ content: { footer: [StubB] } },
|
||||||
|
)
|
||||||
|
assert.deepStrictEqual(result.footer, [StubB])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("byPageType override clears footer with []", () => {
|
||||||
|
const result = resolveLayout(
|
||||||
|
makePageType(),
|
||||||
|
{ head: StubHead, footer: [StubA] },
|
||||||
|
{ content: { footer: [] } },
|
||||||
|
)
|
||||||
|
assert.deepStrictEqual(result.footer, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("byPageType override clears header with []", () => {
|
||||||
|
const result = resolveLayout(
|
||||||
|
makePageType(),
|
||||||
|
{ head: StubHead, header: [StubA] },
|
||||||
|
{ content: { header: [] } },
|
||||||
|
)
|
||||||
|
assert.deepStrictEqual(result.header, [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resolveLayout frame resolution", () => {
|
||||||
|
test("config override frame wins over page type frame", () => {
|
||||||
|
const result = resolveLayout(
|
||||||
|
makePageType({ frame: "minimal" }),
|
||||||
|
{ head: StubHead },
|
||||||
|
{ content: { frame: "full-width" } },
|
||||||
|
)
|
||||||
|
assert.strictEqual(result.frame, "full-width")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("page type frame wins when no config override", () => {
|
||||||
|
const result = resolveLayout(makePageType({ frame: "minimal" }), { head: StubHead }, {})
|
||||||
|
assert.strictEqual(result.frame, "minimal")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("defaults to 'default' when no frame specified", () => {
|
||||||
|
const result = resolveLayout(makePageType(), { head: StubHead }, {})
|
||||||
|
assert.strictEqual(result.frame, "default")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -15,7 +15,8 @@ function getPageTypes(ctx: BuildCtx): QuartzPageTypePluginInstance[] {
|
|||||||
return (ctx.cfg.plugins.pageTypes ?? []) as unknown as QuartzPageTypePluginInstance[]
|
return (ctx.cfg.plugins.pageTypes ?? []) as unknown as QuartzPageTypePluginInstance[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLayout(
|
/** @internal Exported for testing only. */
|
||||||
|
export function resolveLayout(
|
||||||
pageType: QuartzPageTypePluginInstance,
|
pageType: QuartzPageTypePluginInstance,
|
||||||
sharedDefaults: Partial<FullPageLayout>,
|
sharedDefaults: Partial<FullPageLayout>,
|
||||||
byPageType: Record<string, Partial<FullPageLayout>>,
|
byPageType: Record<string, Partial<FullPageLayout>>,
|
||||||
@@ -31,7 +32,7 @@ function resolveLayout(
|
|||||||
afterBody: overrides.afterBody ?? sharedDefaults.afterBody ?? [],
|
afterBody: overrides.afterBody ?? sharedDefaults.afterBody ?? [],
|
||||||
left: overrides.left ?? sharedDefaults.left ?? [],
|
left: overrides.left ?? sharedDefaults.left ?? [],
|
||||||
right: overrides.right ?? sharedDefaults.right ?? [],
|
right: overrides.right ?? sharedDefaults.right ?? [],
|
||||||
footer: overrides.footer ?? sharedDefaults.footer!,
|
footer: overrides.footer ?? sharedDefaults.footer ?? [],
|
||||||
frame,
|
frame,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ function collectComponents(
|
|||||||
...layout.afterBody,
|
...layout.afterBody,
|
||||||
...layout.left,
|
...layout.left,
|
||||||
...layout.right,
|
...layout.right,
|
||||||
layout.footer,
|
...layout.footer,
|
||||||
]
|
]
|
||||||
for (const c of all) {
|
for (const c of all) {
|
||||||
if (c) seen.add(c)
|
if (c) seen.add(c)
|
||||||
|
|||||||
Reference in New Issue
Block a user