@vyriy/ssg 0.9.1 → 0.9.3

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/components.js ADDED
@@ -0,0 +1,343 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useId } from 'react';
3
+ const cn = (...classes) => classes.filter(Boolean).join(' ');
4
+ const getTagHref = (tag) => `/search/?tag=${encodeURIComponent(tag)}`;
5
+ const visiblePaginationPages = 3;
6
+ const getPaginationItems = (page, pages) => {
7
+ if (pages <= visiblePaginationPages + 2) {
8
+ return Array.from({ length: pages }, (_value, index) => ({
9
+ page: index + 1,
10
+ type: 'page',
11
+ }));
12
+ }
13
+ const halfWindow = Math.floor(visiblePaginationPages / 2);
14
+ const windowStart = Math.max(2, Math.min(page - halfWindow, pages - visiblePaginationPages));
15
+ const windowEnd = Math.min(pages - 1, windowStart + visiblePaginationPages - 1);
16
+ const items = [
17
+ {
18
+ page: 1,
19
+ type: 'page',
20
+ },
21
+ ];
22
+ if (windowStart > 2) {
23
+ items.push({
24
+ key: 'start-ellipsis',
25
+ type: 'ellipsis',
26
+ });
27
+ }
28
+ for (let itemPage = windowStart; itemPage <= windowEnd; itemPage += 1) {
29
+ items.push({
30
+ page: itemPage,
31
+ type: 'page',
32
+ });
33
+ }
34
+ if (windowEnd < pages - 1) {
35
+ items.push({
36
+ key: 'end-ellipsis',
37
+ type: 'ellipsis',
38
+ });
39
+ }
40
+ items.push({
41
+ page: pages,
42
+ type: 'page',
43
+ });
44
+ return items;
45
+ };
46
+ const headerSearchScript = String.raw `
47
+ (() => {
48
+ const minimumQueryLength = 2;
49
+ const headers = document.querySelectorAll('.vyriy-header');
50
+
51
+ headers.forEach((header) => {
52
+ if (header.dataset.searchReady === 'true') {
53
+ return;
54
+ }
55
+
56
+ header.dataset.searchReady = 'true';
57
+
58
+ const checkbox = header.querySelector('.vyriy-header__search-checkbox');
59
+ const form = header.querySelector('.vyriy-header__search');
60
+ const input = form ? form.querySelector('input[name="q"]') : null;
61
+
62
+ if (!checkbox || !form || !input) {
63
+ return;
64
+ }
65
+
66
+ const focusSearchInput = (attempt = 0) => {
67
+ if (!checkbox.checked) {
68
+ return;
69
+ }
70
+
71
+ input.focus();
72
+
73
+ if (document.activeElement === input || attempt >= 4) {
74
+ return;
75
+ }
76
+
77
+ const scheduleFocus = window.requestAnimationFrame || window.setTimeout;
78
+
79
+ scheduleFocus(() => focusSearchInput(attempt + 1));
80
+ };
81
+
82
+ checkbox.addEventListener('change', () => {
83
+ if (checkbox.checked) {
84
+ focusSearchInput();
85
+ }
86
+ });
87
+
88
+ document.addEventListener('keydown', (event) => {
89
+ if (event.key !== 'Escape' || !checkbox.checked) {
90
+ return;
91
+ }
92
+
93
+ checkbox.checked = false;
94
+ input.blur();
95
+ });
96
+
97
+ input.addEventListener('input', () => {
98
+ input.setCustomValidity('');
99
+ });
100
+
101
+ form.addEventListener('submit', (event) => {
102
+ const query = input.value.trim();
103
+
104
+ if (query.length >= minimumQueryLength) {
105
+ input.value = query;
106
+ input.setCustomValidity('');
107
+ return;
108
+ }
109
+
110
+ event.preventDefault();
111
+ input.setCustomValidity('Enter at least ' + minimumQueryLength + ' characters.');
112
+ input.reportValidity();
113
+ });
114
+ });
115
+ })();
116
+ `;
117
+ const searchScript = String.raw `
118
+ (() => {
119
+ const root = document.getElementById('search-root');
120
+
121
+ if (!root) {
122
+ return;
123
+ }
124
+
125
+ const documentsUrl = root.dataset.documentsUrl || '/search/documents.json';
126
+ const indexUrl = root.dataset.indexUrl || '/search/minisearch-index.json';
127
+ const parameters = new URLSearchParams(window.location.search);
128
+ const tag = parameters.get('tag');
129
+ const query = (parameters.get('q') || '').trim();
130
+ const formInput = document.querySelector('.vyriy-search-page__form input[name="q"]');
131
+
132
+ if (formInput) {
133
+ formInput.value = query;
134
+ }
135
+
136
+ if (!tag && !query) {
137
+ root.innerHTML = '<p class="vyriy-search-page__status">Choose a post tag or enter a search query to see matching articles and examples.</p>';
138
+ return;
139
+ }
140
+
141
+ const searchOptions = {
142
+ boost: {
143
+ content: 1,
144
+ description: 2,
145
+ tags: 3,
146
+ title: 4
147
+ },
148
+ fuzzy: 0.2,
149
+ prefix: true
150
+ };
151
+ const miniSearchOptions = {
152
+ fields: ['title', 'description', 'tags', 'content'],
153
+ storeFields: ['title', 'description', 'section', 'slug', 'url', 'tags', 'date'],
154
+ searchOptions
155
+ };
156
+ const resultPageSize = 10;
157
+ let currentMatches = [];
158
+ let visibleResultCount = resultPageSize;
159
+ const normalizeTag = (value) => value.trim().toLowerCase();
160
+ const escapeHtml = (value) => String(value)
161
+ .replaceAll('&', '&amp;')
162
+ .replaceAll('<', '&lt;')
163
+ .replaceAll('>', '&gt;')
164
+ .replaceAll('"', '&quot;')
165
+ .replaceAll("'", '&#39;');
166
+ const getDocumentDescription = (document) => document.description || document.content || '';
167
+ const getDocumentTags = (document) => Array.isArray(document.tags) ? document.tags : [];
168
+ const renderTag = (value) => '<a class="vyriy-search-page__tag" href="/search/?tag=' + encodeURIComponent(value) + '">' + escapeHtml(value) + '</a>';
169
+ const renderDocument = (document) => {
170
+ const description = getDocumentDescription(document);
171
+ const tags = getDocumentTags(document);
172
+ const descriptionHtml = description
173
+ ? '<p class="vyriy-search-page__result-description">' + escapeHtml(description) + '</p>'
174
+ : '';
175
+ const tagsHtml = tags.length
176
+ ? '<div class="vyriy-search-page__tags">' + tags.map(renderTag).join('') + '</div>'
177
+ : '';
178
+
179
+ return '<article class="vyriy-search-page__result">' +
180
+ '<a class="vyriy-search-page__result-link" href="' + escapeHtml(document.url) + '">' +
181
+ '<h2 class="vyriy-search-page__result-title">' + escapeHtml(document.title) + '</h2>' +
182
+ descriptionHtml +
183
+ '</a>' +
184
+ tagsHtml +
185
+ '</article>';
186
+ };
187
+ const getDocumentMap = (documents) => new Map(documents.map((document) => [document.id, document]));
188
+ const getTagMatches = (documents, value) => {
189
+ const normalizedTag = normalizeTag(value);
190
+
191
+ return documents.filter((document) =>
192
+ getDocumentTags(document).some((candidate) => normalizeTag(candidate) === normalizedTag),
193
+ );
194
+ };
195
+ const getQueryMatches = (documents, indexJson, value) => {
196
+ const MiniSearch = window.MiniSearch;
197
+
198
+ if (!MiniSearch) {
199
+ throw new Error('MiniSearch is unavailable.');
200
+ }
201
+
202
+ const documentsById = getDocumentMap(documents);
203
+ const search = MiniSearch.loadJSON(JSON.stringify(indexJson), miniSearchOptions);
204
+
205
+ return search.search(value).map((result) => documentsById.get(result.id) || result).filter(Boolean);
206
+ };
207
+ const renderResults = (documents, emptyMessage) => {
208
+ if (!documents.length) {
209
+ return '<p class="vyriy-search-page__status">' + emptyMessage + '</p>';
210
+ }
211
+
212
+ const visibleDocuments = documents.slice(0, visibleResultCount);
213
+ const remainingCount = documents.length - visibleDocuments.length;
214
+ const moreButton = remainingCount > 0
215
+ ? '<button class="vyriy-search-page__more" type="button" data-search-more>Show more (' + remainingCount + ')</button>'
216
+ : '';
217
+
218
+ return (
219
+ '<div class="vyriy-search-page__results">' + visibleDocuments.map(renderDocument).join('') + '</div>' +
220
+ moreButton
221
+ );
222
+ };
223
+ const renderStatus = () => {
224
+ if (tag && query) {
225
+ return 'Query: <strong>' + escapeHtml(query) + '</strong> · Tag: <strong>' + escapeHtml(tag) + '</strong>';
226
+ }
227
+
228
+ if (query) {
229
+ return 'Query: <strong>' + escapeHtml(query) + '</strong>';
230
+ }
231
+
232
+ return 'Tag: <strong>' + escapeHtml(tag) + '</strong>';
233
+ };
234
+ const renderSearchState = (emptyMessage) => {
235
+ root.innerHTML =
236
+ '<p class="vyriy-search-page__status">' + renderStatus() + '</p>' +
237
+ renderResults(currentMatches, emptyMessage);
238
+ };
239
+
240
+ root.addEventListener('click', (event) => {
241
+ const target = event.target;
242
+
243
+ if (!target || !target.closest('[data-search-more]')) {
244
+ return;
245
+ }
246
+
247
+ visibleResultCount += resultPageSize;
248
+ renderSearchState(query ? 'No posts match this search yet.' : 'No posts use this tag yet.');
249
+ });
250
+
251
+ const dataRequests = [fetch(documentsUrl)];
252
+
253
+ if (query) {
254
+ dataRequests.push(fetch(indexUrl));
255
+ }
256
+
257
+ Promise.all(dataRequests)
258
+ .then((responses) => {
259
+ for (const response of responses) {
260
+ if (!response.ok) {
261
+ throw new Error('Search data is unavailable.');
262
+ }
263
+ }
264
+
265
+ return Promise.all(responses.map((response, index) => index === 1 ? response.text() : response.json()));
266
+ })
267
+ .then(([documents, indexText]) => {
268
+ const indexJson = indexText ? JSON.parse(indexText) : undefined;
269
+ const queryMatches = query ? getQueryMatches(documents, indexJson, query) : documents;
270
+ currentMatches = tag ? getTagMatches(queryMatches, tag) : queryMatches;
271
+
272
+ document.title = 'Vyriy Search: ' + (query || tag);
273
+ renderSearchState(query ? 'No posts match this search yet.' : 'No posts use this tag yet.');
274
+ })
275
+ .catch(() => {
276
+ root.innerHTML = '<p class="vyriy-search-page__status">Search data could not be loaded.</p>';
277
+ });
278
+ })();
279
+ `;
280
+ const SearchForm = (props) => {
281
+ const { action = '/search/', buttonLabel = 'Search', className, inputLabel = 'Search query', inputName = 'q', method = 'get', minimumQueryLength = 2, placeholder = 'Search', showButton = true, ...rest } = props;
282
+ const inputId = useId();
283
+ return (_jsxs("form", { ...rest, action: action, className: cn('vyriy-search-form', className), method: method, children: [_jsx("label", { className: "vyriy-search-form__label", htmlFor: inputId, children: inputLabel }), _jsx("input", { className: "vyriy-search-form__input", id: inputId, minLength: minimumQueryLength, name: inputName, placeholder: placeholder, required: true, type: "search" }), showButton ? (_jsx("button", { className: "vyriy-search-form__button", type: "submit", children: buttonLabel })) : null] }));
284
+ };
285
+ const Navigation = (props) => {
286
+ const { ariaLabel = 'Main navigation', className, menuLabel = 'Menu', ...rest } = props;
287
+ const menuId = useId();
288
+ const listId = useId();
289
+ return (_jsxs("nav", { ...rest, "aria-label": ariaLabel, className: cn('vyriy-navigation', className), children: [_jsx("input", { "aria-controls": listId, "aria-label": menuLabel, className: "vyriy-navigation__checkbox", id: menuId, type: "checkbox" }), _jsxs("label", { className: "vyriy-navigation__toggle", htmlFor: menuId, children: [_jsxs("span", { className: "vyriy-navigation__toggle-icon", "aria-hidden": "true", children: [_jsx("span", { className: "vyriy-navigation__toggle-line" }), _jsx("span", { className: "vyriy-navigation__toggle-line" }), _jsx("span", { className: "vyriy-navigation__toggle-line" })] }), _jsx("span", { className: "vyriy-navigation__toggle-text", children: menuLabel })] }), _jsxs("ul", { className: "vyriy-navigation__list", id: listId, children: [_jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "/blog/", children: "Blog" }) }), _jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "/examples/", children: "Examples" }) }), _jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "/docs/", children: "Documentation" }) }), _jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "https://github.com/evheniy/vyriy", rel: "noreferrer", target: "_blank", children: "GitHub" }) }), _jsx("li", { className: "vyriy-navigation__item", children: _jsx("a", { className: "vyriy-navigation__link", href: "/consulting/", children: "Consulting" }) })] })] }));
290
+ };
291
+ const Header = (props) => {
292
+ const { className, homeHref = '/', logoAlt = '', logoSrc = '/assets/vyriy-v-wings.png', name, ...rest } = props;
293
+ const searchToggleId = useId();
294
+ return (_jsxs("header", { ...rest, className: cn('vyriy-header', className), children: [_jsxs("div", { className: "vyriy-header__inner", children: [_jsxs("a", { className: "vyriy-header__brand", href: homeHref, children: [_jsx("img", { className: "vyriy-header__logo", src: logoSrc, alt: logoAlt, width: "72", height: "46", fetchPriority: "low" }), name] }), _jsxs("div", { className: "vyriy-header__actions", children: [_jsxs("div", { className: "vyriy-header__search-shell", children: [_jsx("input", { className: "vyriy-header__search-checkbox", id: searchToggleId, type: "checkbox" }), _jsxs("label", { className: "vyriy-header__search-toggle", htmlFor: searchToggleId, children: [_jsx("span", { className: "vyriy-header__search-label", children: "Search" }), _jsx("span", { className: "vyriy-header__search-icon", "aria-hidden": "true" })] }), _jsx(SearchForm, { className: "vyriy-header__search", placeholder: "Search", showButton: false })] }), _jsx(Navigation, { className: "vyriy-header__navigation" })] })] }), _jsx("script", { dangerouslySetInnerHTML: { __html: headerSearchScript } })] }));
295
+ };
296
+ const Footer = (props) => {
297
+ const { className, text, ...rest } = props;
298
+ return (_jsx("footer", { ...rest, className: cn('vyriy-footer', className), children: _jsx("div", { className: "vyriy-footer__inner", children: _jsx("p", { className: "vyriy-footer__text", children: text }) }) }));
299
+ };
300
+ const Layout = (props) => {
301
+ const { children, footerText, name } = props;
302
+ return (_jsxs("div", { className: "vyriy-layout", children: [_jsx(Header, { name: name }), _jsx("main", { className: "vyriy-layout__main", children: children }), _jsx(Footer, { text: footerText })] }));
303
+ };
304
+ export const Card = (props) => {
305
+ const { className, date, description, href, tags = [], title, ...rest } = props;
306
+ const hasMetadata = Boolean(date) || tags.length > 0;
307
+ return (_jsx("article", { ...rest, className: cn('vyriy-card', className), children: _jsxs("a", { "aria-label": title, className: "vyriy-card__link", href: href, children: [_jsx("h2", { className: "vyriy-card__title", children: title }), _jsx("p", { className: "vyriy-card__description", children: description }), hasMetadata ? (_jsxs("div", { className: "vyriy-card__meta", children: [date ? (_jsx("time", { className: "vyriy-card__date", dateTime: date, children: date })) : null, tags.length > 0 ? (_jsx("ul", { className: "vyriy-card__tags", "aria-label": "Tags", children: tags.map((tag) => (_jsx("li", { className: "vyriy-card__tag", children: tag }, tag))) })) : null] })) : null] }) }));
308
+ };
309
+ export const Page = (props) => {
310
+ const { content, featured = [], related = [], tags = [] } = props;
311
+ return (_jsx(Layout, { footerText: "Copyright \u00A9 2026 Vyriy", name: "Vyriy", children: _jsxs("section", { className: "vyriy-page", children: [_jsx("div", { className: "vyriy-page__content", children: content }), featured.length > 0 ? (_jsx("section", { "aria-label": "Featured posts", className: "vyriy-page__featured", children: _jsx("div", { className: "vyriy-page__featured-list", children: featured.map((item) => (_jsx(Card, { className: "vyriy-page__featured-card", description: item.description, href: item.href, title: item.title }, item.href))) }) })) : null, tags.length > 0 ? (_jsx("nav", { "aria-label": "Post tags", className: "vyriy-page__tags", children: tags.map((tag) => (_jsx("a", { className: "vyriy-page__tag", href: getTagHref(tag), children: tag }, tag))) })) : null, related.length > 0 ? (_jsxs("aside", { "aria-labelledby": "related-posts-title", className: "vyriy-page__related", children: [_jsx("h2", { className: "vyriy-page__section-title", id: "related-posts-title", children: "Related posts" }), _jsx("div", { className: "vyriy-page__related-list", children: related.map((item) => (_jsx(Card, { className: "vyriy-page__related-card", description: item.description, href: item.href, title: item.title }, item.href))) })] })) : null] }) }));
312
+ };
313
+ const Pagination = (props) => {
314
+ const { className, getHref, page, pages, ...rest } = props;
315
+ if (pages <= 1) {
316
+ return null;
317
+ }
318
+ const paginationItems = getPaginationItems(page, pages);
319
+ const renderControl = (label, targetPage, disabled) => {
320
+ if (!getHref || disabled) {
321
+ return (_jsx("button", { className: "vyriy-pagination__control", disabled: disabled, type: "button", children: label }));
322
+ }
323
+ return (_jsx("a", { className: "vyriy-pagination__control", href: getHref(targetPage), children: label }));
324
+ };
325
+ return (_jsxs("nav", { ...rest, "aria-label": "Pagination", className: cn('vyriy-pagination', className), children: [renderControl('Prev', page - 1, page === 1), _jsx("ol", { className: "vyriy-pagination__pages", children: paginationItems.map((item) => {
326
+ if (item.type === 'ellipsis') {
327
+ return (_jsx("li", { className: "vyriy-pagination__item", children: _jsx("span", { className: "vyriy-pagination__ellipsis", "aria-hidden": "true", children: "..." }) }, item.key));
328
+ }
329
+ return (_jsx("li", { className: "vyriy-pagination__item", children: getHref && item.page !== page ? (_jsx("a", { className: "vyriy-pagination__page", href: getHref(item.page), children: item.page })) : (_jsx("button", { "aria-current": item.page === page ? 'page' : undefined, className: "vyriy-pagination__page", disabled: item.page === page, type: "button", children: item.page })) }, item.page));
330
+ }) }), renderControl('Last', pages, page === pages)] }));
331
+ };
332
+ export const Catalog = (props) => {
333
+ const { content, paginate } = props;
334
+ return (_jsx(Layout, { footerText: "Copyright \u00A9 2026 Vyriy", name: "Vyriy", children: _jsxs("section", { className: "vyriy-catalog", children: [_jsx("div", { className: "vyriy-catalog__content", children: content }), _jsx(Pagination, { getHref: paginate.getHref, page: paginate.page, pages: paginate.pages })] }) }));
335
+ };
336
+ export const NotFoundPage = (props) => {
337
+ const { homeHref = '/' } = props;
338
+ return (_jsx(Page, { content: _jsxs(_Fragment, { children: [_jsx("h1", { children: "Page not found" }), _jsx("p", { children: "The page you are looking for does not exist. Return to the home page or use the navigation above." }), _jsx("p", { children: _jsx("a", { href: homeHref, children: "Return home" }) })] }) }));
339
+ };
340
+ export const SearchPage = (props) => {
341
+ const { documentsUrl = '/search/documents.json', indexUrl = '/search/minisearch-index.json', miniSearchScriptUrl = '/assets/minisearch.js', } = props;
342
+ return (_jsx(Page, { content: _jsxs("section", { className: "vyriy-search-page", children: [_jsx("h1", { children: "Search" }), _jsx("p", { children: "Search articles and examples by text query or tag." }), _jsx(SearchForm, { className: "vyriy-search-page__form", placeholder: "Search articles and examples" }), _jsx("div", { "data-documents-url": documentsUrl, "data-index-url": indexUrl, id: "search-root" }), _jsx("script", { src: miniSearchScriptUrl }), _jsx("script", { dangerouslySetInnerHTML: { __html: searchScript } })] }) }));
343
+ };
@@ -0,0 +1,69 @@
1
+ import type { ContentEntry, ContentSection } from './types.js';
2
+ export type SearchDocument = {
3
+ readonly content: string;
4
+ readonly date?: string;
5
+ readonly description: string;
6
+ readonly id: string;
7
+ readonly section: ContentSection;
8
+ readonly slug: string;
9
+ readonly tags: readonly string[];
10
+ readonly title: string;
11
+ readonly updatedAt?: string;
12
+ readonly url: string;
13
+ };
14
+ export type RelatedDocument = {
15
+ readonly description: string;
16
+ readonly score: number;
17
+ readonly section: ContentSection;
18
+ readonly slug: string;
19
+ readonly tags: readonly string[];
20
+ readonly title: string;
21
+ readonly url: string;
22
+ };
23
+ export type RelatedDocumentsMap = Record<string, readonly RelatedDocument[]>;
24
+ export type HomePageFeaturedContentItem = {
25
+ readonly date?: string;
26
+ readonly description: string;
27
+ readonly homePageOrder?: number;
28
+ readonly section: ContentSection;
29
+ readonly slug: string;
30
+ readonly tags: readonly string[];
31
+ readonly title: string;
32
+ readonly url: string;
33
+ };
34
+ export type ContentDataSection = {
35
+ readonly entries: readonly ContentEntry[];
36
+ readonly section: ContentSection;
37
+ };
38
+ export declare const contentSearchOptions: {
39
+ boost: {
40
+ content: number;
41
+ description: number;
42
+ tags: number;
43
+ title: number;
44
+ };
45
+ fuzzy: number;
46
+ prefix: boolean;
47
+ };
48
+ export declare const contentMiniSearchOptions: {
49
+ fields: string[];
50
+ storeFields: string[];
51
+ searchOptions: {
52
+ boost: {
53
+ content: number;
54
+ description: number;
55
+ tags: number;
56
+ title: number;
57
+ };
58
+ fuzzy: number;
59
+ prefix: boolean;
60
+ };
61
+ };
62
+ export declare const getPlainTextFromMarkdown: (markdown: string) => string;
63
+ export declare const getContentUrl: (section: ContentSection, slug: string) => string;
64
+ export declare const getSearchDocuments: (section: ContentSection, entries: readonly ContentEntry[]) => readonly SearchDocument[];
65
+ export declare const getSiteSearchDocuments: (sections: readonly ContentDataSection[]) => readonly SearchDocument[];
66
+ export declare const getMiniSearchIndexJson: (documents: readonly SearchDocument[]) => unknown;
67
+ export declare const getRelatedDocumentsMap: (documents: readonly SearchDocument[]) => RelatedDocumentsMap;
68
+ export declare const getHomePageFeaturedContent: (sections: readonly ContentDataSection[]) => readonly HomePageFeaturedContentItem[];
69
+ export declare const writeContentData: (sections: readonly ContentDataSection[], outputDirectory: string) => Promise<void>;
@@ -1,9 +1,7 @@
1
- import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
2
- import { dirname, join, relative, sep } from 'node:path';
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
3
  import MiniSearch from 'minisearch';
