@opnpress/opnpress-cli 0.2.4 → 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 -1
- 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} +2 -4
- 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,149 @@
|
|
|
1
|
+
import { escapeHtml } from '../utils.js';
|
|
2
|
+
import { hrefForAsset, isExternalUrl } from './shared.js';
|
|
3
|
+
export function normalizeContactHref(value, kind) {
|
|
4
|
+
const trimmed = value.trim();
|
|
5
|
+
if (!trimmed) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
if (isExternalUrl(trimmed) || trimmed.startsWith('mailto:') || trimmed.startsWith('tel:')) {
|
|
9
|
+
return trimmed;
|
|
10
|
+
}
|
|
11
|
+
switch (kind) {
|
|
12
|
+
case 'email':
|
|
13
|
+
return `mailto:${trimmed.replace(/^mailto:/i, '')}`;
|
|
14
|
+
case 'phone':
|
|
15
|
+
return `tel:${trimmed.replace(/^tel:/i, '').replace(/[^\d+*#.,]/g, '')}`;
|
|
16
|
+
case 'website':
|
|
17
|
+
if (trimmed.startsWith('/') || trimmed.startsWith('.') || trimmed.startsWith('#')) {
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
return isExternalUrl(trimmed) ? trimmed : `https://${trimmed}`;
|
|
21
|
+
default:
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function renderContactCardMarkup(params) {
|
|
26
|
+
const { title, description, sectionStyle, currentRoute, className, info } = params;
|
|
27
|
+
const name = info.name?.trim() ?? '';
|
|
28
|
+
const tagline = info.tagline?.trim() ?? '';
|
|
29
|
+
const logo = info.logo?.trim() ?? '';
|
|
30
|
+
const address = info.address?.trim() ?? '';
|
|
31
|
+
const mapUrl = info.mapUrl?.trim() ?? '';
|
|
32
|
+
const phone = info.phone?.trim() ?? '';
|
|
33
|
+
const email = info.email?.trim() ?? '';
|
|
34
|
+
const hours = info.hours?.trim() ?? '';
|
|
35
|
+
const website = info.website?.trim() ?? '';
|
|
36
|
+
const mapHref = mapUrl ? normalizeContactHref(mapUrl, 'website') : '';
|
|
37
|
+
const websiteHref = website ? normalizeContactHref(website, 'website') : '';
|
|
38
|
+
const emailHref = email ? normalizeContactHref(email, 'email') : '';
|
|
39
|
+
const phoneHref = phone ? normalizeContactHref(phone, 'phone') : '';
|
|
40
|
+
const websiteLabel = websiteHref ? contactWebsiteDisplayLabel(websiteHref) : '';
|
|
41
|
+
const mapEmbedUrl = buildMapEmbedUrl(mapUrl, address);
|
|
42
|
+
const logoUrl = logo ? (isExternalUrl(logo) || logo.startsWith('data:') ? logo : hrefForAsset(currentRoute, logo)) : '';
|
|
43
|
+
const mapPane = mapEmbedUrl ? `
|
|
44
|
+
<div class="${className}-map-pane">
|
|
45
|
+
<div class="embed-frame embed-frame--map contact-card-map-frame">
|
|
46
|
+
<iframe src="${escapeHtml(mapEmbedUrl)}" title="${escapeHtml(name || address || 'Map')}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" allowfullscreen></iframe>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
` : '';
|
|
50
|
+
return `
|
|
51
|
+
<section class="card ${className}"${sectionStyle}>
|
|
52
|
+
${title ? `<p class="eyebrow">${escapeHtml(title)}</p>` : ''}
|
|
53
|
+
<div class="${className}-layout ${mapPane ? `${className}-layout-with-map` : `${className}-layout-no-map`}">
|
|
54
|
+
<div class="${className}-main">
|
|
55
|
+
<div class="${className}-header">
|
|
56
|
+
${logoUrl ? `<img class="${className}-logo" src="${escapeHtml(logoUrl)}" alt="" aria-hidden="true" loading="lazy" decoding="async" />` : ''}
|
|
57
|
+
${name ? (websiteHref
|
|
58
|
+
? `<h2 class="${className}-name"><a class="${className}-name-link" href="${escapeHtml(websiteHref)}" rel="noopener noreferrer" target="${websiteHref.startsWith('http') ? '_blank' : '_self'}">${escapeHtml(name)}</a></h2>`
|
|
59
|
+
: `<h2 class="${className}-name">${escapeHtml(name)}</h2>`) : ''}
|
|
60
|
+
</div>
|
|
61
|
+
${tagline ? `<p class="${className}-tagline">${escapeHtml(tagline)}</p>` : ''}
|
|
62
|
+
${description ? `<p class="section-description">${escapeHtml(description)}</p>` : ''}
|
|
63
|
+
${mapHref ? `
|
|
64
|
+
<div class="${className}-row ${className}-map-row">
|
|
65
|
+
<a class="contact-card-map-link" href="${escapeHtml(mapHref)}" rel="noopener noreferrer" target="${mapHref.startsWith('http') ? '_blank' : '_self'}">
|
|
66
|
+
<span class="contact-card-map-icon" aria-hidden="true">${contactLinkIconMarkup('map')}</span>
|
|
67
|
+
<span class="contact-card-map-text">${escapeHtml(address || mapUrl)}</span>
|
|
68
|
+
</a>
|
|
69
|
+
</div>
|
|
70
|
+
` : ''}
|
|
71
|
+
${!mapHref && address ? `<div class="${className}-row ${className}-address-row"><span class="contact-card-text">${escapeHtml(address)}</span></div>` : ''}
|
|
72
|
+
${hours ? `<div class="${className}-row ${className}-hours-row"><span class="contact-card-text">${escapeHtml(hours)}</span></div>` : ''}
|
|
73
|
+
${websiteHref ? `<div class="${className}-row ${className}-website-row"><a class="contact-card-link" href="${escapeHtml(websiteHref)}" rel="noopener noreferrer" target="${websiteHref.startsWith('http') ? '_blank' : '_self'}">${escapeHtml(websiteLabel)}</a></div>` : ''}
|
|
74
|
+
${(emailHref || phoneHref) ? `
|
|
75
|
+
<div class="${className}-row ${className}-contact-row">
|
|
76
|
+
${emailHref ? `<a class="contact-card-link" href="${escapeHtml(emailHref)}">${escapeHtml(email)}</a>` : ''}
|
|
77
|
+
${emailHref && phoneHref ? `<span class="contact-card-separator" aria-hidden="true">|</span>` : ''}
|
|
78
|
+
${phoneHref ? `<a class="contact-card-link" href="${escapeHtml(phoneHref)}">${escapeHtml(phone)}</a>` : ''}
|
|
79
|
+
</div>
|
|
80
|
+
` : ''}
|
|
81
|
+
</div>
|
|
82
|
+
${mapPane}
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
export function renderSingleLinkShortcode(params) {
|
|
88
|
+
const { title, description, label, href, sectionStyle, className, iconLabel, showIcon = true } = params;
|
|
89
|
+
return `
|
|
90
|
+
<section class="card ${className}"${sectionStyle}>
|
|
91
|
+
${title ? `<h2>${escapeHtml(title)}</h2>` : ''}
|
|
92
|
+
${description ? `<p>${escapeHtml(description)}</p>` : ''}
|
|
93
|
+
<a class="share-link ${className}-link" href="${escapeHtml(href)}" rel="noopener noreferrer"${href.startsWith('http') ? ' target="_blank"' : ''}>
|
|
94
|
+
${showIcon && iconLabel ? `<span class="contact-link-icon-wrap">${contactLinkIconMarkup(iconLabel)}</span>` : ''}
|
|
95
|
+
<span class="${className}-label">${escapeHtml(label)}</span>
|
|
96
|
+
</a>
|
|
97
|
+
</section>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
function contactWebsiteDisplayLabel(href) {
|
|
101
|
+
return href.startsWith('http://') || href.startsWith('https://') ? href : 'Goto Page';
|
|
102
|
+
}
|
|
103
|
+
function contactLinkIconMarkup(kind) {
|
|
104
|
+
switch (kind) {
|
|
105
|
+
case 'email':
|
|
106
|
+
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm0 4.236-8 5-8-5V6l8 5 8-5v2.236Z"/></svg>`;
|
|
107
|
+
case 'phone':
|
|
108
|
+
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M6.62 10.79a15.5 15.5 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.24c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.31 21 3 13.69 3 4a1 1 0 0 1 1-1h2.49a1 1 0 0 1 1 1c0 1.24.2 2.45.57 3.57a1 1 0 0 1-.24 1.02l-2.2 2.2Z"/></svg>`;
|
|
109
|
+
case 'website':
|
|
110
|
+
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm7.9 9h-3.17a15.7 15.7 0 0 0-1.16-5.2A8.02 8.02 0 0 1 19.9 11Zm-1.8 2a8.03 8.03 0 0 1-4.33 4.2c.44-1.3.73-2.75.84-4.2h3.5ZM12 20c-.82 0-2.22-2.05-2.8-6h5.6c-.58 3.95-1.98 6-2.8 6Zm-2.8-8c.11-1.45.4-2.9.84-4.2A8.03 8.03 0 0 0 5.7 12h3.5ZM12 4c.82 0 2.22 2.05 2.8 6H9.2c.58-3.95 1.98-6 2.8-6Zm-3.77 1.8A15.7 15.7 0 0 0 7.07 11H3.9a8.02 8.02 0 0 1 4.33-5.2ZM3.9 13h3.17a15.7 15.7 0 0 0 1.16 5.2A8.02 8.02 0 0 1 3.9 13Zm12.77 5.2c.44-1.3.73-2.75.84-4.2h3.17a8.02 8.02 0 0 1-4.01 4.2Z"/></svg>`;
|
|
111
|
+
case 'map':
|
|
112
|
+
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2C8.14 2 5 5.14 5 9c0 4.86 7 13 7 13s7-8.14 7-13c0-3.86-3.14-7-7-7Zm0 9.5A2.5 2.5 0 1 1 12 6a2.5 2.5 0 0 1 0 5.5Z"/></svg>`;
|
|
113
|
+
case 'address':
|
|
114
|
+
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 2.3L18 7l-6 3.7L6 7l6-2.7ZM5 8.6l6 3.7v7.6l-6-3.3V8.6Zm14 0v7.9l-6 3.2v-7.6l6-3.5Z"/></svg>`;
|
|
115
|
+
case 'hours':
|
|
116
|
+
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 10.41V7h-2v6l5 3 .99-1.63L13 12.41Z"/></svg>`;
|
|
117
|
+
default:
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function buildMapEmbedUrl(mapUrl, address) {
|
|
122
|
+
const rawMapUrl = mapUrl?.trim() ?? '';
|
|
123
|
+
const rawAddress = address?.trim() ?? '';
|
|
124
|
+
const source = rawMapUrl || rawAddress;
|
|
125
|
+
if (!source) {
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
if (isExternalUrl(source) && /\/embed\b/i.test(source)) {
|
|
129
|
+
return source;
|
|
130
|
+
}
|
|
131
|
+
if (isExternalUrl(source)) {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = new URL(source);
|
|
134
|
+
if (/google\./i.test(parsed.hostname)) {
|
|
135
|
+
const query = parsed.searchParams.get('q') || parsed.searchParams.get('query') || parsed.searchParams.get('destination') || rawAddress || source;
|
|
136
|
+
return `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`;
|
|
137
|
+
}
|
|
138
|
+
if (/maps\.apple\.com/i.test(parsed.hostname)) {
|
|
139
|
+
return source.includes('output=embed') ? source : `${source}${source.includes('?') ? '&' : '?'}output=embed`;
|
|
140
|
+
}
|
|
141
|
+
return source;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return source;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const query = rawAddress || source;
|
|
148
|
+
return `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`;
|
|
149
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { escapeHtml } from '../utils.js';
|
|
3
|
+
export function isExternalUrl(url) {
|
|
4
|
+
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url) || url.startsWith('//');
|
|
5
|
+
}
|
|
6
|
+
export 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
|
+
return relative ? `${relative}/` : './';
|
|
22
|
+
}
|
|
23
|
+
export function hrefForAsset(currentRoute, assetPath) {
|
|
24
|
+
if (isExternalUrl(assetPath)) {
|
|
25
|
+
return assetPath;
|
|
26
|
+
}
|
|
27
|
+
const current = normalizeRoutePath(currentRoute);
|
|
28
|
+
const fromDir = current === '/' ? '' : current.replace(/^\//, '');
|
|
29
|
+
const toPath = assetPath.startsWith('/') ? assetPath.replace(/^\//, '') : assetPath;
|
|
30
|
+
const relative = path.posix.relative(fromDir, toPath);
|
|
31
|
+
return relative || path.posix.basename(toPath);
|
|
32
|
+
}
|
|
33
|
+
export function hrefForSourceMirror(currentRoute, targetRoute, fileName = 'source.md') {
|
|
34
|
+
const current = normalizeRoutePath(currentRoute);
|
|
35
|
+
const target = normalizeRoutePath(targetRoute);
|
|
36
|
+
const fromDir = current === '/' ? '' : current.replace(/^\//, '');
|
|
37
|
+
const toDir = target === '/' ? '' : target.replace(/^\//, '');
|
|
38
|
+
const relative = path.posix.relative(fromDir, path.posix.join(toDir, fileName));
|
|
39
|
+
return relative || fileName;
|
|
40
|
+
}
|
|
41
|
+
function normalizeShortcodeText(value) {
|
|
42
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
43
|
+
}
|
|
44
|
+
export function sectionStyleFromConfig(config, currentRoute) {
|
|
45
|
+
const parts = [];
|
|
46
|
+
const backgroundImage = normalizeShortcodeText(config.backgroundImage);
|
|
47
|
+
const backgroundPosition = normalizeShortcodeText(config.backgroundPosition);
|
|
48
|
+
const backgroundSize = normalizeShortcodeText(config.backgroundSize);
|
|
49
|
+
const backgroundRepeat = normalizeShortcodeText(config.backgroundRepeat);
|
|
50
|
+
const backgroundColor = normalizeShortcodeText(config.backgroundColor);
|
|
51
|
+
const color = normalizeShortcodeText(config.color);
|
|
52
|
+
const minHeight = normalizeShortcodeText(config.minHeight);
|
|
53
|
+
if (backgroundImage) {
|
|
54
|
+
const resolved = isExternalUrl(backgroundImage) || backgroundImage.startsWith('data:')
|
|
55
|
+
? backgroundImage
|
|
56
|
+
: hrefForAsset(currentRoute, backgroundImage);
|
|
57
|
+
parts.push(`background-image:url("${escapeHtml(resolved)}")`);
|
|
58
|
+
parts.push('background-position:center');
|
|
59
|
+
parts.push('background-size:cover');
|
|
60
|
+
parts.push('background-repeat:no-repeat');
|
|
61
|
+
}
|
|
62
|
+
if (backgroundPosition)
|
|
63
|
+
parts.push(`background-position:${escapeHtml(backgroundPosition)}`);
|
|
64
|
+
if (backgroundSize)
|
|
65
|
+
parts.push(`background-size:${escapeHtml(backgroundSize)}`);
|
|
66
|
+
if (backgroundRepeat)
|
|
67
|
+
parts.push(`background-repeat:${escapeHtml(backgroundRepeat)}`);
|
|
68
|
+
if (backgroundColor)
|
|
69
|
+
parts.push(`background-color:${escapeHtml(backgroundColor)}`);
|
|
70
|
+
if (color)
|
|
71
|
+
parts.push(`color:${escapeHtml(color)}`);
|
|
72
|
+
if (minHeight)
|
|
73
|
+
parts.push(`min-height:${escapeHtml(minHeight)}`);
|
|
74
|
+
return parts.length ? ` style="${parts.join(';')}"` : '';
|
|
75
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { escapeHtml } from '../utils.js';
|
|
3
|
+
import { hrefForRoute, hrefForSourceMirror, normalizeRoutePath, sectionStyleFromConfig } from '../rendering/shared.js';
|
|
4
|
+
const CARD_PRESETS = {
|
|
5
|
+
pages: {
|
|
6
|
+
title: 'Pages',
|
|
7
|
+
href: '/about/',
|
|
8
|
+
body: 'Create markdown pages with YAML frontmatter.'
|
|
9
|
+
},
|
|
10
|
+
integrations: {
|
|
11
|
+
title: 'Integrations',
|
|
12
|
+
href: '/integrations/',
|
|
13
|
+
body: 'Render semantic blocks for forms, social links, maps, video, and calendars.'
|
|
14
|
+
},
|
|
15
|
+
services: {
|
|
16
|
+
title: 'Services',
|
|
17
|
+
href: '/services/',
|
|
18
|
+
body: 'Create service pages with the same markdown-first workflow.'
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function renderCardRowMarkup(config, currentRoute) {
|
|
22
|
+
const rawCards = Array.isArray(config.cards) && config.cards.length ? config.cards : ['pages', 'integrations', 'services'];
|
|
23
|
+
const cards = rawCards.map((entry) => {
|
|
24
|
+
if (typeof entry === 'string') {
|
|
25
|
+
return CARD_PRESETS[entry] ?? {
|
|
26
|
+
title: entry,
|
|
27
|
+
href: '#',
|
|
28
|
+
body: ''
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (entry && typeof entry === 'object') {
|
|
32
|
+
const card = entry;
|
|
33
|
+
const presetName = typeof card.preset === 'string' ? card.preset : undefined;
|
|
34
|
+
const preset = presetName ? CARD_PRESETS[presetName] : undefined;
|
|
35
|
+
return {
|
|
36
|
+
title: typeof card.title === 'string' ? card.title : preset?.title ?? 'Card',
|
|
37
|
+
href: typeof card.href === 'string' ? card.href : preset?.href ?? '#',
|
|
38
|
+
body: typeof card.body === 'string' ? card.body : preset?.body ?? ''
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
title: 'Card',
|
|
43
|
+
href: '#',
|
|
44
|
+
body: ''
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
const title = typeof config.title === 'string' ? config.title : '';
|
|
48
|
+
const description = typeof config.description === 'string' ? config.description : '';
|
|
49
|
+
const sectionStyle = sectionStyleFromConfig(config, currentRoute);
|
|
50
|
+
const currentRoutePath = normalizeRoutePath(currentRoute);
|
|
51
|
+
return `
|
|
52
|
+
<section class="cardrow-section"${sectionStyle}>
|
|
53
|
+
${title ? `<h2 class="section-title">${escapeHtml(title)}</h2>` : ''}
|
|
54
|
+
${description ? `<p class="section-description">${escapeHtml(description)}</p>` : ''}
|
|
55
|
+
<div class="grid cardrow">
|
|
56
|
+
${cards
|
|
57
|
+
.map((card) => {
|
|
58
|
+
const isExplicitSourceLink = card.href.endsWith('/source.md') || card.href.endsWith('/source.html');
|
|
59
|
+
const targetRoute = card.href.startsWith('http') || card.href.startsWith('#')
|
|
60
|
+
? card.href
|
|
61
|
+
: isExplicitSourceLink
|
|
62
|
+
? path.posix.join(currentRoutePath, card.href).replace(/\/source\.(md|html)$/i, '')
|
|
63
|
+
: path.posix.join(currentRoutePath, card.href);
|
|
64
|
+
const resolvedHref = card.href.startsWith('http') || card.href.startsWith('#')
|
|
65
|
+
? targetRoute
|
|
66
|
+
: isExplicitSourceLink
|
|
67
|
+
? hrefForSourceMirror(currentRoute, targetRoute)
|
|
68
|
+
: hrefForRoute(currentRoute, targetRoute);
|
|
69
|
+
return `
|
|
70
|
+
<article class="card">
|
|
71
|
+
<h3><a href="${escapeHtml(resolvedHref)}">${escapeHtml(card.title)}</a></h3>
|
|
72
|
+
${card.body ? `<p>${escapeHtml(card.body)}</p>` : ''}
|
|
73
|
+
</article>
|
|
74
|
+
`;
|
|
75
|
+
})
|
|
76
|
+
.join('')}
|
|
77
|
+
</div>
|
|
78
|
+
</section>
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
const handler = {
|
|
82
|
+
getTrigger: () => 'cardrow',
|
|
83
|
+
render: (config, { source }) => renderCardRowMarkup(config, source.routePath)
|
|
84
|
+
};
|
|
85
|
+
export default handler;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { sectionStyleFromConfig } from '../rendering/shared.js';
|
|
2
|
+
import { renderContactCardMarkup } from '../rendering/contact.js';
|
|
3
|
+
const handler = {
|
|
4
|
+
getTrigger: () => 'contact-card',
|
|
5
|
+
render: (config, { source }) => renderContactCardMarkup({
|
|
6
|
+
title: typeof config.title === 'string' ? config.title : '',
|
|
7
|
+
description: typeof config.description === 'string' ? config.description : '',
|
|
8
|
+
sectionStyle: sectionStyleFromConfig(config, source.routePath),
|
|
9
|
+
currentRoute: source.routePath,
|
|
10
|
+
className: 'contact-card',
|
|
11
|
+
info: {
|
|
12
|
+
name: typeof config.name === 'string' ? config.name : '',
|
|
13
|
+
tagline: typeof config.tagline === 'string' ? config.tagline : '',
|
|
14
|
+
logo: typeof config.logo === 'string' ? config.logo : '',
|
|
15
|
+
address: typeof config.address === 'string' ? config.address : '',
|
|
16
|
+
mapUrl: typeof config.mapUrl === 'string' ? config.mapUrl : '',
|
|
17
|
+
phone: typeof config.phone === 'string' ? config.phone : '',
|
|
18
|
+
email: typeof config.email === 'string' ? config.email : '',
|
|
19
|
+
hours: typeof config.hours === 'string' ? config.hours : '',
|
|
20
|
+
website: typeof config.website === 'string' ? config.website : ''
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
};
|
|
24
|
+
export default handler;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import cardrow from './cardrow.js';
|
|
2
|
+
import pagelist from './pagelist.js';
|
|
3
|
+
import contactCard from './contact-card.js';
|
|
4
|
+
import mailto from './mailto.js';
|
|
5
|
+
import tel from './tel.js';
|
|
6
|
+
import js from './js.js';
|
|
7
|
+
export const shortcodeHandlers = new Map([
|
|
8
|
+
[cardrow.getTrigger(), cardrow],
|
|
9
|
+
[pagelist.getTrigger(), pagelist],
|
|
10
|
+
[contactCard.getTrigger(), contactCard],
|
|
11
|
+
[mailto.getTrigger(), mailto],
|
|
12
|
+
[tel.getTrigger(), tel],
|
|
13
|
+
[js.getTrigger(), js]
|
|
14
|
+
]);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { sectionStyleFromConfig } from '../rendering/shared.js';
|
|
2
|
+
import { normalizeContactHref, renderSingleLinkShortcode } from '../rendering/contact.js';
|
|
3
|
+
const handler = {
|
|
4
|
+
getTrigger: () => 'mailto',
|
|
5
|
+
render: (config, { source }) => {
|
|
6
|
+
const email = typeof config.email === 'string' ? config.email : '';
|
|
7
|
+
if (!email) {
|
|
8
|
+
throw new Error('mailto block requires an email parameter in the shortcode body');
|
|
9
|
+
}
|
|
10
|
+
const subject = typeof config.subject === 'string' ? config.subject : '';
|
|
11
|
+
const bodyText = typeof config.body === 'string' ? config.body : '';
|
|
12
|
+
const label = typeof config.label === 'string' ? config.label : email;
|
|
13
|
+
const query = new URLSearchParams();
|
|
14
|
+
if (subject)
|
|
15
|
+
query.set('subject', subject);
|
|
16
|
+
if (bodyText)
|
|
17
|
+
query.set('body', bodyText);
|
|
18
|
+
const href = `mailto:${email}${query.toString() ? `?${query.toString()}` : ''}`;
|
|
19
|
+
return renderSingleLinkShortcode({
|
|
20
|
+
title: typeof config.title === 'string' ? config.title : '',
|
|
21
|
+
description: typeof config.description === 'string' ? config.description : '',
|
|
22
|
+
label,
|
|
23
|
+
href: normalizeContactHref(href, 'email'),
|
|
24
|
+
sectionStyle: sectionStyleFromConfig(config, source.routePath),
|
|
25
|
+
className: 'shortcode-mailto',
|
|
26
|
+
iconLabel: 'email',
|
|
27
|
+
showIcon: typeof config.showIcon === 'boolean' ? config.showIcon : true
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
export default handler;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { escapeHtml } from '../utils.js';
|
|
2
|
+
import { buildPageTitle } from '../content.js';
|
|
3
|
+
import { hrefForRoute } from '../rendering/shared.js';
|
|
4
|
+
function normalizeRoutePrefix(folder) {
|
|
5
|
+
if (!folder) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const cleaned = folder.trim().replace(/^\/+|\/+$/g, '');
|
|
9
|
+
if (!cleaned) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return `/${cleaned}/`;
|
|
13
|
+
}
|
|
14
|
+
function normalizeDateValueFromSource(source) {
|
|
15
|
+
const value = source.frontmatter.date ?? source.frontmatter.updated ?? '';
|
|
16
|
+
if (value instanceof Date) {
|
|
17
|
+
return value.toISOString();
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
function sortSourcesByDate(sources, order) {
|
|
25
|
+
const sorted = [...sources].sort((left, right) => {
|
|
26
|
+
const leftValue = normalizeDateValueFromSource(left);
|
|
27
|
+
const rightValue = normalizeDateValueFromSource(right);
|
|
28
|
+
return leftValue.localeCompare(rightValue);
|
|
29
|
+
});
|
|
30
|
+
return order === 'desc' ? sorted.reverse() : sorted;
|
|
31
|
+
}
|
|
32
|
+
function renderPageListMarkup(config, source, allSources) {
|
|
33
|
+
const folderPrefix = normalizeRoutePrefix(typeof config.folder === 'string' ? config.folder : 'blog');
|
|
34
|
+
const count = typeof config.count === 'number' && Number.isFinite(config.count) ? Math.max(0, Math.trunc(config.count)) : 10;
|
|
35
|
+
const order = config.order === 'asc' ? 'asc' : 'desc';
|
|
36
|
+
const title = typeof config.title === 'string' ? config.title : '';
|
|
37
|
+
const description = typeof config.description === 'string' ? config.description : '';
|
|
38
|
+
let pages = allSources.filter((item) => item.kind === 'markdown');
|
|
39
|
+
if (folderPrefix) {
|
|
40
|
+
pages = pages.filter((item) => item.routePath.startsWith(folderPrefix) && item.routePath !== folderPrefix);
|
|
41
|
+
}
|
|
42
|
+
pages = sortSourcesByDate(pages, order);
|
|
43
|
+
if (count > 0) {
|
|
44
|
+
pages = pages.slice(0, count);
|
|
45
|
+
}
|
|
46
|
+
if (!pages.length) {
|
|
47
|
+
return `
|
|
48
|
+
<section class="page-list-section">
|
|
49
|
+
${title ? `<h2 class="section-title">${escapeHtml(title)}</h2>` : ''}
|
|
50
|
+
${description ? `<p class="section-description">${escapeHtml(description)}</p>` : ''}
|
|
51
|
+
<p class="empty-state">No matching pages found.</p>
|
|
52
|
+
</section>
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
return `
|
|
56
|
+
<section class="page-list-section">
|
|
57
|
+
${title ? `<h2 class="section-title">${escapeHtml(title)}</h2>` : ''}
|
|
58
|
+
${description ? `<p class="section-description">${escapeHtml(description)}</p>` : ''}
|
|
59
|
+
<div class="post-list">
|
|
60
|
+
${pages
|
|
61
|
+
.map((page) => {
|
|
62
|
+
const pageTitle = buildPageTitle(page, '');
|
|
63
|
+
const pageDescription = page.frontmatter.description ?? '';
|
|
64
|
+
const dateValue = normalizeDateValueFromSource(page) || 'Page';
|
|
65
|
+
return `
|
|
66
|
+
<article class="card post-card">
|
|
67
|
+
<p class="eyebrow">${escapeHtml(dateValue)}</p>
|
|
68
|
+
<h3><a href="${escapeHtml(hrefForRoute(source.routePath, page.routePath))}">${escapeHtml(pageTitle)}</a></h3>
|
|
69
|
+
${pageDescription ? `<p>${escapeHtml(pageDescription)}</p>` : ''}
|
|
70
|
+
</article>
|
|
71
|
+
`;
|
|
72
|
+
})
|
|
73
|
+
.join('')}
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
const handler = {
|
|
79
|
+
getTrigger: () => 'pagelist',
|
|
80
|
+
render: (config, { source, allSources }) => renderPageListMarkup(config, source, allSources)
|
|
81
|
+
};
|
|
82
|
+
export default handler;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { sectionStyleFromConfig } from '../rendering/shared.js';
|
|
2
|
+
import { normalizeContactHref, renderSingleLinkShortcode } from '../rendering/contact.js';
|
|
3
|
+
const handler = {
|
|
4
|
+
getTrigger: () => 'tel',
|
|
5
|
+
render: (config, { source }) => {
|
|
6
|
+
const phone = typeof config.phone === 'string' ? config.phone : '';
|
|
7
|
+
if (!phone) {
|
|
8
|
+
throw new Error('tel block requires a phone parameter in the shortcode body');
|
|
9
|
+
}
|
|
10
|
+
const label = typeof config.label === 'string' ? config.label : phone;
|
|
11
|
+
const href = normalizeContactHref(phone, 'phone');
|
|
12
|
+
return renderSingleLinkShortcode({
|
|
13
|
+
title: typeof config.title === 'string' ? config.title : '',
|
|
14
|
+
description: typeof config.description === 'string' ? config.description : '',
|
|
15
|
+
label,
|
|
16
|
+
href,
|
|
17
|
+
sectionStyle: sectionStyleFromConfig(config, source.routePath),
|
|
18
|
+
className: 'shortcode-tel',
|
|
19
|
+
iconLabel: 'phone',
|
|
20
|
+
showIcon: typeof config.showIcon === 'boolean' ? config.showIcon : true
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export default handler;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opnpress/opnpress-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -17,15 +17,15 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"bin": {
|
|
20
|
-
"opnPrs": "./dist/cli.js",
|
|
21
|
-
"opnPress": "./dist/cli.js"
|
|
20
|
+
"opnPrs": "./dist/cli/main.js",
|
|
21
|
+
"opnPress": "./dist/cli/main.js"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "tsc -p tsconfig.build.json",
|
|
25
25
|
"check": "tsc --noEmit",
|
|
26
|
-
"dev": "tsx src/cli.ts",
|
|
27
|
-
"init": "tsx src/cli.ts init",
|
|
28
|
-
"run": "tsx src/cli.ts run",
|
|
26
|
+
"dev": "tsx src/cli/main.ts",
|
|
27
|
+
"init": "tsx src/cli/main.ts init",
|
|
28
|
+
"run": "tsx src/cli/main.ts run",
|
|
29
29
|
"test": "vitest run"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|