support for rss/calendar

This commit is contained in:
James Brumond 2023-05-20 19:08:43 -07:00
parent 5689c64c4e
commit e5f7af48cb
Signed by: james
GPG Key ID: E8F2FC44BAA3357A
14 changed files with 782 additions and 90 deletions

234
package-lock.json generated
View File

@ -12,6 +12,8 @@
"@doc-utils/jsonschema2markdown": "^0.1.1",
"@doc-utils/markdown2html": "^0.2.1",
"glob": "^10.2.3",
"ical": "^0.8.0",
"ical-generator": "^4.1.0",
"luxon": "^3.3.0",
"mustache": "^4.2.0",
"xmlbuilder2": "^3.1.1",
@ -21,6 +23,7 @@
"docs2website": "bin/docs2website"
},
"devDependencies": {
"@types/ical": "^0.8.0",
"@types/jsdom": "^20.0.1",
"@types/luxon": "^3.3.0",
"@types/mustache": "^4.2.2",
@ -152,6 +155,43 @@
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.4.tgz",
"integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q=="
},
"node_modules/@types/ical": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@types/ical/-/ical-0.8.0.tgz",
"integrity": "sha512-46KAYxAwRuCh+jRgl9k5cTaXJJkB16gWpvMVPuog9UBBb2zXTf9M0MsfWYBQ21JohFSuMZfPTvk6tohqGr1tCg==",
"dev": true,
"dependencies": {
"rrule": "2.6.4"
}
},
"node_modules/@types/ical/node_modules/luxon": {
"version": "1.28.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz",
"integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==",
"dev": true,
"optional": true,
"engines": {
"node": "*"
}
},
"node_modules/@types/ical/node_modules/rrule": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.4.tgz",
"integrity": "sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==",
"dev": true,
"dependencies": {
"tslib": "^1.10.0"
},
"optionalDependencies": {
"luxon": "^1.21.3"
}
},
"node_modules/@types/ical/node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
},
"node_modules/@types/jsdom": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
@ -167,7 +207,7 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz",
"integrity": "sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==",
"dev": true
"devOptional": true
},
"node_modules/@types/mustache": {
"version": "4.2.2",
@ -179,7 +219,7 @@
"version": "18.16.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.13.tgz",
"integrity": "sha512-uZRomboV1vBL61EBXneL4j9/hEn+1Yqa4LQdpGrKmXFyJmVfWc9JV9+yb2AlnOnuaDnb2PDO3hC6/LKmzJxP1A==",
"dev": true
"devOptional": true
},
"node_modules/@types/prismjs": {
"version": "1.26.0",
@ -1061,6 +1101,82 @@
"node": ">= 6"
}
},
"node_modules/ical": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/ical/-/ical-0.8.0.tgz",
"integrity": "sha512-/viUSb/RGLLnlgm0lWRlPBtVeQguQRErSPYl3ugnUaKUnzQswKqOG3M8/P1v1AB5NJwlHTuvTq1cs4mpeG2rCg==",
"dependencies": {
"rrule": "2.4.1"
}
},
"node_modules/ical-generator": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-4.1.0.tgz",
"integrity": "sha512-5GrFDJ8SAOj8cB9P1uEZIfKrNxSZ1R2eOQfZePL+CtdWh4RwNXWe8b0goajz+Hu37vcipG3RVldoa2j57Y20IA==",
"dependencies": {
"uuid-random": "^1.3.2"
},
"engines": {
"node": "^14.8.0 || >=16.0.0"
},
"peerDependencies": {
"@touch4it/ical-timezones": ">=1.6.0",
"@types/luxon": ">= 1.26.0",
"@types/mocha": ">= 8.2.1",
"@types/node": ">= 15.0.0",
"dayjs": ">= 1.10.0",
"luxon": ">= 1.26.0",
"moment": ">= 2.29.0",
"moment-timezone": ">= 0.5.33",
"rrule": ">= 2.6.8"
},
"peerDependenciesMeta": {
"@touch4it/ical-timezones": {
"optional": true
},
"@types/luxon": {
"optional": true
},
"@types/mocha": {
"optional": true
},
"@types/node": {
"optional": true
},
"dayjs": {
"optional": true
},
"luxon": {
"optional": true
},
"moment": {
"optional": true
},
"moment-timezone": {
"optional": true
},
"rrule": {
"optional": true
}
}
},
"node_modules/ical/node_modules/luxon": {
"version": "1.28.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz",
"integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==",
"optional": true,
"engines": {
"node": "*"
}
},
"node_modules/ical/node_modules/rrule": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.4.1.tgz",
"integrity": "sha512-+NcvhETefswZq13T8nkuEnnQ6YgUeZaqMqVbp+ZiFDPCbp3AVgQIwUvNVDdMNrP05bKZG9ddDULFp0qZZYDrxg==",
"optionalDependencies": {
"luxon": "^1.3.3"
}
},
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -1545,6 +1661,16 @@
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz",
"integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g=="
},
"node_modules/rrule": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.2.tgz",
"integrity": "sha512-NkBsEEB6FIZOZ3T8frvEBOB243dm46SPufpDckY/Ap/YH24V1zLeMmDY8OA10lk452NdrF621+ynDThE7FQU2A==",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
@ -1792,6 +1918,13 @@
"node": ">=12"
}
},
"node_modules/tslib": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
"integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==",
"optional": true,
"peer": true
},
"node_modules/type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
@ -1841,6 +1974,11 @@
"requires-port": "^1.0.0"
}
},
"node_modules/uuid-random": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz",
"integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ=="
},
"node_modules/vega": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/vega/-/vega-5.25.0.tgz",
@ -2637,6 +2775,40 @@
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.4.tgz",
"integrity": "sha512-MHmwBtCb7OCv1DSivz2UNJXPGU/1btAWRKlqJ2saEhVJkpkvqHMMaOpKg0v4sAbDWSQekHGvPVMM8nQ+Jen03Q=="
},
"@types/ical": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@types/ical/-/ical-0.8.0.tgz",
"integrity": "sha512-46KAYxAwRuCh+jRgl9k5cTaXJJkB16gWpvMVPuog9UBBb2zXTf9M0MsfWYBQ21JohFSuMZfPTvk6tohqGr1tCg==",
"dev": true,
"requires": {
"rrule": "2.6.4"
},
"dependencies": {
"luxon": {
"version": "1.28.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz",
"integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==",
"dev": true,
"optional": true
},
"rrule": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.4.tgz",
"integrity": "sha512-sLdnh4lmjUqq8liFiOUXD5kWp/FcnbDLPwq5YAc/RrN6120XOPb86Ae5zxF7ttBVq8O3LxjjORMEit1baluahA==",
"dev": true,
"requires": {
"luxon": "^1.21.3",
"tslib": "^1.10.0"
}
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
}
}
},
"@types/jsdom": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
@ -2652,7 +2824,7 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz",
"integrity": "sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==",
"dev": true
"devOptional": true
},
"@types/mustache": {
"version": "4.2.2",
@ -2664,7 +2836,7 @@
"version": "18.16.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.13.tgz",
"integrity": "sha512-uZRomboV1vBL61EBXneL4j9/hEn+1Yqa4LQdpGrKmXFyJmVfWc9JV9+yb2AlnOnuaDnb2PDO3hC6/LKmzJxP1A==",
"dev": true
"devOptional": true
},
"@types/prismjs": {
"version": "1.26.0",
@ -3307,6 +3479,38 @@
"debug": "4"
}
},
"ical": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/ical/-/ical-0.8.0.tgz",
"integrity": "sha512-/viUSb/RGLLnlgm0lWRlPBtVeQguQRErSPYl3ugnUaKUnzQswKqOG3M8/P1v1AB5NJwlHTuvTq1cs4mpeG2rCg==",
"requires": {
"rrule": "2.4.1"
},
"dependencies": {
"luxon": {
"version": "1.28.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.1.tgz",
"integrity": "sha512-gYHAa180mKrNIUJCbwpmD0aTu9kV0dREDrwNnuyFAsO1Wt0EVYSZelPnJlbj9HplzXX/YWXHFTL45kvZ53M0pw==",
"optional": true
},
"rrule": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.4.1.tgz",
"integrity": "sha512-+NcvhETefswZq13T8nkuEnnQ6YgUeZaqMqVbp+ZiFDPCbp3AVgQIwUvNVDdMNrP05bKZG9ddDULFp0qZZYDrxg==",
"requires": {
"luxon": "^1.3.3"
}
}
}
},
"ical-generator": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ical-generator/-/ical-generator-4.1.0.tgz",
"integrity": "sha512-5GrFDJ8SAOj8cB9P1uEZIfKrNxSZ1R2eOQfZePL+CtdWh4RwNXWe8b0goajz+Hu37vcipG3RVldoa2j57Y20IA==",
"requires": {
"uuid-random": "^1.3.2"
}
},
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -3650,6 +3854,16 @@
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.1.tgz",
"integrity": "sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g=="
},
"rrule": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.2.tgz",
"integrity": "sha512-NkBsEEB6FIZOZ3T8frvEBOB243dm46SPufpDckY/Ap/YH24V1zLeMmDY8OA10lk452NdrF621+ynDThE7FQU2A==",
"optional": true,
"peer": true,
"requires": {
"tslib": "^2.4.0"
}
},
"rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
@ -3835,6 +4049,13 @@
"punycode": "^2.1.1"
}
},
"tslib": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
"integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==",
"optional": true,
"peer": true
},
"type-check": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
@ -3868,6 +4089,11 @@
"requires-port": "^1.0.0"
}
},
"uuid-random": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz",
"integrity": "sha512-UOzej0Le/UgkbWEO8flm+0y+G+ljUon1QWTEZOq1rnMAsxo2+SckbiZdKzAHHlVh6gJqI1TjC/xwgR50MuCrBQ=="
},
"vega": {
"version": "5.25.0",
"resolved": "https://registry.npmjs.org/vega/-/vega-5.25.0.tgz",

View File

@ -13,6 +13,7 @@
},
"main": "./build/index.js",
"devDependencies": {
"@types/ical": "^0.8.0",
"@types/jsdom": "^20.0.1",
"@types/luxon": "^3.3.0",
"@types/mustache": "^4.2.2",
@ -25,6 +26,8 @@
"@doc-utils/jsonschema2markdown": "^0.1.1",
"@doc-utils/markdown2html": "^0.2.1",
"glob": "^10.2.3",
"ical": "^0.8.0",
"ical-generator": "^4.1.0",
"luxon": "^3.3.0",
"mustache": "^4.2.0",
"xmlbuilder2": "^3.1.1",

View File

@ -1,12 +1,15 @@
import { dirname, join as path_join } from 'path';
import { mkdirp, write_text } from '../fs';
import { icons } from '../icons';
import { BuildState } from './state';
import { load_partials, FrontMatter, Context, load_layout, render_template } from '../template';
import { render_theme_css_properties } from '../themes';
import { render_markdown_to_html, render_markdown_to_html_inline_sync } from '@doc-utils/markdown2html';
import { mkdirp, write_text } from '../fs';
import { CalendarConfig, RSSConfig } from '../conf';
import { render_theme_css_properties } from '../themes';
import { load_partials, FrontMatter, Context, load_layout, render_template } from '../template';
import { render_markdown_to_html, render_markdown_to_html_inline_sync } from '@doc-utils/markdown2html';
import { RSSEntry } from './rss';
import { DateTime } from 'luxon';
import { EventEntry } from './icalendar';
export interface OutFileURL {
base_url: string;
@ -84,8 +87,12 @@ export function mustache_context(state: BuildState, page_url: string, frontmatte
page: frontmatter,
base_url: state.conf.base_url,
page_url: page_url,
site_title: state.conf.title,
author: get_author(state, frontmatter),
build_time: state.build_time,
icons: icons,
rss_feeds: state.conf.rss || [ ],
calendars: state.conf.calendars || [ ],
themes: Object.values(state.themes),
theme_groups: structuredClone(state.theme_groups),
markdown: {
@ -102,7 +109,7 @@ export function mustache_context(state: BuildState, page_url: string, frontmatte
};
}
export async function render_page(state: BuildState, out_file: string, out_url: OutFileURL, text: string, render_as_markdown: boolean, frontmatter?: any) {
export async function render_page(state: BuildState, in_file: string, out_file: string, out_url: OutFileURL, text: string, render_as_markdown: boolean, frontmatter?: any) {
if (render_as_markdown) {
const opts = Object.assign({ }, state.conf.markdown, {
base_url: out_url.abs_url
@ -127,69 +134,107 @@ export async function render_page(state: BuildState, out_file: string, out_url:
const rendered = render_template(text, context, layout, structuredClone(state.partials), tags);
await write_text(out_file, rendered);
handle_page_side_effects(state, out_file, out_url, frontmatter);
handle_page_side_effects(state, in_file, out_file, out_url, text, frontmatter);
}
function handle_page_side_effects(state: BuildState, out_file: string, out_url: OutFileURL, frontmatter?: any) {
function handle_page_side_effects(state: BuildState, in_file: string, out_file: string, out_url: OutFileURL, text: string, frontmatter?: any) {
// Only actual HTML webpages are registered with things like the RSS feed; This is
// to prevent things like CSS files showing up, which may be templated (and therefore
// pass through this function), but are not really "pages"
if (frontmatter && typeof frontmatter === 'object' && out_file.endsWith('.html')) {
if (state.conf.rss) {
if (Array.isArray(state.conf.rss)) {
for (const rss_conf of state.conf.rss) {
handle_rss(state, rss_conf, out_url, frontmatter);
}
}
else {
handle_rss(state, { }, out_url, frontmatter);
for (const [index, rss_conf] of Object.entries(state.conf.rss)) {
if (in_file.startsWith(rss_conf.in_dir + '/')) {
const entries = (state.rss[index] = state.rss[index] || [ ]);
handle_rss(state, rss_conf, entries, in_file, out_url, text, frontmatter);
}
}
}
if (state.conf.sitemap) {
handle_sitemap(state, out_url, frontmatter);
handle_event(state, out_url, frontmatter);
}
if (frontmatter?.event) {
if (state.conf.events) {
handle_event(state, in_file, out_url, frontmatter);
}
if (state.conf.calendars) {
for (const cal_conf of state.conf.calendars) {
handle_calendar(state, cal_conf, out_url, frontmatter);
for (const [index, cal_conf] of Object.entries(state.conf.calendars)) {
if (in_file.startsWith(cal_conf.in_dir + '/')) {
const entries = (state.rss[index] = state.rss[index] || [ ]);
handle_calendar(state, cal_conf, entries, in_file, out_url, frontmatter);
}
}
}
}
}
}
function handle_rss(state: BuildState, rss_conf: RSSConfig, out_url: OutFileURL, frontmatter: any) {
// const field = rss_conf
// const rss_frontmatter = state.
// //
}
function handle_rss(state: BuildState, rss_conf: RSSConfig, entries: RSSEntry[], in_file: string, out_url: OutFileURL, text: string, frontmatter: FrontMatter) {
const author_or_authors = get_author(state, frontmatter);
const author = Array.isArray(author_or_authors) ? author_or_authors[0] : author_or_authors;
function handle_sitemap(state: BuildState, out_url: OutFileURL, frontmatter: any) {
if (! state.conf.sitemap) {
return;
}
const sitemap_frontmatter = state.conf.sitemap.front_matter_field
? frontmatter?.[state.conf.sitemap.front_matter_field] || { }
: frontmatter?.sitemap || { };
state.sitemap.push({
entries.push({
url: out_url.abs_url,
lastmod: state.build_time.iso,
change_freq: sitemap_frontmatter?.change_freq,
priority: sitemap_frontmatter?.priority,
in_file: in_file,
html_content: text,
title: frontmatter?.title,
description: frontmatter?.description,
author_name: author?.name,
tags: frontmatter?.tags,
});
}
function handle_event(state: BuildState, out_url: OutFileURL, frontmatter: any) {
//
function handle_sitemap(state: BuildState, out_url: OutFileURL, frontmatter: FrontMatter) {
state.sitemap.push({
url: out_url.abs_url,
lastmod: state.build_time.iso,
change_freq: frontmatter?.sitemap?.change_freq,
priority: frontmatter?.sitemap?.priority,
});
}
function handle_calendar(state: BuildState, cal_conf: CalendarConfig, out_url: OutFileURL, frontmatter: any) {
//
function handle_event(state: BuildState, in_file: string, out_url: OutFileURL, frontmatter: FrontMatter) {
if (! state.conf.events) {
return;
}
export function file_hash_matches(state: BuildState, in_file: string, new_hash: string) {
const author_or_authors = get_author(state, frontmatter);
const author = Array.isArray(author_or_authors) ? author_or_authors[0] : author_or_authors;
state.events.push({
url: out_url.abs_url,
in_file: in_file,
title: frontmatter.title,
description: frontmatter.description,
author_name: author?.name,
author_email: author?.email,
start_time: frontmatter.event?.start_time,
end_time: frontmatter.event?.end_time,
time_zone: frontmatter.event?.time_zone,
});
}
function handle_calendar(state: BuildState, cal_conf: CalendarConfig, entries: EventEntry[], in_file: string, out_url: OutFileURL, frontmatter: FrontMatter) {
const author_or_authors = get_author(state, frontmatter);
const author = Array.isArray(author_or_authors) ? author_or_authors[0] : author_or_authors;
entries.push({
url: out_url.abs_url,
in_file: in_file,
title: frontmatter.title,
description: frontmatter.description,
author_name: author?.name,
author_email: author?.email,
start_time: frontmatter.event?.start_time,
end_time: frontmatter.event?.end_time,
time_zone: frontmatter.event?.time_zone,
});
}
export function config_hash_matches(state: BuildState) {
const { old_metadata, new_metadata } = state;
if (! old_metadata.last_build?.config_hash) {
@ -200,6 +245,10 @@ export function file_hash_matches(state: BuildState, in_file: string, new_hash:
return false;
}
return true;
}
export function file_hash_matches(state: BuildState, in_file: string, new_hash: string) {
in_file = in_file.slice(state.conf.input.root.length);
const old_hash = state.old_metadata.files[in_file]?.last_build_hash;
@ -217,7 +266,12 @@ export function file_hash_matches(state: BuildState, in_file: string, new_hash:
export function skip_file(state: BuildState, in_file: string, out_file: string, out_url: OutFileURL, frontmatter?: any) {
in_file = in_file.slice(state.conf.input.root.length);
state.new_metadata.files[in_file] = structuredClone(state.old_metadata?.files?.[in_file]);
handle_page_side_effects(state, out_file, out_url, frontmatter);
handle_page_side_effects(state, in_file, out_file, out_url, frontmatter);
}
export function copy_metadata(state: BuildState, in_file: string) {
in_file = in_file.slice(state.conf.input.root.length);
state.new_metadata.files[in_file] = structuredClone(state.old_metadata?.files?.[in_file]);
}
export function update_metadata(state: BuildState, in_file: string, hash: string) {
@ -228,3 +282,23 @@ export function update_metadata(state: BuildState, in_file: string, hash: string
last_updated_time: state.build_time.iso,
};
}
export function get_author(state: BuildState, frontmatter?: FrontMatter) {
if (! frontmatter?.author) {
return null;
}
if (Array.isArray(frontmatter.author)) {
const list = frontmatter.author
.map((author) => state.conf.authors?.[author])
.filter((author) => author);
if (! list.length) {
return null;
}
return list;
}
return state.conf.authors?.[frontmatter.author]
}

View File

@ -0,0 +1,131 @@
import { write_text } from '../fs';
import { BuildState } from './state';
import { map_input_file_to_output_file } from './helpers';
import { parseICS, CalendarComponent } from 'ical';
import create_calendar, { ICalEventData, ICalCalendarData } from 'ical-generator';
export type { ICalEventData, ICalCalendarData, ICalAttendeeData, ICalAttendeeStatus } from 'ical-generator';
export interface EventEntry {
url: string;
in_file: string;
title?: string;
description?: string;
author_name?: string;
author_email?: string;
start_time?: string;
end_time?: string;
time_zone?: `${string}/${string}`;
}
export async function write_events_and_calendars_if_needed(state: BuildState) {
if (state.conf.events) {
for (const entry of state.events) {
const event = icalendar_event(state, entry);
const cal_data: ICalCalendarData = {
prodId: {
company: 'jbrumond.me',
product: 'docs2website',
language: 'EN',
},
// ...
};
const calendar = create_icalendar(cal_data, event);
const out_file = await map_input_file_to_output_file(state, entry.in_file, [ '.html', '.md', '.markdown' ], '.isc');
await write_text(out_file, calendar);
}
}
if (state.conf.calendars) {
for (let index = 0; index < state.conf.calendars.length; index++) {
const cal_conf = state.conf.calendars[index];
const cal_entries = state.calendars[index];
for (const entry of cal_entries) {
const event = icalendar_event(state, entry);
const cal_data: ICalCalendarData = {
name: cal_conf.title,
prodId: {
company: 'jbrumond.me',
product: 'docs2website',
language: 'EN',
},
// ...
};
const calendar = create_icalendar(cal_data, event);
await write_text(cal_conf.out_file, calendar);
}
}
}
}
export function parse_icalendar(contents: string) {
const parsed = parseICS(contents);
const calendar: CalendarComponent[] = [ ];
for (const data of Object.values(parsed)) {
calendar.push(data);
}
return calendar;
}
export function create_icalendar(cal: ICalCalendarData, events: ICalEventData | ICalEventData[]) {
const calendar = create_calendar(cal);
if (Array.isArray(events)) {
for (const event of events) {
calendar.createEvent(event);
}
}
else {
calendar.createEvent(events);
}
return calendar.toString();
}
export function icalendar_event(state: BuildState, entry: EventEntry) : ICalEventData {
const in_file = entry.in_file.slice(state.conf.input.root.length);
const metadata = state.new_metadata.files[in_file];
return {
id: entry.url,
summary: entry.title,
description: entry.description || void 0,
start: entry.start_time,
end: entry.end_time,
url: entry.url,
timezone: entry.time_zone,
created: metadata.first_seen_time,
lastModified: metadata.last_updated_time,
organizer: {
name: entry.author_name || 'Unknown',
email: entry.author_email,
},
// attendees: post.mentions.flatMap((mention) : ICalAttendeeData | ICalAttendeeData[] => {
// if (mention.is_rsvp && mention.is_reply_to_this) {
// const ext = mention.external as ExternalEntry;
// const status = ext.rsvp_type === 'yes'
// ? 'ACCEPTED' as const
// : ext.rsvp_type === 'no'
// ? 'DECLINED' as const
// : ext.rsvp_type === 'maybe'
// ? 'TENTATIVE' as const
// : 'NEEDS-ACTION' as const;
// return {
// name: mention.author_name,
// rsvp: ext.rsvp_type === 'yes',
// status: status as ICalAttendeeStatus
// };
// }
// return [ ];
// }),
};
}

View File

@ -14,6 +14,8 @@ import { render_text_file_templates } from './mustache';
import { render_markdown_files } from './markdown';
import { render_json_schema_files } from './jsonschema';
import { write_sitemap_if_needed } from './sitemap';
import { write_rss_if_needed } from './rss';
import { write_events_and_calendars_if_needed } from './icalendar';
export { BuildState, ThemeGroups } from './state';
@ -55,7 +57,10 @@ export async function build_docs_project(conf: Config) {
},
extras: await load_extras(),
made_directories: new Set<string>(),
rss: [ ],
sitemap: [ ],
events: [ ],
calendars: [ ],
build_time: {
iso: now.toISO(),
rfc2822: now.toRFC2822(),
@ -87,8 +92,8 @@ export async function build_docs_project(conf: Config) {
// todo: other file types...
await write_sitemap_if_needed(state);
// todo: rss
// todo: events
await write_rss_if_needed(state);
await write_events_and_calendars_if_needed(state);
// Write the updated metadata file
await write_json(conf.metadata, state.new_metadata, true);

View File

@ -4,7 +4,7 @@ import { BuildState } from './state';
import { read_json, write_text, read_yaml } from '../fs';
import { build_markdown_from_json_schema } from '@doc-utils/jsonschema2markdown';
import { stringify as to_yaml } from 'yaml';
import { build_partials, file_hash_matches, map_input_file_to_output_file, map_output_file_to_url, render_page, skip_file, update_metadata } from './helpers';
import { build_partials, copy_metadata, file_hash_matches, map_input_file_to_output_file, map_output_file_to_url, render_page, skip_file, update_metadata } from './helpers';
export async function render_json_schema_files(state: BuildState) {
const promises: Promise<any>[] = [ ];
@ -113,10 +113,6 @@ export async function render_json_schema(state: BuildState, schema: unknown, in_
const out_url = map_output_file_to_url(state, out_file);
if (file_hash_matches(state, in_file, hash)) {
return skip_file(state, in_file, out_file, out_url, frontmatter);
}
const promises: Promise<any>[] = [ ];
const markdown = build_markdown_from_json_schema(schema);
@ -128,9 +124,14 @@ export async function render_json_schema(state: BuildState, schema: unknown, in_
}
promises.push(
render_page(state, out_file, out_url, markdown, true, frontmatter)
render_page(state, in_file, out_file, out_url, markdown, true, frontmatter)
);
await Promise.all(promises);
if (file_hash_matches(state, in_file, hash)) {
return copy_metadata(state, in_file);
}
update_metadata(state, in_file, hash);
}

View File

@ -2,7 +2,7 @@
import { glob } from 'glob';
import { read_text } from '../fs';
import { BuildState } from './state';
import { build_partials, file_hash_matches, map_input_file_to_output_file, map_output_file_to_url, render_page, skip_file, update_metadata } from './helpers';
import { build_partials, copy_metadata, file_hash_matches, map_input_file_to_output_file, map_output_file_to_url, render_page, skip_file, update_metadata } from './helpers';
export async function render_markdown_files(state: BuildState) {
const promises: Promise<any>[] = [ ];
@ -39,10 +39,11 @@ export async function render_markdown_file(state: BuildState, in_file: string) {
return;
}
await render_page(state, in_file, out_file, out_url, text, true, frontmatter);
if (file_hash_matches(state, in_file, hash)) {
return skip_file(state, in_file, out_file, out_url, frontmatter);
return copy_metadata(state, in_file);
}
await render_page(state, out_file, out_url, text, true, frontmatter);
update_metadata(state, in_file, hash);
}

View File

@ -2,7 +2,7 @@
import { glob } from 'glob';
import { read_text } from '../fs';
import { BuildState } from './state';
import { build_partials, file_hash_matches, map_input_file_to_output_file, map_output_file_to_url, render_page, skip_file, update_metadata } from './helpers';
import { build_partials, copy_metadata, file_hash_matches, map_input_file_to_output_file, map_output_file_to_url, render_page, skip_file, update_metadata } from './helpers';
export async function render_text_file_templates(state: BuildState) {
const promises: Promise<any>[] = [ ];
@ -39,10 +39,11 @@ export async function render_text_file_template(state: BuildState, in_file: stri
return;
}
await render_page(state, in_file, out_file, out_url, text, false, frontmatter);
if (file_hash_matches(state, in_file, hash)) {
return skip_file(state, in_file, out_file, out_url, frontmatter);
return copy_metadata(state, in_file);
}
await render_page(state, out_file, out_url, text, false, frontmatter);
update_metadata(state, in_file, hash);
}

131
src/build-files/rss.ts Normal file
View File

@ -0,0 +1,131 @@
import type { XMLBuilder } from 'xmlbuilder2/lib/interfaces';
import { create as create_xml } from 'xmlbuilder2';
import { BuildState } from './state';
import { write_text } from '../fs';
import { app_version } from '../conf';
import { map_output_file_to_url } from './helpers';
import { DateTime } from 'luxon';
export interface RSSEntry {
url: string;
in_file: string;
title?: string;
description?: string;
author_name?: string;
html_content?: string;
tags?: string[];
}
export async function write_rss_if_needed(state: BuildState) {
if (! state.conf.rss) {
return;
}
for (let index = 0; index < state.conf.rss.length; index++) {
const rss_conf = state.conf.rss[index];
const doc = create_xml({ version: '1.0', encoding: 'UTF-8' });
const { abs_url: self_url } = map_output_file_to_url(state, rss_conf.out_file);
if (rss_conf.xsl) {
for (const url of rss_conf.xsl) {
doc.ins('xml-stylesheet', `href="${url}" type="text/xsl"`);
}
}
const rss = doc.ele('rss', {
version: '2.0',
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'xmlns:content': 'http://purl.org/rss/1.0/modules/content/',
'xmlns:atom': 'http://www.w3.org/2005/Atom',
// 'xmlns:docs2website': 'urn:uuid:7fc4e5d4-f68e-11ed-b0d0-00155ddef564',
});
const channel = rss.ele('channel');
channel.ele('generator').txt(`docs2website ${app_version}`);
if (rss_conf.title) {
channel.ele('title').txt(rss_conf.title);
}
if (rss_conf.link) {
channel.ele('link').txt(rss_conf.link);
}
link(channel, 'application/rss+xml', 'self', self_url);
if (rss_conf.alternates?.length) {
for (const alt of rss_conf.alternates) {
link(channel, alt.type, 'alternate', alt.href);
}
}
if (rss_conf.description) {
channel.ele('description').txt(rss_conf.description);
}
if (rss_conf.language) {
channel.ele('language').txt(rss_conf.language);
}
if (rss_conf.copyright) {
channel.ele('copyright').txt(rss_conf.copyright);
}
channel.ele('lastBuildDate').txt(state.build_time.rfc2822);
// channel.ele('rating').txt(''); // see: https://www.w3.org/PICS/
// channel.ele('pubDate').txt(publish_date.toUTCString());
// channel.ele('categories').txt('');
// channel.ele('docs').txt('');
// channel.ele('managingEditor').txt('james@jbrumond.me');
// channel.ele('webMaster').txt('james@jbrumond.me');
// channel.ele('ttl').txt('');
// channel.ele('image').txt('');
// channel.ele('skipHours').txt('');
// channel.ele('skipDays').txt('');
for (const entry of state.rss[index] || [ ]) {
const item = channel.ele('item');
const in_file = entry.in_file.slice(state.conf.input.root.length);
const metadata = state.new_metadata.files[in_file];
item.ele('link').txt(entry.url);
if (entry.title) {
item.ele('title').txt(entry.title);
}
if (entry.description) {
item.ele('description').txt(entry.description);
}
if (entry.author_name) {
item.ele('dc:creator').ele({ $: entry.author_name });
}
if (entry.tags) {
entry.tags.forEach((tag) => item.ele('category').txt(tag));
}
item.ele('guid').txt(entry.url);
item.ele('pubDate').txt(DateTime.fromISO(metadata.first_seen_time).toRFC2822());
if (entry.html_content) {
item.ele('content:encoded').ele({ $: entry.html_content });
}
}
const xml = doc.toString({
indent: ' ',
prettyPrint: true,
});
await write_text(rss_conf.out_file, xml);
}
}
function link(channel: XMLBuilder, type: string, rel: string, href: string) {
// channel.ele('link', { href, rel, type, });
channel.ele('atom:link', { type, rel, href, });
}

View File

@ -3,6 +3,8 @@ import type { Config } from '../conf';
import type { Metadata } from '../metadata';
import type { ColorTheme } from '@doc-utils/color-themes';
import type { SitemapEntry } from './sitemap';
import { RSSEntry } from './rss';
import { EventEntry } from './icalendar';
export interface BuildState {
conf: Config;
@ -16,12 +18,10 @@ export interface BuildState {
made_directories: Set<string>;
old_metadata: Metadata;
new_metadata: Metadata;
// rss: {
// url: string;
// last_updated: string;
// }[];
rss: RSSEntry[][];
sitemap: SitemapEntry[];
// events: EventEntry[];
events: EventEntry[];
calendars: EventEntry[][];
build_time: {
iso: string;
rfc2822: string;

View File

@ -4,31 +4,59 @@ import { parse as parse_yaml } from 'yaml';
import { resolve as resolve_path, dirname } from 'path';
import { MarkdownOptions } from '@doc-utils/markdown2html';
export const app_version = require('../package.json').version;
export async function read_config(file: string) {
const path = resolve_path(process.cwd(), file);
const yaml = await fs.readFile(path, 'utf8');
const config = parse_yaml(yaml);
validate_config(config);
config.title = resolve_env_var_if_needed(config.title);
config.metadata = resolve_env_var_if_needed(config.metadata);
config.base_url = resolve_env_var_if_needed(config.base_url);
config.input.root = resolve_env_var_if_needed(config.input.root);
config.output.root = resolve_env_var_if_needed(config.output.root);
process_markdown_config(config);
resolve_paths(path, config);
return config;
}
export interface RSSConfig {
dir_path?: string;
out_file?: string;
front_matter_field?: string;
in_dir: string;
out_file: string;
title?: string;
description?: string;
link?: string;
xsl?: string[];
language?: string;
copyright?: string;
alternates?: {
type: string;
href: string;
}[];
}
export interface CalendarConfig {
dir_path?: string;
in_dir?: string;
out_file?: string;
front_matter_field?: string;
title?: string;
}
export interface AuthorConfig {
name?: string;
email?: string;
url?: string;
}
export interface Config {
title: string;
metadata: string;
base_url: string;
authors?: Record<string, AuthorConfig>;
input: {
root: string;
raw?: string[];
@ -55,11 +83,9 @@ export interface Config {
};
sitemap?: false | {
out_file?: string;
front_matter_field?: string;
};
rss?: false | RSSConfig[];
events?: false | {
front_matter_field?: string;
};
calendars?: false | CalendarConfig[];
markdown?: Omit<MarkdownOptions, 'base_url' | 'inline' | 'extensions'>;
@ -68,7 +94,7 @@ export interface Config {
};
openapi?: {
// ;
}
};
// ...
}
@ -100,8 +126,8 @@ function resolve_paths(file_path: string, config: Config) {
if (Array.isArray(config.rss)) {
for (const rss_conf of config.rss) {
if (rss_conf.dir_path) {
rss_conf.dir_path = resolve_path(config.input.root, rss_conf.dir_path);
if (rss_conf.in_dir) {
rss_conf.in_dir = resolve_path(config.input.root, rss_conf.in_dir);
rss_conf.out_file = resolve_path(config.output.root, rss_conf.out_file);
}
}
@ -113,8 +139,8 @@ function resolve_paths(file_path: string, config: Config) {
if (Array.isArray(config.calendars)) {
for (const cal_conf of config.calendars) {
if (cal_conf.dir_path) {
cal_conf.dir_path = resolve_path(config.input.root, cal_conf.dir_path);
if (cal_conf.in_dir) {
cal_conf.in_dir = resolve_path(config.input.root, cal_conf.in_dir);
cal_conf.out_file = resolve_path(config.output.root, cal_conf.out_file);
}
}
@ -136,3 +162,11 @@ function process_markdown_config(config: any) {
}
}
}
function resolve_env_var_if_needed<T>(value: T | { from_env: string }) : T | string {
if (typeof value === 'object' && 'from_env' in value) {
return process.env[value.from_env];
}
return value;
}

View File

@ -1,16 +1,20 @@
export interface Metadata {
last_build: {
time: string;
config_hash: string;
};
last_build: BuildMetadata;
files: {
[file_path: string]: {
first_seen_time: string;
last_build_hash: string;
last_updated_time: string;
};
[file_path: string]: FileMetadata;
};
}
export interface BuildMetadata {
time: string;
config_hash: string;
}
export interface FileMetadata {
first_seen_time: string;
last_build_hash: string;
last_updated_time: string;
}
//

View File

@ -1,5 +1,5 @@
import { Config } from './conf';
import { AuthorConfig, CalendarConfig, Config, RSSConfig } from './conf';
import { render as mustache_render } from 'mustache';
import { promises as fs } from 'fs';
import { resolve as resolve_path } from 'path';
@ -7,15 +7,20 @@ import { glob } from 'glob';
import { load_from_dir } from './fs';
import { ColorTheme } from '@doc-utils/color-themes';
import { ThemeGroups } from './build-files';
import { ChangeFreq } from './build-files/sitemap';
export interface Context {
env?: Record<string, string>;
page?: FrontMatter;
base_url: string;
page_url: string;
site_title: string;
author: AuthorConfig | AuthorConfig[];
icons: Record<string, string>;
themes: ColorTheme[];
theme_groups: ThemeGroups;
rss_feeds: RSSConfig[];
calendars: CalendarConfig[];
build_time: {
iso: string;
rfc2822: string;
@ -26,11 +31,33 @@ export interface Context {
}
export interface FrontMatter {
title?: string;
skip?: boolean;
layout?: string;
title?: string;
description?: string;
tags?: string[];
author?: string | string[];
rss?: RSSFrontmatter;
sitemap?: SitemapFrontmatter;
event?: EventFrontmatter;
[key: string]: unknown;
}
interface SitemapFrontmatter {
change_freq?: ChangeFreq;
priority?: number;
}
interface EventFrontmatter {
start_time?: string;
end_time?: string;
time_zone?: `${string}/${string}`;
}
interface RSSFrontmatter {
//
}
export function render_template(template: string, context: Context, layout?: string, partials: Record<string, string> = { }, tags?: [ string, string ]) {
partials['.content'] = template;
return mustache_render(layout || template, context, partials, tags);

54
src/time.ts Normal file
View File

@ -0,0 +1,54 @@
import { DateTime } from 'luxon';
export function now(zone?: string) {
return zone
? DateTime.now().setZone(zone)
: DateTime.now();
}
export function from_iso(time: string, zone?: string) {
return zone
? DateTime.fromISO(time, { zone })
: DateTime.fromISO(time);
}
// function date_formatters(lang: string, time_zone: string) {
// return {
// date() {
// return (text, render) => {
// return format_with_config(render(text), {
// dateStyle: 'short',
// timeZone: time_zone,
// });
// };
// },
// time() {
// return (text, render) => {
// return format_with_config(render(text), {
// timeStyle: 'long',
// timeZone: time_zone,
// });
// };
// },
// datetime() {
// return (text, render) => {
// return format_with_config(render(text), {
// dateStyle: 'short',
// timeStyle: 'long',
// timeZone: time_zone,
// });
// };
// },
// };
// function format_with_config(text: string, config: Intl.DateTimeFormatOptions) {
// const date = new Date(text.trim());
// const formatter = new Intl.DateTimeFormat(lang, config);
// return formatter.format(date);
// }
// }