mdorigin 0.1.0 → 0.1.1

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.
@@ -32,11 +32,20 @@ export async function loadSiteConfig(options = {}) {
32
32
  parsedConfig.siteDescription !== ''
33
33
  ? parsedConfig.siteDescription
34
34
  : undefined,
35
+ siteUrl: normalizeSiteUrl(parsedConfig.siteUrl),
36
+ favicon: normalizeSiteHref(parsedConfig.favicon),
37
+ logo: normalizeLogo(parsedConfig.logo),
35
38
  showDate: parsedConfig.showDate ?? true,
36
39
  showSummary: parsedConfig.showSummary ?? true,
37
40
  theme: isBuiltInThemeName(parsedConfig.theme) ? parsedConfig.theme : 'paper',
38
41
  template: isTemplateName(parsedConfig.template) ? parsedConfig.template : 'document',
39
42
  topNav: normalizeTopNav(parsedConfig.topNav),
43
+ footerNav: normalizeTopNav(parsedConfig.footerNav),
44
+ footerText: typeof parsedConfig.footerText === 'string' && parsedConfig.footerText !== ''
45
+ ? parsedConfig.footerText
46
+ : undefined,
47
+ socialLinks: normalizeSocialLinks(parsedConfig.socialLinks),
48
+ editLink: normalizeEditLink(parsedConfig.editLink),
40
49
  showHomeIndex: typeof parsedConfig.showHomeIndex === 'boolean'
41
50
  ? parsedConfig.showHomeIndex
42
51
  : normalizeTopNav(parsedConfig.topNav).length === 0,
@@ -83,7 +92,7 @@ function isBuiltInThemeName(value) {
83
92
  return value === 'paper' || value === 'atlas' || value === 'gazette';
84
93
  }
85
94
  function isTemplateName(value) {
86
- return value === 'document' || value === 'editorial';
95
+ return value === 'document' || value === 'catalog';
87
96
  }
88
97
  function isNodeNotFound(error) {
89
98
  return (typeof error === 'object' &&
@@ -116,8 +125,81 @@ function normalizeTopNav(value) {
116
125
  item.label !== '' &&
117
126
  typeof item.href === 'string' &&
118
127
  item.href !== '') {
119
- return [{ label: item.label, href: item.href }];
128
+ const href = normalizeSiteHref(item.href);
129
+ return href ? [{ label: item.label, href }] : [];
120
130
  }
121
131
  return [];
122
132
  });
123
133
  }
134
+ function normalizeLogo(value) {
135
+ if (typeof value !== 'object' ||
136
+ value === null ||
137
+ !('src' in value) ||
138
+ typeof value.src !== 'string' ||
139
+ value.src === '') {
140
+ return undefined;
141
+ }
142
+ const src = normalizeSiteHref(value.src);
143
+ if (!src) {
144
+ return undefined;
145
+ }
146
+ const href = 'href' in value && typeof value.href === 'string'
147
+ ? normalizeSiteHref(value.href)
148
+ : undefined;
149
+ return {
150
+ src,
151
+ alt: 'alt' in value && typeof value.alt === 'string' && value.alt !== ''
152
+ ? value.alt
153
+ : undefined,
154
+ href,
155
+ };
156
+ }
157
+ function normalizeSocialLinks(value) {
158
+ if (!Array.isArray(value)) {
159
+ return [];
160
+ }
161
+ return value.flatMap((item) => {
162
+ if (typeof item !== 'object' ||
163
+ item === null ||
164
+ !('icon' in item) ||
165
+ !('label' in item) ||
166
+ !('href' in item) ||
167
+ typeof item.icon !== 'string' ||
168
+ item.icon === '' ||
169
+ typeof item.label !== 'string' ||
170
+ item.label === '' ||
171
+ typeof item.href !== 'string' ||
172
+ item.href === '') {
173
+ return [];
174
+ }
175
+ const href = normalizeSiteHref(item.href);
176
+ return href
177
+ ? [{ icon: item.icon, label: item.label, href }]
178
+ : [];
179
+ });
180
+ }
181
+ function normalizeEditLink(value) {
182
+ if (typeof value !== 'object' ||
183
+ value === null ||
184
+ !('baseUrl' in value) ||
185
+ typeof value.baseUrl !== 'string' ||
186
+ value.baseUrl === '') {
187
+ return undefined;
188
+ }
189
+ return { baseUrl: value.baseUrl };
190
+ }
191
+ function normalizeSiteUrl(value) {
192
+ return typeof value === 'string' && value !== '' ? value.replace(/\/+$/, '') : undefined;
193
+ }
194
+ function normalizeSiteHref(value) {
195
+ if (typeof value !== 'string' || value === '') {
196
+ return undefined;
197
+ }
198
+ if (value.startsWith('/') ||
199
+ value.startsWith('#') ||
200
+ value.startsWith('//') ||
201
+ /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value)) {
202
+ return value;
203
+ }
204
+ return `/${value.replace(/^\.?\//, '')}`;
205
+ }
@@ -1 +1 @@
1
- export type TemplateName = 'document' | 'editorial';
1
+ export type TemplateName = 'document' | 'catalog';
@@ -1,9 +1,13 @@
1
- import type { SiteNavItem } from '../core/site-config.js';
1
+ import type { SiteLogo, SiteNavItem, SiteSocialLink } from '../core/site-config.js';
2
+ import type { ManagedIndexEntry } from '../core/markdown.js';
2
3
  import type { TemplateName } from './template-kind.js';