4
- import { renderContentIndex, renderPage } from './html.js';
5
- import { getPlainTextFromMarkdown } from './markdown.js';
6
- import { parsePage } from './parse-page.js';
4
+ import { replaceInlineCode, replaceMarkdownLinks, stripFencedCode, stripHtmlTags } from './markdown-plain-text.js';
7
5
  const relatedDocumentCount = 4;
8
6
  const homePageFeaturedContentCount = 4;
9
7
  const minimumRelatedScore = 10;
@@ -35,101 +33,21 @@ export const contentMiniSearchOptions = {
35
33
  ],
36
34
  searchOptions: contentSearchOptions,
37
35
  };
38
- const isNodeError = (error) => {
39
- return typeof error === 'object' && error !== null && 'code' in error;
36
+ const markdownSyntaxPattern = /[#>*_~|[\](){}\\-]+/gu;
37
+ const whitespacePattern = /\s+/gu;
38
+ const wordPattern = /[\p{L}\p{N}]+/gu;
39
+ export const getPlainTextFromMarkdown = (markdown) => {
40
+ return stripHtmlTags(replaceInlineCode(replaceMarkdownLinks(stripFencedCode(markdown))))
41
+ .replaceAll(markdownSyntaxPattern, ' ')
42
+ .replaceAll(whitespacePattern, ' ')
43
+ .trim();
40
44
  };
41
- export const findReadmePaths = async (directory) => {
42
- let entries;
43
- try {
44
- entries = await readdir(directory, {
45
- withFileTypes: true,
46
- });
47
- }
48
- catch (error) {
49
- if (isNodeError(error) && error.code === 'ENOENT') {
50
- return [];
51
- }
52
- throw error;
53
- }
54
- const paths = await Promise.all(entries.map(async (entry) => {
55
- const entryPath = join(directory, entry.name);
56
- if (entry.isDirectory()) {
57
- return findReadmePaths(entryPath);
58
- }
59
- return entry.name === 'README.md' ? [entryPath] : [];
60
- }));
61
- return paths.flat();
45
+ export const getContentUrl = (section, slug) => {
46
+ return slug ? `/${section}/${slug}/` : `/${section}/`;
62
47
  };
63
- const getSlug = (sectionDirectory, readmePath) => {
64
- return dirname(relative(sectionDirectory, readmePath))
65
- .split(sep)
66
- .filter((segment) => segment && segment !== '.')
67
- .join('/');
48
+ const getOptionalDate = (date) => {
49
+ return date || undefined;
68
50
  };
69
- const writeDocument = async (outputPath, document) => {
70
- await mkdir(dirname(outputPath), {
71
- recursive: true,
72
- });
73
- await writeFile(outputPath, document);
74
- };
75
- const getContentIndexHref = (sectionPath, page) => {
76
- return page <= 1 ? `/${sectionPath}/` : `/${sectionPath}/${page}/`;
77
- };
78
- const getContentIndexOutputPath = (outputDirectory, sectionPath, page) => {
79
- return page <= 1
80
- ? join(outputDirectory, sectionPath, 'index.html')
81
- : join(outputDirectory, sectionPath, String(page), 'index.html');
82
- };
83
- export const buildContentEntries = async (section, contentPath, defaultTitle = 'Vyriy') => {
84
- const sectionDirectory = join(contentPath, section.path);
85
- const readmePaths = await findReadmePaths(sectionDirectory);
86
- const entries = (await Promise.all(readmePaths.map(async (readmePath) => {
87
- const slug = getSlug(sectionDirectory, readmePath);
88
- const page = parsePage(await readFile(readmePath, 'utf8'), defaultTitle);
89
- if (!slug || !page.published) {
90
- return undefined;
91
- }
92
- return {
93
- ...page,
94
- href: `/${section.path}/${slug}/`,
95
- section: section.path,
96
- slug,
97
- };
98
- })))
99
- .filter((entry) => Boolean(entry))
100
- .sort((left, right) => right.date.localeCompare(left.date) || left.title.localeCompare(right.title));
101
- return entries;
102
- };
103
- export const buildContentSection = async (section, contentPath, outputDirectory, renderOptions) => {
104
- const entries = await buildContentEntries(section, contentPath, renderOptions.defaultTitle);
105
- const pageSize = section.pageSize ?? 10;
106
- const pages = Math.max(1, Math.ceil(entries.length / pageSize));
107
- const indexPaths = Array.from({ length: pages }, (_value, index) => getContentIndexHref(section.path, index + 1));
108
- await Promise.all(indexPaths.map((_path, index) => {
109
- const page = index + 1;
110
- const pageEntries = entries.slice(index * pageSize, page * pageSize);
111
- return writeDocument(getContentIndexOutputPath(outputDirectory, section.path, page), renderContentIndex(pageEntries, {
112
- ...renderOptions,
113
- page,
114
- pages,
115
- sectionPath: section.path,
116
- sectionTitle: section.title,
117
- }));
118
- }));
119
- return {
120
- entries,
121
- indexPaths,
122
- };
123
- };
124
- export const writeContentEntryDocuments = async (section, entries, outputDirectory, renderOptions) => {
125
- await Promise.all(entries.map((entry) => writeDocument(join(outputDirectory, section.path, entry.slug, 'index.html'), renderPage(entry, {
126
- ...renderOptions,
127
- canonicalPath: entry.href,
128
- related: renderOptions.relatedDocuments?.[`${section.path}:${entry.slug}`] ?? [],
129
- showTags: true,
130
- }))));
131
- };
132
- const getOptionalDate = (date) => date || undefined;
133
51
  export const getSearchDocuments = (section, entries) => {
134
52
  return entries.map((entry) => ({
135
53
  content: getPlainTextFromMarkdown(entry.content),
@@ -141,7 +59,7 @@ export const getSearchDocuments = (section, entries) => {
141
59
  tags: entry.tags,
142
60
  title: entry.title,
143
61
  updatedAt: entry.updatedAt,
144
- url: entry.href,
62
+ url: getContentUrl(section, entry.slug),
145
63
  }));
146
64
  };
147
65
  export const getSiteSearchDocuments = (sections) => {
@@ -152,7 +70,6 @@ export const getMiniSearchIndexJson = (documents) => {
152
70
  miniSearch.addAll([...documents]);
153
71
  return miniSearch.toJSON();
154
72
  };
155
- const wordPattern = /[\p{L}\p{N}]+/gu;
156
73
  const getKeywords = (text) => {
157
74
  return new Set((text.toLowerCase().match(wordPattern) ?? []).map((word) => word.trim()).filter((word) => word.length >= 4));
158
75
  };
@@ -216,7 +133,7 @@ export const getHomePageFeaturedContent = (sections) => {
216
133
  slug: entry.slug,
217
134
  tags: entry.tags,
218
135
  title: entry.title,
219
- url: entry.href,
136
+ url: getContentUrl(section, entry.slug),
220
137
  }))
221
138
  .slice(0, homePageFeaturedContentCount);
222
139
  };
@@ -0,0 +1,11 @@
1
+ import type { RelatedDocumentsMap } from './content-data.js';
2
+ import type { ContentEntry, ContentSection, ContentSectionBuildResult } from './types.js';
3
+ export declare const buildContentEntries: (section: ContentSection, projectRoot: string) => Promise<ContentEntry[]>;
4
+ export declare const writeContentEntryDocuments: (section: ContentSection, entries: readonly ContentEntry[], outputDirectory: string, { googleAnalyticsMeasurementId, relatedDocuments, siteUrl, stylesheetContent, stylesheetHref, }?: {
5
+ readonly googleAnalyticsMeasurementId?: string;
6
+ readonly relatedDocuments?: RelatedDocumentsMap;
7
+ readonly siteUrl?: string;
8
+ readonly stylesheetContent?: string;
9
+ readonly stylesheetHref?: string;
10
+ }) => Promise<void>;
11
+ export declare const buildContentSection: (section: ContentSection, projectRoot: string, outputDirectory: string, stylesheetHref?: string, siteUrl?: string, stylesheetContent?: string, googleAnalyticsMeasurementId?: string) => Promise<ContentSectionBuildResult>;