methanol 0.0.25 → 0.0.27
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.
- package/package.json +7 -7
- package/src/client/sw.js +1 -2
- package/src/dev-server.js +19 -10
- package/src/feed.js +1 -1
- package/src/pages-index.js +1 -1
- package/src/pages.js +54 -22
- package/src/vite-plugins.js +1 -2
- package/src/workers/build-worker.js +10 -1
- package/themes/blog/sources/style.css +40 -20
- package/themes/blog/src/post-utils.js +1 -1
- package/themes/default/src/nav-tree.jsx +8 -8
- package/themes/default/src/page.jsx +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "methanol",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"description": "Static site generator powered by rEFui and MDX",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -31,21 +31,21 @@
|
|
|
31
31
|
"@stefanprobst/rehype-extract-toc": "^3.0.0",
|
|
32
32
|
"@wooorm/starry-night": "^3.9.0",
|
|
33
33
|
"chokidar": "^5.0.0",
|
|
34
|
-
"esbuild": "^0.
|
|
34
|
+
"esbuild": "^0.28.0",
|
|
35
35
|
"fast-glob": "^3.3.3",
|
|
36
36
|
"gray-matter": "^4.0.3",
|
|
37
37
|
"hast-util-is-element": "^3.0.0",
|
|
38
|
-
"htmlparser2": "^
|
|
38
|
+
"htmlparser2": "^12.0.0",
|
|
39
39
|
"json5": "^2.2.3",
|
|
40
|
-
"null-prototype-object": "^1.2.
|
|
41
|
-
"picomatch": "^4.0.
|
|
42
|
-
"refui": "^0.17.
|
|
40
|
+
"null-prototype-object": "^1.2.7",
|
|
41
|
+
"picomatch": "^4.0.4",
|
|
42
|
+
"refui": "^0.17.2",
|
|
43
43
|
"refurbish": "^0.1.8",
|
|
44
44
|
"rehype-slug": "^6.0.0",
|
|
45
45
|
"rehype-starry-night": "^2.2.0",
|
|
46
46
|
"remark-gfm": "^4.0.1",
|
|
47
47
|
"unist-util-visit": "^5.1.0",
|
|
48
|
-
"vite": "^
|
|
48
|
+
"vite": "^8.0.10",
|
|
49
49
|
"workbox-core": "^7.4.0",
|
|
50
50
|
"workbox-routing": "^7.4.0",
|
|
51
51
|
"workbox-strategies": "^7.4.0",
|
package/src/client/sw.js
CHANGED
|
@@ -25,8 +25,6 @@ const MANIFEST_HASH = '__METHANOL_MANIFEST_HASH__'
|
|
|
25
25
|
const DEFAULT_BATCH_SIZE = 5
|
|
26
26
|
const REVISION_HEADER = 'X-Methanol-Revision'
|
|
27
27
|
|
|
28
|
-
self.skipWaiting()
|
|
29
|
-
|
|
30
28
|
const resolveBasePrefix = cached(() => normalizeBasePrefix(import.meta.env?.BASE_URL || '/'))
|
|
31
29
|
|
|
32
30
|
const withBase = cachedStr((path) => {
|
|
@@ -126,6 +124,7 @@ async function getManifestIndex() {
|
|
|
126
124
|
|
|
127
125
|
// Precache prioritized entries during install
|
|
128
126
|
self.addEventListener('install', (event) => {
|
|
127
|
+
self.skipWaiting()
|
|
129
128
|
event.waitUntil(
|
|
130
129
|
(async () => {
|
|
131
130
|
try {
|
package/src/dev-server.js
CHANGED
|
@@ -532,16 +532,18 @@ export const runViteDev = async () => {
|
|
|
532
532
|
|
|
533
533
|
let html = ''
|
|
534
534
|
try {
|
|
535
|
-
html = await
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
535
|
+
html = await enqueueRender(() =>
|
|
536
|
+
renderHtml({
|
|
537
|
+
routePath: renderRoutePath,
|
|
538
|
+
path,
|
|
539
|
+
components: {
|
|
540
|
+
...themeComponents,
|
|
541
|
+
...components
|
|
542
|
+
},
|
|
543
|
+
pagesContext,
|
|
544
|
+
pageMeta
|
|
545
|
+
})
|
|
546
|
+
)
|
|
545
547
|
} catch (err) {
|
|
546
548
|
logMdxError('MDX render', err, pageMeta || { path, routePath: renderRoutePath })
|
|
547
549
|
await sendDevError(res, err, req.url)
|
|
@@ -581,6 +583,13 @@ export const runViteDev = async () => {
|
|
|
581
583
|
return queue
|
|
582
584
|
}
|
|
583
585
|
|
|
586
|
+
let renderQueue = Promise.resolve()
|
|
587
|
+
const enqueueRender = (task) => {
|
|
588
|
+
const next = renderQueue.then(task, task)
|
|
589
|
+
renderQueue = next.catch(() => {})
|
|
590
|
+
return next
|
|
591
|
+
}
|
|
592
|
+
|
|
584
593
|
const reload = () => {
|
|
585
594
|
server.ws.send({ type: 'full-reload' })
|
|
586
595
|
}
|
package/src/feed.js
CHANGED
|
@@ -111,7 +111,7 @@ export const selectFeedPages = (pages, options) => {
|
|
|
111
111
|
const limit = resolveFeedLimit(options)
|
|
112
112
|
if (!limit) return []
|
|
113
113
|
return (Array.isArray(pages) ? pages : [])
|
|
114
|
-
.filter((page) => page && !page.hidden && !page.
|
|
114
|
+
.filter((page) => page && !page.hidden && !page.hiddenByParents)
|
|
115
115
|
.sort((a, b) => getSortTime(b) - getSortTime(a))
|
|
116
116
|
.slice(0, limit)
|
|
117
117
|
}
|
package/src/pages-index.js
CHANGED
|
@@ -44,7 +44,7 @@ const sanitizePage = (page) => {
|
|
|
44
44
|
|
|
45
45
|
export const serializePagesIndex = (pages) => {
|
|
46
46
|
const list = Array.isArray(pages)
|
|
47
|
-
? pages.filter((page) => !page
|
|
47
|
+
? pages.filter((page) => !page.hidden && !page.hiddenByParents).map(sanitizePage)
|
|
48
48
|
: []
|
|
49
49
|
return JSON.stringify(JSON.stringify(list))
|
|
50
50
|
}
|
package/src/pages.js
CHANGED
|
@@ -526,7 +526,7 @@ export const buildPageEntry = async ({ path, pagesDir, source }) => {
|
|
|
526
526
|
const hidden =
|
|
527
527
|
frontmatterHidden === false
|
|
528
528
|
? false
|
|
529
|
-
:
|
|
529
|
+
: hiddenByFrontmatter
|
|
530
530
|
? true
|
|
531
531
|
: isSpecialPage
|
|
532
532
|
? true
|
|
@@ -695,37 +695,48 @@ const buildNavSequence = (nodes, pagesByRoute) => {
|
|
|
695
695
|
|
|
696
696
|
export const createPagesContextFromPages = ({ pages, excludedRoutes, excludedDirs } = {}) => {
|
|
697
697
|
const pageList = Array.isArray(pages) ? pages : []
|
|
698
|
-
const
|
|
698
|
+
const pagesAll = pageList
|
|
699
|
+
const pagesByRoute = new Map()
|
|
699
700
|
for (const page of pageList) {
|
|
700
|
-
if (
|
|
701
|
-
|
|
702
|
-
if (prefix !== '/') {
|
|
703
|
-
hiddenPrefixes.push(prefix)
|
|
704
|
-
}
|
|
701
|
+
if (!pagesByRoute.has(page.routePath)) {
|
|
702
|
+
pagesByRoute.set(page.routePath, page)
|
|
705
703
|
}
|
|
706
704
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
705
|
+
const resolveParentDirRoute = (routePath) => {
|
|
706
|
+
const normalized = normalizeRoutePath(routePath || '/')
|
|
707
|
+
if (normalized === '/') return null
|
|
708
|
+
if (normalized.endsWith('/')) {
|
|
709
|
+
const stripped = stripTrailingSlash(normalized)
|
|
710
|
+
const index = stripped.lastIndexOf('/')
|
|
711
|
+
if (index <= 0) return '/'
|
|
712
|
+
return `${stripped.slice(0, index)}/`
|
|
713
|
+
}
|
|
714
|
+
const index = normalized.lastIndexOf('/')
|
|
715
|
+
if (index <= 0) return '/'
|
|
716
|
+
return `${normalized.slice(0, index)}/`
|
|
717
|
+
}
|
|
718
|
+
const resolveHiddenAncestor = (routePath) => {
|
|
719
|
+
let cursor = resolveParentDirRoute(routePath)
|
|
720
|
+
while (cursor && cursor !== '/') {
|
|
721
|
+
const page = pagesByRoute.get(cursor)
|
|
722
|
+
if (page?.isIndex && page?.hidden) {
|
|
723
|
+
return cursor
|
|
713
724
|
}
|
|
725
|
+
cursor = resolveParentDirRoute(cursor)
|
|
714
726
|
}
|
|
727
|
+
return null
|
|
728
|
+
}
|
|
729
|
+
for (const page of pageList) {
|
|
730
|
+
const nearestHidden = resolveHiddenAncestor(page.routePath)
|
|
731
|
+
page.hiddenByParent = nearestHidden
|
|
732
|
+
page.hiddenByParents = Boolean(nearestHidden)
|
|
715
733
|
}
|
|
716
|
-
const pagesAll = pageList
|
|
717
734
|
const isSpecialPage = (page) => page?.routePath === '/404' || page?.routePath === '/offline'
|
|
718
735
|
const listForNavigation = pageList.filter(
|
|
719
736
|
(page) => !(isSpecialPage(page) && page.hidden !== false)
|
|
720
737
|
)
|
|
721
738
|
const routeExcludes = excludedRoutes || new Set()
|
|
722
739
|
const dirExcludes = excludedDirs || new Set()
|
|
723
|
-
const pagesByRoute = new Map()
|
|
724
|
-
for (const page of pageList) {
|
|
725
|
-
if (!pagesByRoute.has(page.routePath)) {
|
|
726
|
-
pagesByRoute.set(page.routePath, page)
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
740
|
const getPageByRoute = (routePath, options = {}) => {
|
|
730
741
|
const { path } = options || {}
|
|
731
742
|
if (path) {
|
|
@@ -869,10 +880,31 @@ export const createPagesContextFromPages = ({ pages, excludedRoutes, excludedDir
|
|
|
869
880
|
index = sequence.findIndex((entry) => entry.routePath === routePath)
|
|
870
881
|
}
|
|
871
882
|
if (index < 0) return { prev: null, next: null }
|
|
883
|
+
const normalizedRoutePath = normalizeRoutePath(routePath)
|
|
884
|
+
const isUnderRoute = (routeValue, baseValue) => {
|
|
885
|
+
if (!routeValue || !baseValue) return false
|
|
886
|
+
const route = normalizeRoutePath(routeValue)
|
|
887
|
+
const base = normalizeRoutePath(baseValue)
|
|
888
|
+
if (stripTrailingSlash(route) === stripTrailingSlash(base)) return true
|
|
889
|
+
const basePrefix = base.endsWith('/') ? base : `${base}/`
|
|
890
|
+
return route.startsWith(basePrefix)
|
|
891
|
+
}
|
|
872
892
|
const isVisible = (entry) => {
|
|
873
893
|
if (!entry) return false
|
|
874
|
-
if (entry.isRoot
|
|
875
|
-
|
|
894
|
+
if (entry.isRoot) {
|
|
895
|
+
if (!entry.hidden || routePath.startsWith(entry.routePath)) {
|
|
896
|
+
return true
|
|
897
|
+
}
|
|
898
|
+
return false
|
|
899
|
+
}
|
|
900
|
+
if (entry.hidden) {
|
|
901
|
+
return isUnderRoute(normalizedRoutePath, entry.routePath)
|
|
902
|
+
}
|
|
903
|
+
const entryHiddenRoot = entry.hiddenByParent
|
|
904
|
+
if (entryHiddenRoot && !isUnderRoute(normalizedRoutePath, entryHiddenRoot)) {
|
|
905
|
+
return false
|
|
906
|
+
}
|
|
907
|
+
return true
|
|
876
908
|
}
|
|
877
909
|
let prevIndex = index - 1
|
|
878
910
|
while (prevIndex >= 0 && !isVisible(sequence[prevIndex])) {
|
package/src/vite-plugins.js
CHANGED
|
@@ -145,8 +145,7 @@ const virtualModuleMap = {
|
|
|
145
145
|
},
|
|
146
146
|
get pages() {
|
|
147
147
|
const pages = state.PAGES_CONTEXT?.pages || []
|
|
148
|
-
const
|
|
149
|
-
return `export const pages = ${serializePagesIndex(filtered)}\nexport default pages`
|
|
148
|
+
return `export const pages = JSON.parse(${serializePagesIndex(pages)})\nexport default pages`
|
|
150
149
|
}
|
|
151
150
|
}
|
|
152
151
|
|
|
@@ -360,7 +360,7 @@ const handleRewrite = async (message) => {
|
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
-
|
|
363
|
+
const handleMessage = async (message) => {
|
|
364
364
|
const { type, stage } = message || {}
|
|
365
365
|
try {
|
|
366
366
|
await ensureInit()
|
|
@@ -397,4 +397,13 @@ parentPort?.on('message', async (message) => {
|
|
|
397
397
|
} catch (error) {
|
|
398
398
|
parentPort?.postMessage({ type: 'error', stage, error: serializeError(error) })
|
|
399
399
|
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let messageQueue = Promise.resolve()
|
|
403
|
+
parentPort?.on('message', (message) => {
|
|
404
|
+
messageQueue = messageQueue
|
|
405
|
+
.then(() => handleMessage(message))
|
|
406
|
+
.catch((error) => {
|
|
407
|
+
parentPort?.postMessage({ type: 'error', stage: message?.stage, error: serializeError(error) })
|
|
408
|
+
})
|
|
400
409
|
})
|
|
@@ -5,31 +5,51 @@ html {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
:root {
|
|
8
|
-
|
|
9
|
-
--
|
|
10
|
-
--text
|
|
11
|
-
--
|
|
12
|
-
--primary
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
8
|
+
color-scheme: light dark;
|
|
9
|
+
--bg: light-dark(#ffffff, #030712);
|
|
10
|
+
--text: light-dark(#111827, #f9fafb);
|
|
11
|
+
--text-muted: light-dark(#6b7280, #9ca3af);
|
|
12
|
+
--primary: light-dark(#2563eb, #60a5fa);
|
|
13
|
+
--primary-soft: light-dark(#eff6ff, #1e3a8a33);
|
|
14
|
+
--border: light-dark(#f3f4f6, #1f2937);
|
|
15
|
+
--bg-soft: light-dark(#f9fafb, #111827);
|
|
16
|
+
--selection-bg: light-dark(#dbeafe, #1e3a8a);
|
|
16
17
|
|
|
17
18
|
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
18
19
|
--max-width: 820px;
|
|
19
20
|
--container-px: 1.5rem;
|
|
20
|
-
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
/* Starry Night Syntax Highlighting */
|
|
23
|
+
--color-prettylights-syntax-brackethighlighter-angle: light-dark(#59636e, #9198a1);
|
|
24
|
+
--color-prettylights-syntax-brackethighlighter-unmatched: light-dark(#82071e, #f85149);
|
|
25
|
+
--color-prettylights-syntax-carriage-return-bg: light-dark(#cf222e, #b62324);
|
|
26
|
+
--color-prettylights-syntax-carriage-return-text: light-dark(#f6f8fa, #f0f6fc);
|
|
27
|
+
--color-prettylights-syntax-comment: light-dark(#59636e, #9198a1);
|
|
28
|
+
--color-prettylights-syntax-constant: light-dark(#0550ae, #79c0ff);
|
|
29
|
+
--color-prettylights-syntax-constant-other-reference-link: light-dark(#0a3069, #a5d6ff);
|
|
30
|
+
--color-prettylights-syntax-entity: light-dark(#6639ba, #d2a8ff);
|
|
31
|
+
--color-prettylights-syntax-entity-tag: light-dark(#0550ae, #7ee787);
|
|
32
|
+
--color-prettylights-syntax-invalid-illegal-bg: light-dark(#82071e, #8e1519);
|
|
33
|
+
--color-prettylights-syntax-invalid-illegal-text: light-dark(#f6f8fa, #f0f6fc);
|
|
34
|
+
--color-prettylights-syntax-keyword: light-dark(#cf222e, #ff7b72);
|
|
35
|
+
--color-prettylights-syntax-markup-changed-bg: light-dark(#ffd8b5, #5a1e02);
|
|
36
|
+
--color-prettylights-syntax-markup-changed-text: light-dark(#953800, #ffdfb6);
|
|
37
|
+
--color-prettylights-syntax-markup-deleted-bg: light-dark(#ffebe9, #67060c);
|
|
38
|
+
--color-prettylights-syntax-markup-deleted-text: light-dark(#82071e, #ffdcd7);
|
|
39
|
+
--color-prettylights-syntax-markup-heading: light-dark(#0550ae, #1f6feb);
|
|
40
|
+
--color-prettylights-syntax-markup-ignored-bg: light-dark(#0550ae, #1158c7);
|
|
41
|
+
--color-prettylights-syntax-markup-ignored-text: light-dark(#d1d9e0, #f0f6fc);
|
|
42
|
+
--color-prettylights-syntax-markup-inserted-bg: light-dark(#dafbe1, #033a16);
|
|
43
|
+
--color-prettylights-syntax-markup-inserted-text: light-dark(#116329, #aff5b4);
|
|
44
|
+
--color-prettylights-syntax-markup-list: light-dark(#3b2300, #f2cc60);
|
|
45
|
+
--color-prettylights-syntax-meta-diff-range: light-dark(#8250df, #d2a8ff);
|
|
46
|
+
--color-prettylights-syntax-string: light-dark(#0a3069, #a5d6ff);
|
|
47
|
+
--color-prettylights-syntax-string-regexp: light-dark(#116329, #7ee787);
|
|
48
|
+
--color-prettylights-syntax-sublimelinter-gutter-mark: light-dark(#818b98, #3d444d);
|
|
49
|
+
--color-prettylights-syntax-variable: light-dark(#953800, #ffa657);
|
|
50
|
+
--color-prettylights-syntax-markup-bold: light-dark(#1f2328, #f0f6fc);
|
|
51
|
+
--color-prettylights-syntax-markup-italic: light-dark(#1f2328, #f0f6fc);
|
|
52
|
+
--color-prettylights-syntax-storage-modifier-import: light-dark(#1f2328, #f0f6fc);
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
*, *::before, *::after {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { extractExcerpt } from 'methanol'
|
|
22
22
|
|
|
23
23
|
const isBlogPost = (page) => {
|
|
24
|
-
if (!page || page.hidden || page.
|
|
24
|
+
if (!page || page.hidden || page.hiddenByParents) return false
|
|
25
25
|
const href = page.routeHref
|
|
26
26
|
if (!href) return false
|
|
27
27
|
if (href === '/' || href === '/404' || href === '/offline') return false
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* under the License.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { If, For, $, signal, onCondition, read, extract } from 'refui'
|
|
21
|
+
import { If, For, $, signal, onCondition, read, extract, nextTick } from 'refui'
|
|
22
22
|
import NullProtoObj from 'null-prototype-object'
|
|
23
23
|
import { HTMLRenderer } from 'methanol'
|
|
24
24
|
|
|
@@ -50,21 +50,20 @@ const NavTree = ({ nodes, depth }) => {
|
|
|
50
50
|
<For entries={nodes}>
|
|
51
51
|
{({ item }) => {
|
|
52
52
|
const node = read(item)
|
|
53
|
-
const { routeHref: href, routePath, type, name, isRoot } = node
|
|
54
|
-
const { title
|
|
53
|
+
const { routeHref: href, routePath, type, name, isRoot, hidden } = node
|
|
54
|
+
const { title } = extract(item, 'title')
|
|
55
55
|
|
|
56
56
|
const isActive = matchCurrentPath(routePath)
|
|
57
57
|
let show = isActive
|
|
58
|
-
if (type === 'directory') {
|
|
58
|
+
if (isRoot || type === 'directory') {
|
|
59
59
|
show = $(() => {
|
|
60
|
-
const _active = isActive.value
|
|
61
60
|
const _currentPath = currentPath.value
|
|
62
|
-
return
|
|
61
|
+
return _currentPath.startsWith(routePath)
|
|
63
62
|
})
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
return (
|
|
67
|
-
<If condition={hidden
|
|
66
|
+
<If condition={!hidden || show}>
|
|
68
67
|
{() => {
|
|
69
68
|
if (type === 'directory') {
|
|
70
69
|
const label = title.or(name)
|
|
@@ -114,8 +113,9 @@ const _rootNodes = signal()
|
|
|
114
113
|
const rootNodes = signal(_rootNodes, (nodes) => nodes?.map(toSignal))
|
|
115
114
|
const rootTree = HTMLRenderer.createElement(NavTree, { nodes: rootNodes, depth: 0 })
|
|
116
115
|
|
|
117
|
-
export const renderNavTree = (nodes, path) => {
|
|
116
|
+
export const renderNavTree = async (nodes, path) => {
|
|
118
117
|
currentPath.value = path
|
|
119
118
|
_rootNodes.value = nodes
|
|
119
|
+
await nextTick()
|
|
120
120
|
return rootTree
|
|
121
121
|
}
|
|
@@ -22,7 +22,7 @@ import { DOCTYPE_HTML } from 'methanol'
|
|
|
22
22
|
import { renderToc } from '../components/ThemeToCContainer.static.jsx'
|
|
23
23
|
import { renderNavTree } from './nav-tree.jsx'
|
|
24
24
|
|
|
25
|
-
const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx }) => {
|
|
25
|
+
const PAGE_TEMPLATE = async ({ PageContent, ExtraHead, components, ctx }) => {
|
|
26
26
|
const page = ctx.page
|
|
27
27
|
const pagesByRoute = ctx.pagesByRoute
|
|
28
28
|
const pages = ctx.pages || []
|
|
@@ -204,7 +204,7 @@ const PAGE_TEMPLATE = ({ PageContent, ExtraHead, components, ctx }) => {
|
|
|
204
204
|
{pagefindEnabled ? <ThemeSearchBox options={pagefindOptions} /> : null}
|
|
205
205
|
</div>
|
|
206
206
|
<nav>
|
|
207
|
-
<ul data-depth="0">{renderNavTree(pagesTree, page.routePath)}</ul>
|
|
207
|
+
<ul data-depth="0">{await renderNavTree(pagesTree, page.routePath)}</ul>
|
|
208
208
|
</nav>
|
|
209
209
|
<div class="sidebar-footer">
|
|
210
210
|
{languageSelector}
|