3
4
  import { type BuiltInThemeName } from './theme.js';
4
5
  export interface RenderDocumentOptions {
5
6
  siteTitle: string;
6
7
  siteDescription?: string;
8
+ siteUrl?: string;
9
+ favicon?: string;
10
+ logo?: SiteLogo;
7
11
  title: string;
8
12
  body: string;
9
13
  summary?: string;
@@ -13,7 +17,14 @@ export interface RenderDocumentOptions {
13
17
  theme: BuiltInThemeName;
14
18
  template: TemplateName;
15
19
  topNav?: SiteNavItem[];
20
+ footerNav?: SiteNavItem[];
21
+ footerText?: string;
22
+ socialLinks?: SiteSocialLink[];
23
+ editLinkHref?: string;
16
24
  stylesheetContent?: string;
25
+ canonicalPath?: string;
26
+ alternateMarkdownPath?: string;
27
+ catalogEntries?: ManagedIndexEntry[];
17
28
  }
18
29
  export declare function renderDocument(options: RenderDocumentOptions): string;
19
30
  export declare function escapeHtml(value: string): string;
@@ -8,17 +8,49 @@ export function renderDocument(options) {
8
8
  const summaryMeta = options.summary
9
9
  ? `<meta name="description" content="${escapeHtml(options.summary)}">`
10
10
  : '';
11
+ const canonicalMeta = options.siteUrl && options.canonicalPath
12
+ ? `<link rel="canonical" href="${escapeHtml(`${options.siteUrl}${options.canonicalPath}`)}">`
13
+ : '';
14
+ const faviconMeta = options.favicon
15
+ ? `<link rel="icon" href="${escapeHtml(options.favicon)}">`
16
+ : '';
17
+ const alternateMarkdownMeta = options.alternateMarkdownPath
18
+ ? `<link rel="alternate" type="text/markdown" href="${escapeHtml(options.alternateMarkdownPath)}">`
19
+ : '';
11
20
  const stylesheetBlock = `<style>${getBuiltInThemeStyles(options.theme)}${options.stylesheetContent ? `\n${options.stylesheetContent}` : ''}</style>`;
12
21
  const navBlock = options.topNav && options.topNav.length > 0
13
22
  ? `<nav class="site-nav"><ul>${options.topNav
14
23
  .map((item) => `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a></li>`)
15
24
  .join('')}</ul></nav>`
16
25
  : '';
26
+ const footerNavBlock = options.footerNav && options.footerNav.length > 0
27
+ ? `<nav class="site-footer__nav"><ul>${options.footerNav
28
+ .map((item) => `<li><a href="${escapeHtml(item.href)}">${escapeHtml(item.label)}</a></li>`)
29
+ .join('')}</ul></nav>`
30
+ : '';
31
+ const socialLinksBlock = options.socialLinks && options.socialLinks.length > 0
32
+ ? `<ul class="site-footer__social">${options.socialLinks
33
+ .map((item) => `<li><a href="${escapeHtml(item.href)}" aria-label="${escapeHtml(item.label)}" title="${escapeHtml(item.label)}">${renderSocialIcon(item.icon)}</a></li>`)
34
+ .join('')}</ul>`
35
+ : '';
17
36
  const siteDescriptionBlock = siteDescription
18
37
  ? `<span>${siteDescription}</span>`
19
38
  : '';
20
- const articleBody = options.template === 'editorial'
21
- ? renderEditorialBody(options)
39
+ const logoBlock = options.logo
40
+ ? `<span class="site-header__logo"><img src="${escapeHtml(options.logo.src)}" alt="${escapeHtml(options.logo.alt ?? '')}"></span>`
41
+ : '';
42
+ const brandHref = escapeHtml(options.logo?.href ?? '/');
43
+ const editLinkBlock = options.editLinkHref
44
+ ? `<a class="site-footer__edit-link" href="${escapeHtml(options.editLinkHref)}">Edit this page</a>`
45
+ : '';
46
+ const footerTextBlock = options.footerText
47
+ ? `<p class="site-footer__text">${escapeHtml(options.footerText)}</p>`
48
+ : '';
49
+ const footerBlock = footerNavBlock || socialLinksBlock || footerTextBlock || editLinkBlock
50
+ ? `<footer class="site-footer"><div class="site-footer__inner">${footerNavBlock}${socialLinksBlock}${footerTextBlock}${editLinkBlock}</div></footer>`
51
+ : '';
52
+ const articleBody = options.template === 'catalog'
53
+ ? renderCatalogArticle(options.body, options.catalogEntries ?? [])
22
54
  : options.body;
23
55
  return [
24
56
  '<!doctype html>',
@@ -28,35 +60,21 @@ export function renderDocument(options) {
28
60
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
29
61
  `<title>${title} | ${siteTitle}</title>`,
30
62
  summaryMeta,
63
+ canonicalMeta,
64
+ faviconMeta,
65
+ alternateMarkdownMeta,
31
66
  stylesheetBlock,
32
67
  '</head>',
33
68
  `<body data-theme="${options.theme}" data-template="${options.template}">`,
34
- `<header class="site-header"><div class="site-header__inner"><div class="site-header__brand"><p class="site-header__title"><a href="/">${siteTitle}</a></p>${siteDescriptionBlock}</div>${navBlock}</div></header>`,
69
+ `<header class="site-header"><div class="site-header__inner"><div class="site-header__brand"><p class="site-header__title"><a href="${brandHref}">${logoBlock}<span>${siteTitle}</span></a></p>${siteDescriptionBlock}</div>${navBlock}</div></header>`,
35
70
  '<main>',
36
71
  `<article>${articleBody}</article>`,
37
72
  '</main>',
73
+ footerBlock,
38
74
  '</body>',
39
75
  '</html>',
40
76
  ].join('');
41
77
  }
42
- function renderEditorialBody(options) {
43
- const heroLines = [
44
- '<div class="page-intro">',
45
- `<p class="page-intro__eyebrow">${escapeHtml(options.siteTitle)}</p>`,
46
- `<h1 class="page-intro__title">${escapeHtml(options.title)}</h1>`,
47
- ];
48
- if (options.showSummary !== false && options.summary) {
49
- heroLines.push(`<p class="page-intro__summary">${escapeHtml(options.summary)}</p>`);
50
- }
51
- if (options.showDate !== false && options.date) {
52
- heroLines.push(`<p class="page-intro__meta">${escapeHtml(options.date)}</p>`);
53
- }
54
- heroLines.push('</div>');
55
- return `${heroLines.join('')}<div class="page-body">${stripLeadingH1(options.body)}</div>`;
56
- }
57
- function stripLeadingH1(html) {
58
- return html.replace(/^\s*<h1>.*?<\/h1>\s*/s, '');
59
- }
60
78
  export function escapeHtml(value) {
61
79
  return value
62
80
  .replaceAll('&', '&amp;')
@@ -65,3 +83,54 @@ export function escapeHtml(value) {
65
83
  .replaceAll('"', '&quot;')
66
84
  .replaceAll("'", '&#39;');
67
85
  }
86
+ function renderSocialIcon(icon) {
87
+ switch (icon) {
88
+ case 'github':
89
+ return iconSvg('M12 2C6.48 2 2 6.58 2 12.11c0 4.43 2.87 8.18 6.84 9.5.5.1.68-.22.68-.48 0-.24-.01-1.04-.01-1.89-2.78.62-3.37-1.19-3.37-1.19-.46-1.17-1.11-1.48-1.11-1.48-.91-.63.07-.62.07-.62 1 .08 1.53 1.04 1.53 1.04.9 1.54 2.35 1.09 2.92.84.09-.66.35-1.09.63-1.34-2.22-.25-4.55-1.12-4.55-4.97 0-1.1.39-2 1.03-2.71-.1-.26-.45-1.29.1-2.68 0 0 .84-.27 2.75 1.03A9.4 9.4 0 0 1 12 6.84c.85 0 1.71.12 2.51.35 1.91-1.3 2.75-1.03 2.75-1.03.55 1.39.2 2.42.1 2.68.64.71 1.03 1.61 1.03 2.71 0 3.86-2.33 4.72-4.56 4.97.36.31.68.91.68 1.84 0 1.33-.01 2.4-.01 2.73 0 .27.18.58.69.48A10.11 10.11 0 0 0 22 12.11C22 6.58 17.52 2 12 2Z');
90
+ case 'rss':
91
+ return iconSvg('M5 3a16 16 0 0 1 16 16h-3A13 13 0 0 0 5 6V3Zm0 6a10 10 0 0 1 10 10h-3a7 7 0 0 0-7-7V9Zm0 6a4 4 0 0 1 4 4H5v-4Zm0 3a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z');
92
+ case 'npm':
93
+ return iconSvg('M3 7h18v10h-9v-8h-3v8H3V7Zm10 2h2v6h2V9h2v6h1V9h1V8h-8v1Z');
94
+ case 'x':
95
+ return iconSvg('M18.9 3H21l-6.87 7.85L22 21h-6.17l-4.83-6.32L5.47 21H3.36l7.35-8.4L2 3h6.32l4.37 5.77L18.9 3Zm-2.17 16h1.17L7.68 4H6.43l10.3 15Z');
96
+ case 'home':
97
+ return iconSvg('M12 3 3 10.2V21h6v-6h6v6h6V10.2L12 3Z');
98
+ default:
99
+ return `<span class="site-footer__social-label">${escapeHtml(icon.slice(0, 1).toUpperCase())}</span>`;
100
+ }
101
+ }
102
+ function iconSvg(pathData) {
103
+ return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="${pathData}"></path></svg>`;
104
+ }
105
+ function renderCatalogArticle(body, entries) {
106
+ if (entries.length === 0) {
107
+ return body;
108
+ }
109
+ const directories = entries.filter((entry) => entry.kind === 'directory');
110
+ const articles = entries.filter((entry) => entry.kind === 'article');
111
+ return [
112
+ `<div class="catalog-page__body">${body}</div>`,
113
+ '<section class="catalog-page" aria-label="Catalog">',
114
+ directories.length > 0 ? renderCatalogDirectories(directories) : '',
115
+ articles.length > 0 ? renderCatalogArticles(articles) : '',
116
+ '</section>',
117
+ ].join('');
118
+ }
119
+ function renderCatalogDirectories(entries) {
120
+ return [
121
+ '<div class="catalog-list catalog-list--directories">',
122
+ ...entries.map((entry) => `<a class="catalog-item catalog-item--directory" href="${escapeHtml(entry.href)}"><strong class="catalog-item__title">${escapeHtml(entry.title)}</strong>${entry.detail
123
+ ? `<span class="catalog-item__detail">${escapeHtml(entry.detail)}</span>`
124
+ : '<span class="catalog-item__detail">Browse this section.</span>'}</a>`),
125
+ '</div>',
126
+ ].join('');
127
+ }
128
+ function renderCatalogArticles(entries) {
129
+ return [
130
+ '<div class="catalog-list">',
131
+ ...entries.map((entry) => `<a class="catalog-item" href="${escapeHtml(entry.href)}"><strong class="catalog-item__title">${escapeHtml(entry.title)}</strong>${entry.detail
132
+ ? `<span class="catalog-item__detail">${escapeHtml(entry.detail)}</span>`
133
+ : ''}</a>`),
134
+ '</div>',
135
+ ].join('');
136
+ }