@opnpress/opnpress-cli 0.2.3 → 0.3.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.
- package/dist/{build.js → cli/buildSite.js} +4 -4
- package/dist/{init.js → cli/initProject.js} +2 -2
- package/dist/{cli.js → cli/main.js} +2 -2
- package/dist/integrations/booking-calendar.js +23 -0
- package/dist/integrations/company-info.js +24 -0
- package/dist/integrations/contact-form.js +35 -0
- package/dist/integrations/contact-links.js +87 -0
- package/dist/integrations/index.js +18 -0
- package/dist/integrations/maps.js +23 -0
- package/dist/integrations/shareable-links.js +58 -0
- package/dist/integrations/socials-links.js +198 -0
- package/dist/integrations/video.js +74 -0
- package/dist/{linkEmbedding.js → renderer/aioHelper.js} +1 -16
- package/dist/renderer/footerHtmlBuilder.js +11 -0
- package/dist/renderer/headerHtmlBuilder.js +20 -0
- package/dist/renderer/mdBodyHtmlBuilder.js +117 -0
- package/dist/renderer/pageGenerator.js +204 -0
- package/dist/renderer/pageHelpers.js +132 -0
- package/dist/renderer/rawBodyHtmlBuilder.js +3 -0
- package/dist/{jsEmbedding.js → renderer/scriptBuilder.js} +14 -5
- package/dist/renderer/themeBuilder.js +723 -0
- package/dist/rendering/contact.js +149 -0
- package/dist/rendering/shared.js +75 -0
- package/dist/shortcodes/cardrow.js +85 -0
- package/dist/shortcodes/contact-card.js +24 -0
- package/dist/shortcodes/index.js +14 -0
- package/dist/shortcodes/js.js +7 -0
- package/dist/shortcodes/mailto.js +31 -0
- package/dist/shortcodes/pagelist.js +82 -0
- package/dist/shortcodes/tel.js +24 -0
- package/package.json +6 -6
- package/dist/render.js +0 -2041
- /package/dist/{server.js → cli/server.js} +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { escapeHtml } from '../utils.js';
|
|
2
|
+
export function buildHeaderHtml(params) {
|
|
3
|
+
const { siteName, homeHref, navigationHtml, discoveryHeaderNav, logoUrl } = params;
|
|
4
|
+
return `
|
|
5
|
+
<header class="site-header">
|
|
6
|
+
<a class="site-brand" href="${escapeHtml(homeHref)}">
|
|
7
|
+
${logoUrl ? `<img class="site-logo" src="${escapeHtml(logoUrl)}" alt="" aria-hidden="true" />` : ''}
|
|
8
|
+
<span>${escapeHtml(siteName)}</span>
|
|
9
|
+
</a>
|
|
10
|
+
<nav class="site-nav" aria-label="Main navigation">${navigationHtml}</nav>
|
|
11
|
+
${discoveryHeaderNav}
|
|
12
|
+
</header>
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
15
|
+
export function renderNavList(items) {
|
|
16
|
+
if (!items.length) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
return items.map((item) => `<a class="nav-link" href="${escapeHtml(item.url)}">${escapeHtml(item.title)}</a>`).join('');
|
|
20
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { unified } from 'unified';
|
|
4
|
+
import remarkParse from 'remark-parse';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
6
|
+
import remarkRehype from 'remark-rehype';
|
|
7
|
+
import rehypeRaw from 'rehype-raw';
|
|
8
|
+
import rehypeSlug from 'rehype-slug';
|
|
9
|
+
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|
10
|
+
import rehypeStringify from 'rehype-stringify';
|
|
11
|
+
import YAML from 'yaml';
|
|
12
|
+
import { escapeHtml } from '../utils.js';
|
|
13
|
+
import { shortcodeHandlers } from '../shortcodes/index.js';
|
|
14
|
+
import { integrationHandlers } from '../integrations/index.js';
|
|
15
|
+
export async function renderMarkdown(markdown) {
|
|
16
|
+
const file = await unified()
|
|
17
|
+
.use(remarkParse)
|
|
18
|
+
.use(remarkGfm)
|
|
19
|
+
.use(remarkRehype, { allowDangerousHtml: true })
|
|
20
|
+
.use(rehypeRaw)
|
|
21
|
+
.use(rehypeSlug)
|
|
22
|
+
.use(rehypeAutolinkHeadings, { behavior: 'append' })
|
|
23
|
+
.use(rehypeStringify, { allowDangerousHtml: true })
|
|
24
|
+
.process(markdown);
|
|
25
|
+
return String(file);
|
|
26
|
+
}
|
|
27
|
+
const blockHandlers = new Map([
|
|
28
|
+
...shortcodeHandlers,
|
|
29
|
+
...integrationHandlers
|
|
30
|
+
]);
|
|
31
|
+
function parseBlockConfig(body, handler) {
|
|
32
|
+
const trimmed = body.trim();
|
|
33
|
+
if (!trimmed) {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
if (handler?.parseConfig) {
|
|
37
|
+
return handler.parseConfig(body);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const parsed = YAML.parse(trimmed);
|
|
41
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Fall through to the empty config below.
|
|
47
|
+
}
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
async function renderSemanticBlock(params) {
|
|
51
|
+
const { blockName, body, site, title, canonicalUrl, source, allSources } = params;
|
|
52
|
+
const handler = blockHandlers.get(blockName);
|
|
53
|
+
if (!handler) {
|
|
54
|
+
return `
|
|
55
|
+
<section class="card integration integration-unknown">
|
|
56
|
+
<h2>${escapeHtml(blockName)}</h2>
|
|
57
|
+
<p class="empty-state">Unknown integration block: ${escapeHtml(blockName)}</p>
|
|
58
|
+
</section>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
const config = parseBlockConfig(body, handler);
|
|
62
|
+
return handler.render(config, {
|
|
63
|
+
site,
|
|
64
|
+
title,
|
|
65
|
+
canonicalUrl,
|
|
66
|
+
source,
|
|
67
|
+
allSources
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function expandSemanticBlocks(input, params) {
|
|
71
|
+
const blockPattern = /:::([a-z-]+)\n([\s\S]*?)\n:::/g;
|
|
72
|
+
let result = '';
|
|
73
|
+
let lastIndex = 0;
|
|
74
|
+
let match;
|
|
75
|
+
const blocks = [];
|
|
76
|
+
while ((match = blockPattern.exec(input))) {
|
|
77
|
+
result += input.slice(lastIndex, match.index);
|
|
78
|
+
const html = await renderSemanticBlock({
|
|
79
|
+
blockName: match[1],
|
|
80
|
+
body: match[2],
|
|
81
|
+
site: params.site,
|
|
82
|
+
title: params.title,
|
|
83
|
+
canonicalUrl: params.canonicalUrl,
|
|
84
|
+
source: params.source,
|
|
85
|
+
allSources: params.allSources
|
|
86
|
+
});
|
|
87
|
+
const token = `opnpress-block-${blocks.length + 1}`;
|
|
88
|
+
blocks.push({ token, html });
|
|
89
|
+
result += `\n\n<div data-opnpress-block="${token}"></div>\n\n`;
|
|
90
|
+
lastIndex = match.index + match[0].length;
|
|
91
|
+
}
|
|
92
|
+
result += input.slice(lastIndex);
|
|
93
|
+
return { source: result, blocks };
|
|
94
|
+
}
|
|
95
|
+
function injectSemanticBlocks(html, blocks) {
|
|
96
|
+
let result = html;
|
|
97
|
+
for (const block of blocks) {
|
|
98
|
+
const placeholder = `<div data-opnpress-block="${block.token}"></div>`;
|
|
99
|
+
result = result.replace(placeholder, block.html);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
export async function buildMarkdownBodyHtml(params) {
|
|
104
|
+
const { source, site, title, canonicalUrl, allSources } = params;
|
|
105
|
+
const semanticResult = await expandSemanticBlocks(source.body, {
|
|
106
|
+
site,
|
|
107
|
+
title,
|
|
108
|
+
canonicalUrl,
|
|
109
|
+
source,
|
|
110
|
+
allSources
|
|
111
|
+
});
|
|
112
|
+
return injectSemanticBlocks(await renderMarkdown(semanticResult.source), semanticResult.blocks);
|
|
113
|
+
}
|
|
114
|
+
export async function writeRenderedPage(outputPath, html) {
|
|
115
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
116
|
+
await fs.writeFile(outputPath, html, 'utf8');
|
|
117
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { escapeHtml, isFullHtmlDocument } from '../utils.js';
|
|
5
|
+
import { buildDiscoveryBodyMarkup, buildDiscoveryFooterMarkup, buildDiscoveryHeadMarkup, buildDiscoveryHeaderMarkup, buildSystemReminderMarkup } from './aioHelper.js';
|
|
6
|
+
import { buildCustomScriptTags } from './scriptBuilder.js';
|
|
7
|
+
import { buildThemeCss } from './themeBuilder.js';
|
|
8
|
+
import { buildHeaderHtml, renderNavList } from './headerHtmlBuilder.js';
|
|
9
|
+
import { buildFooterHtml } from './footerHtmlBuilder.js';
|
|
10
|
+
import { buildMarkdownBodyHtml } from './mdBodyHtmlBuilder.js';
|
|
11
|
+
import { buildRawBodyHtml } from './rawBodyHtmlBuilder.js';
|
|
12
|
+
import { analyticsSnippet, canonicalUrlForRoute, extractExcerpt, extractHeadings, hrefForAsset, hrefForRoute, hrefForSourceMirror, normalizeDateValue, pageClass, serializeJsonLd, socialMetaHandle, stripHtml, wordCount } from './pageHelpers.js';
|
|
13
|
+
import { buildPageTitle } from '../content.js';
|
|
14
|
+
function pageJsonToText(pageJson) {
|
|
15
|
+
return JSON.stringify(pageJson, null, 2) + '\n';
|
|
16
|
+
}
|
|
17
|
+
function sourceMirrorContent(source) {
|
|
18
|
+
if (source.kind === 'markdown') {
|
|
19
|
+
return {
|
|
20
|
+
path: 'source.md',
|
|
21
|
+
content: `---\n${YAML.stringify(source.frontmatter).trim()}\n---\n\n${source.body}\n`
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
path: 'source.html',
|
|
26
|
+
content: source.body
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function buildPageArtifact(params) {
|
|
30
|
+
const { source, site, theme, navigation, allPublishedSources, headerLogoUrl, customScriptUrls = [] } = params;
|
|
31
|
+
const title = buildPageTitle(source, site.site.name);
|
|
32
|
+
const description = source.frontmatter.description ?? site.site.description ?? '';
|
|
33
|
+
const canonicalUrl = source.frontmatter.canonical
|
|
34
|
+
? source.frontmatter.canonical
|
|
35
|
+
: canonicalUrlForRoute(site.site.domain, source.routePath);
|
|
36
|
+
const pageUrl = canonicalUrlForRoute(site.site.domain, source.routePath);
|
|
37
|
+
const bodyHtml = source.kind === 'markdown'
|
|
38
|
+
? await buildMarkdownBodyHtml({
|
|
39
|
+
source,
|
|
40
|
+
site,
|
|
41
|
+
title,
|
|
42
|
+
canonicalUrl,
|
|
43
|
+
allSources: allPublishedSources
|
|
44
|
+
})
|
|
45
|
+
: buildRawBodyHtml(source.body);
|
|
46
|
+
const content = `<section class="page-content">${bodyHtml}</section>`;
|
|
47
|
+
const navigationHtml = renderNavList(navigation.main.map((item) => ({
|
|
48
|
+
...item,
|
|
49
|
+
url: hrefForRoute(source.routePath, item.url)
|
|
50
|
+
})));
|
|
51
|
+
const footerNavHtml = renderNavList(navigation.footer.map((item) => ({
|
|
52
|
+
...item,
|
|
53
|
+
url: hrefForRoute(source.routePath, item.url)
|
|
54
|
+
})));
|
|
55
|
+
const customScriptTags = buildCustomScriptTags(customScriptUrls.map((scriptUrl) => hrefForAsset(source.routePath, scriptUrl)));
|
|
56
|
+
const isRawHtml = source.kind === 'html' && isFullHtmlDocument(source.body);
|
|
57
|
+
const ogImage = site.seo.defaultImage
|
|
58
|
+
? canonicalUrlForRoute(site.site.domain, site.seo.defaultImage)
|
|
59
|
+
: undefined;
|
|
60
|
+
const renderedBodyText = stripHtml(bodyHtml);
|
|
61
|
+
const headings = extractHeadings(bodyHtml);
|
|
62
|
+
const excerpt = description || extractExcerpt(renderedBodyText);
|
|
63
|
+
const tags = source.frontmatter.tags ?? [];
|
|
64
|
+
const categories = source.frontmatter.categories ?? [];
|
|
65
|
+
const date = normalizeDateValue(source.frontmatter.date);
|
|
66
|
+
const updated = normalizeDateValue(source.frontmatter.updated);
|
|
67
|
+
const jsonLd = {
|
|
68
|
+
'@context': 'https://schema.org',
|
|
69
|
+
'@type': 'WebPage',
|
|
70
|
+
name: title,
|
|
71
|
+
description,
|
|
72
|
+
url: canonicalUrl
|
|
73
|
+
};
|
|
74
|
+
const analyticsId = site.integrations.analytics.id ?? site.integrations.analytics.measurementId;
|
|
75
|
+
const analytics = analyticsId ? analyticsSnippet(analyticsId) : '';
|
|
76
|
+
const poweredBy = site.branding.poweredByOpnPress
|
|
77
|
+
? `<a class="nav-link" href="https://opnpress.com" rel="noopener noreferrer">Built with OpnPress</a>`
|
|
78
|
+
: '';
|
|
79
|
+
const logoUrl = headerLogoUrl ?? (site.site.logo ? hrefForAsset(source.routePath, site.site.logo) : '');
|
|
80
|
+
const faviconAsset = site.site.favicon ?? site.site.logo;
|
|
81
|
+
const faviconUrl = faviconAsset ? hrefForAsset(source.routePath, faviconAsset) : '';
|
|
82
|
+
const twitterSiteHandle = socialMetaHandle(site.socials.twitter ?? site.socials.x);
|
|
83
|
+
const pageJson = {
|
|
84
|
+
generator: 'OpnPress',
|
|
85
|
+
title,
|
|
86
|
+
description,
|
|
87
|
+
routePath: source.routePath,
|
|
88
|
+
url: pageUrl,
|
|
89
|
+
canonicalUrl,
|
|
90
|
+
section: source.section,
|
|
91
|
+
kind: source.kind,
|
|
92
|
+
published: true,
|
|
93
|
+
draft: false,
|
|
94
|
+
date,
|
|
95
|
+
updated,
|
|
96
|
+
image: source.frontmatter.image,
|
|
97
|
+
tags,
|
|
98
|
+
categories,
|
|
99
|
+
headings,
|
|
100
|
+
entities: [...tags, ...categories],
|
|
101
|
+
wordCount: wordCount(bodyHtml),
|
|
102
|
+
excerpt
|
|
103
|
+
};
|
|
104
|
+
const sourceMirrorUrl = source.kind === 'markdown'
|
|
105
|
+
? new URL('source.md', canonicalUrl).toString()
|
|
106
|
+
: new URL('source.html', canonicalUrl).toString();
|
|
107
|
+
const sourceMirrorRelativeUrl = source.kind === 'markdown'
|
|
108
|
+
? hrefForSourceMirror(source.routePath, source.routePath)
|
|
109
|
+
: hrefForSourceMirror(source.routePath, source.routePath, 'source.html');
|
|
110
|
+
const llmsRelativeUrl = hrefForAsset(source.routePath, '/llms.txt');
|
|
111
|
+
const fullSiteContentRelativeUrl = hrefForAsset(source.routePath, '/fullSiteContent.md');
|
|
112
|
+
const discoveryTargets = {
|
|
113
|
+
llmsRelativeUrl,
|
|
114
|
+
fullSiteContentRelativeUrl,
|
|
115
|
+
sourceMirrorRelativeUrl
|
|
116
|
+
};
|
|
117
|
+
const discoveryHead = buildDiscoveryHeadMarkup({
|
|
118
|
+
llmsRelativeUrl,
|
|
119
|
+
fullSiteContentRelativeUrl,
|
|
120
|
+
contentSource: source.kind === 'markdown' ? 'markdown' : 'html',
|
|
121
|
+
sourceMirrorRelativeUrl
|
|
122
|
+
});
|
|
123
|
+
const discoveryHeaderNav = buildDiscoveryHeaderMarkup(discoveryTargets);
|
|
124
|
+
const discoveryFooterNav = buildDiscoveryFooterMarkup(discoveryTargets);
|
|
125
|
+
const discoveryBodyMarkup = buildDiscoveryBodyMarkup(discoveryTargets);
|
|
126
|
+
if (isRawHtml) {
|
|
127
|
+
const bodyDiscoveryMarkup = `${customScriptTags}\n${discoveryBodyMarkup}\n${buildSystemReminderMarkup(fullSiteContentRelativeUrl)}`;
|
|
128
|
+
const wrappedHtml = source.body
|
|
129
|
+
.replace(/<\/head>/i, `${discoveryHead}\n</head>`)
|
|
130
|
+
.replace(/<\/body>/i, `${bodyDiscoveryMarkup}\n</body>`);
|
|
131
|
+
return {
|
|
132
|
+
html: isFullHtmlDocument(source.body) ? wrappedHtml : source.body,
|
|
133
|
+
pageJson,
|
|
134
|
+
sourceMirrorUrl,
|
|
135
|
+
sourceMirror: sourceMirrorContent(source)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
html: `
|
|
140
|
+
<!doctype html>
|
|
141
|
+
<html lang="${escapeHtml(site.site.language)}">
|
|
142
|
+
<head>
|
|
143
|
+
<!-- opnpress:generator=OpnPress -->
|
|
144
|
+
<meta charset="utf-8" />
|
|
145
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
146
|
+
${discoveryHead}
|
|
147
|
+
${faviconUrl ? `<link rel="icon" href="${escapeHtml(faviconUrl)}" type="image/png" />` : ''}
|
|
148
|
+
${faviconUrl ? `<link rel="apple-touch-icon" href="${escapeHtml(faviconUrl)}" />` : ''}
|
|
149
|
+
<title>${escapeHtml(title)}</title>
|
|
150
|
+
${description ? `<meta name="description" content="${escapeHtml(description)}" />` : ''}
|
|
151
|
+
<link rel="canonical" href="${escapeHtml(canonicalUrl)}" />
|
|
152
|
+
<meta property="og:type" content="article" />
|
|
153
|
+
<meta property="og:title" content="${escapeHtml(title)}" />
|
|
154
|
+
${description ? `<meta property="og:description" content="${escapeHtml(description)}" />` : ''}
|
|
155
|
+
<meta property="og:url" content="${escapeHtml(canonicalUrl)}" />
|
|
156
|
+
${ogImage ? `<meta property="og:image" content="${escapeHtml(ogImage)}" />` : ''}
|
|
157
|
+
<meta name="twitter:card" content="${ogImage ? 'summary_large_image' : 'summary'}" />
|
|
158
|
+
<meta name="twitter:title" content="${escapeHtml(title)}" />
|
|
159
|
+
${description ? `<meta name="twitter:description" content="${escapeHtml(description)}" />` : ''}
|
|
160
|
+
${twitterSiteHandle ? `<meta name="twitter:site" content="${escapeHtml(twitterSiteHandle)}" />` : ''}
|
|
161
|
+
${ogImage ? `<meta name="twitter:image" content="${escapeHtml(ogImage)}" />` : ''}
|
|
162
|
+
<script type="application/ld+json">${serializeJsonLd(jsonLd)}</script>
|
|
163
|
+
${analytics}
|
|
164
|
+
${customScriptTags}
|
|
165
|
+
<style>${buildThemeCss(theme)}</style>
|
|
166
|
+
</head>
|
|
167
|
+
<body>
|
|
168
|
+
<div class="site-shell">
|
|
169
|
+
${buildHeaderHtml({
|
|
170
|
+
siteName: site.site.name,
|
|
171
|
+
siteDescription: site.site.description,
|
|
172
|
+
homeHref: hrefForRoute(source.routePath, '/'),
|
|
173
|
+
navigationHtml,
|
|
174
|
+
discoveryHeaderNav,
|
|
175
|
+
logoUrl
|
|
176
|
+
})}
|
|
177
|
+
<main class="${pageClass()}">
|
|
178
|
+
${content}
|
|
179
|
+
${discoveryBodyMarkup}
|
|
180
|
+
</main>
|
|
181
|
+
${buildSystemReminderMarkup(fullSiteContentRelativeUrl)}
|
|
182
|
+
${buildFooterHtml({
|
|
183
|
+
siteDescription: site.site.description,
|
|
184
|
+
poweredBy,
|
|
185
|
+
footerHtml: footerNavHtml,
|
|
186
|
+
discoveryFooterNav
|
|
187
|
+
})}
|
|
188
|
+
</div>
|
|
189
|
+
</body>
|
|
190
|
+
</html>
|
|
191
|
+
`,
|
|
192
|
+
pageJson,
|
|
193
|
+
sourceMirrorUrl,
|
|
194
|
+
sourceMirror: sourceMirrorContent(source)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export async function renderPageDocument(params) {
|
|
198
|
+
const artifact = await buildPageArtifact(params);
|
|
199
|
+
return artifact.html;
|
|
200
|
+
}
|
|
201
|
+
export async function writeRenderedPage(outputPath, html) {
|
|
202
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
203
|
+
await fs.writeFile(outputPath, html, 'utf8');
|
|
204
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { escapeHtml } from '../utils.js';
|
|
3
|
+
function isExternalUrl(url) {
|
|
4
|
+
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url) || url.startsWith('//');
|
|
5
|
+
}
|
|
6
|
+
function normalizeRoutePath(routePath) {
|
|
7
|
+
if (routePath === '/') {
|
|
8
|
+
return '/';
|
|
9
|
+
}
|
|
10
|
+
return routePath.endsWith('/') ? routePath : `${routePath}/`;
|
|
11
|
+
}
|
|
12
|
+
export function hrefForRoute(currentRoute, targetRoute) {
|
|
13
|
+
if (isExternalUrl(targetRoute)) {
|
|
14
|
+
return targetRoute;
|
|
15
|
+
}
|
|
16
|
+
const current = normalizeRoutePath(currentRoute);
|
|
17
|
+
const target = normalizeRoutePath(targetRoute);
|
|
18
|
+
const fromDir = current === '/' ? '' : current.replace(/^\//, '');
|
|
19
|
+
const toDir = target === '/' ? '' : target.replace(/^\//, '');
|
|
20
|
+
const relative = path.posix.relative(fromDir, toDir);
|
|
21
|
+
if (!relative) {
|
|
22
|
+
return './';
|
|
23
|
+
}
|
|
24
|
+
return `${relative}/`;
|
|
25
|
+
}
|
|
26
|
+
export function hrefForAsset(currentRoute, assetPath) {
|
|
27
|
+
if (isExternalUrl(assetPath)) {
|
|
28
|
+
return assetPath;
|
|
29
|
+
}
|
|
30
|
+
const current = normalizeRoutePath(currentRoute);
|
|
31
|
+
const fromDir = current === '/' ? '' : current.replace(/^\//, '');
|
|
32
|
+
const toPath = assetPath.startsWith('/') ? assetPath.replace(/^\//, '') : assetPath;
|
|
33
|
+
const relative = path.posix.relative(fromDir, toPath);
|
|
34
|
+
return relative || path.posix.basename(toPath);
|
|
35
|
+
}
|
|
36
|
+
export function hrefForSourceMirror(currentRoute, targetRoute, fileName = 'source.md') {
|
|
37
|
+
const current = normalizeRoutePath(currentRoute);
|
|
38
|
+
const target = normalizeRoutePath(targetRoute);
|
|
39
|
+
const fromDir = current === '/' ? '' : current.replace(/^\//, '');
|
|
40
|
+
const toDir = target === '/' ? '' : target.replace(/^\//, '');
|
|
41
|
+
const relative = path.posix.relative(fromDir, path.posix.join(toDir, fileName));
|
|
42
|
+
return relative || fileName;
|
|
43
|
+
}
|
|
44
|
+
export function getSiteBaseUrl(domain) {
|
|
45
|
+
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(domain)) {
|
|
46
|
+
return domain.endsWith('/') ? domain : `${domain}/`;
|
|
47
|
+
}
|
|
48
|
+
return `https://${domain.replace(/\/+$/, '')}/`;
|
|
49
|
+
}
|
|
50
|
+
export function canonicalUrlForRoute(domain, routePath) {
|
|
51
|
+
const baseUrl = getSiteBaseUrl(domain);
|
|
52
|
+
return new URL(routePath === '/' ? '/' : routePath, baseUrl).toString();
|
|
53
|
+
}
|
|
54
|
+
export function serializeJsonLd(value) {
|
|
55
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
56
|
+
}
|
|
57
|
+
export function normalizeDateValue(value) {
|
|
58
|
+
if (value instanceof Date) {
|
|
59
|
+
return value.toISOString();
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === 'string' && value.trim()) {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
export function stripHtml(input) {
|
|
67
|
+
return input
|
|
68
|
+
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
69
|
+
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
70
|
+
.replace(/<[^>]+>/g, ' ')
|
|
71
|
+
.replace(/\s+/g, ' ')
|
|
72
|
+
.trim();
|
|
73
|
+
}
|
|
74
|
+
export function extractHeadings(html) {
|
|
75
|
+
const headings = [];
|
|
76
|
+
const pattern = /<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi;
|
|
77
|
+
let match;
|
|
78
|
+
while ((match = pattern.exec(html))) {
|
|
79
|
+
headings.push(stripHtml(match[2]));
|
|
80
|
+
}
|
|
81
|
+
return headings;
|
|
82
|
+
}
|
|
83
|
+
export function extractExcerpt(text, maxLength = 180) {
|
|
84
|
+
const stripped = stripHtml(text);
|
|
85
|
+
if (stripped.length <= maxLength) {
|
|
86
|
+
return stripped;
|
|
87
|
+
}
|
|
88
|
+
return `${stripped.slice(0, maxLength).trimEnd()}…`;
|
|
89
|
+
}
|
|
90
|
+
export function wordCount(text) {
|
|
91
|
+
const stripped = stripHtml(text);
|
|
92
|
+
if (!stripped) {
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
return stripped.split(/\s+/).filter(Boolean).length;
|
|
96
|
+
}
|
|
97
|
+
export function pageClass() {
|
|
98
|
+
return 'page page-default';
|
|
99
|
+
}
|
|
100
|
+
export function analyticsSnippet(analyticsId) {
|
|
101
|
+
const escapedId = escapeHtml(analyticsId);
|
|
102
|
+
return `
|
|
103
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedId}"></script>
|
|
104
|
+
<script>
|
|
105
|
+
window.dataLayer = window.dataLayer || [];
|
|
106
|
+
function gtag(){dataLayer.push(arguments);}
|
|
107
|
+
gtag('js', new Date());
|
|
108
|
+
gtag('config', '${escapedId}');
|
|
109
|
+
</script>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
112
|
+
export function socialMetaHandle(value) {
|
|
113
|
+
if (!value || !value.trim()) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const trimmed = value.trim();
|
|
117
|
+
if (trimmed.startsWith('@')) {
|
|
118
|
+
return trimmed;
|
|
119
|
+
}
|
|
120
|
+
if (isExternalUrl(trimmed)) {
|
|
121
|
+
try {
|
|
122
|
+
const url = new URL(trimmed);
|
|
123
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
124
|
+
const lastSegment = segments[segments.length - 1];
|
|
125
|
+
return lastSegment ? `@${lastSegment.replace(/^@+/, '')}` : trimmed;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return trimmed;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return `@${trimmed.replace(/^@+/, '')}`;
|
|
132
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml } from '
|
|
1
|
+
import { escapeHtml } from '../utils.js';
|
|
2
2
|
export function buildCustomScriptTags(scriptUrls) {
|
|
3
3
|
const externalScripts = scriptUrls
|
|
4
4
|
.map((scriptUrl) => `<script src="${escapeHtml(scriptUrl)}" defer></script>`)
|
|
@@ -7,14 +7,23 @@ export function buildCustomScriptTags(scriptUrls) {
|
|
|
7
7
|
<script defer>
|
|
8
8
|
document.addEventListener('DOMContentLoaded', () => {
|
|
9
9
|
document.querySelectorAll('.full-content-link').forEach(element => {
|
|
10
|
-
element.style.
|
|
10
|
+
element.style.width = '1px';
|
|
11
|
+
element.style.height = '1px';
|
|
12
|
+
element.style.minWidth = '1px';
|
|
13
|
+
element.style.minHeight = '1px';
|
|
14
|
+
element.style.margin = '0';
|
|
15
|
+
element.style.padding = '0';
|
|
16
|
+
element.style.border = '0';
|
|
17
|
+
element.style.overflow = 'hidden';
|
|
18
|
+
element.style.position = 'absolute';
|
|
19
|
+
element.style.top = '0';
|
|
20
|
+
element.style.left = '0';
|
|
21
|
+
element.style.pointerEvents = 'none';
|
|
11
22
|
});
|
|
12
23
|
});
|
|
13
24
|
</script>`
|
|
14
25
|
.trim();
|
|
15
|
-
return [externalScripts, fullContentLinkScript]
|
|
16
|
-
.filter(Boolean)
|
|
17
|
-
.join('\n');
|
|
26
|
+
return [externalScripts, fullContentLinkScript].filter(Boolean).join('\n');
|
|
18
27
|
}
|
|
19
28
|
export function buildInlineScriptShortcodeMarkup(body) {
|
|
20
29
|
const trimmed = body.replace(/^\n+|\n+$/g, '');
|