Compare commits
	
		
			30 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 95a20df58b | |||
| 0452c5fcf0 | |||
| 2636e42533 | |||
| 1a09a51780 | |||
| beb80145a4 | |||
| 0076cbb971 | |||
| bd9945b24b | |||
| 760af891c5 | |||
| f8b15f0ff4 | |||
| 20e585e708 | |||
| 3da2c28b13 | |||
| 35a2080714 | |||
| e99170f015 | |||
| f48d0a6194 | |||
| 92a9fe5685 | |||
| d0166b1db9 | |||
| 821456c226 | |||
| 5ff9cee8de | |||
| df4545a7f1 | |||
| 34c4144c3d | |||
| fbe142f12f | |||
| bcd60473b6 | |||
| 46f7424bc9 | |||
| dc3b8ea5d8 | |||
| 5d36def32c | |||
| e3bf8dc139 | |||
| 7297f9cd7a | |||
| 2dd300f4dc | |||
| 17a406e546 | |||
| 5149be7c8c | 
							
								
								
									
										40
									
								
								.gitea/workflows/build-and-pubilsh.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.gitea/workflows/build-and-pubilsh.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  |  | ||||||
|  | name: Build and publish | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   workflow_dispatch: | ||||||
|  |     inputs: | ||||||
|  |       version: | ||||||
|  |         type: string | ||||||
|  |         description: Semver to publish | ||||||
|  |         required: true | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   build-and-publish: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     env: | ||||||
|  |       NPM_PUBLISH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||||||
|  |     steps: | ||||||
|  |       - name: Check out the repo | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |  | ||||||
|  |       - name: Use Node.js 20 | ||||||
|  |         uses: actions/setup-node@v3 | ||||||
|  |         with: | ||||||
|  |           node-version: 20 | ||||||
|  |  | ||||||
|  |       - name: Login to package registry | ||||||
|  |         run: | | ||||||
|  |           npm config set @doc-utils:registry https://gitea.jbrumond.me/api/packages/doc-utils/npm/ | ||||||
|  |           npm config set -- '//gitea.jbrumond.me/api/packages/doc-utils/npm/:_authToken' "$NPM_PUBLISH_TOKEN" | ||||||
|  |  | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: npm ci | ||||||
|  |  | ||||||
|  |       - name: Compile TypeScript | ||||||
|  |         run: npm run tsc | ||||||
|  |  | ||||||
|  |       - name: Publish package | ||||||
|  |         run: | | ||||||
|  |           npm version ${{ inputs.version }} --allow-same-version --git-tag-version false | ||||||
|  |           npm publish | ||||||
							
								
								
									
										40
									
								
								.gitea/workflows/build-and-test.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								.gitea/workflows/build-and-test.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  |  | ||||||
|  | name: Build and test | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |     - master | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |     - master | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   build-and-test: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       matrix: | ||||||
|  |         node-version: [18.x, 20.x] | ||||||
|  |     steps: | ||||||
|  |       - name: Check out the repo | ||||||
|  |         uses: actions/checkout@v3 | ||||||
|  |  | ||||||
|  |       - name: Use Node.js ${{ matrix.node-version }} | ||||||
|  |         uses: actions/setup-node@v3 | ||||||
|  |         with: | ||||||
|  |           node-version: ${{ matrix.node-version }} | ||||||
|  |  | ||||||
|  |       - name: Login to package registry | ||||||
|  |         run: | | ||||||
|  |           npm config set @doc-utils:registry https://gitea.jbrumond.me/api/packages/doc-utils/npm/ | ||||||
|  |           npm config set -- '//gitea.jbrumond.me/api/packages/doc-utils/npm/:_authToken' "$NPM_PUBLISH_TOKEN" | ||||||
|  |  | ||||||
|  |       - name: Install dependencies | ||||||
|  |         run: npm ci | ||||||
|  |  | ||||||
|  |       - name: Compile TypeScript | ||||||
|  |         run: npm run tsc | ||||||
|  |  | ||||||
|  |       # todo: tests | ||||||
|  |       - name: Run tests | ||||||
|  |         run: exit 0 | ||||||
| @@ -116,9 +116,16 @@ | |||||||
| 		const outline = [ ]; | 		const outline = [ ]; | ||||||
|  |  | ||||||
| 		for (const heading of headings) { | 		for (const heading of headings) { | ||||||
|  | 			const content = heading.cloneNode(true); | ||||||
|  | 			const anchor = content.querySelector('a.heading-anchor'); | ||||||
|  |  | ||||||
|  | 			if (anchor) { | ||||||
|  | 				anchor.parentNode.removeChild(anchor); | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			outline.push(` | 			outline.push(` | ||||||
| 			<li data-depth="${heading.tagName.toLowerCase()}"> | 			<li data-depth="${heading.tagName.toLowerCase()}"> | ||||||
| 				<a href="#${heading.id}">${heading.innerText}</a> | 				<a href="#${heading.id}">${content.innerHTML}</a> | ||||||
| 			</li> | 			</li> | ||||||
| 			`); | 			`); | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -92,9 +92,16 @@ | |||||||
| 		const outline = [ ]; | 		const outline = [ ]; | ||||||
|  |  | ||||||
| 		for (const heading of headings) { | 		for (const heading of headings) { | ||||||
|  | 			const content = heading.cloneNode(true); | ||||||
|  | 			const anchor = content.querySelector('a.heading-anchor'); | ||||||
|  |  | ||||||
|  | 			if (anchor) { | ||||||
|  | 				anchor.parentNode.removeChild(anchor); | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			outline.push(` | 			outline.push(` | ||||||
| 			<li data-depth="${heading.tagName.toLowerCase()}"> | 			<li data-depth="${heading.tagName.toLowerCase()}"> | ||||||
| 				<a href="#${heading.id}">${heading.innerText}</a> | 				<a href="#${heading.id}">${content.innerHTML}</a> | ||||||
| 			</li> | 			</li> | ||||||
| 			`); | 			`); | ||||||
| 		} | 		} | ||||||
|   | |||||||
							
								
								
									
										163
									
								
								extras/figures.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								extras/figures.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | |||||||
|  |  | ||||||
|  | figure[data-lang] { | ||||||
|  | 	margin-block: 2rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure a.view-svg { | ||||||
|  | 	font-size: 0.85rem; | ||||||
|  | 	font-family: var(--font-body); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-lang] svg { | ||||||
|  | 	display: block; | ||||||
|  | 	margin-inline: auto; | ||||||
|  | 	margin-block: 2rem; | ||||||
|  |  | ||||||
|  | 	/* The auto-scaling font-size from typography/*.css does bad things to a lot of SVGs. | ||||||
|  | 	 * The SVGs themselves are inherently scalable, so there is no need for it here anyway. */ | ||||||
|  | 	font-size: 16px !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-size='medium']:has(svg) { | ||||||
|  | 	margin-block: 4rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-size='large'] { | ||||||
|  | 	margin-block: 6rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-size='small'] svg { | ||||||
|  | 	max-width: 40rem; | ||||||
|  | 	max-height: min(20rem, 50vw); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-size='medium'] svg { | ||||||
|  | 	max-width: 60rem; | ||||||
|  | 	max-height: min(40rem, 50vw); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-size='large'] svg { | ||||||
|  | 	max-height: min(60rem, 80vw); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* figure[data-lang].big { | ||||||
|  | 	background: var(--theme-bg-main); | ||||||
|  | 	position: fixed; | ||||||
|  | 	top: 0; | ||||||
|  | 	bottom: 0; | ||||||
|  | 	left: 0; | ||||||
|  | 	right: 0; | ||||||
|  | 	z-index: 1000; | ||||||
|  | 	display: flex; | ||||||
|  | 	border: 0.25rem var(--theme-line) solid; | ||||||
|  | 	margin: 3rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-lang].big svg { | ||||||
|  | 	max-width: none !important; | ||||||
|  | 	max-height: none !important; | ||||||
|  | } */ | ||||||
|  |  | ||||||
|  | /* figure:is([data-lang='pikchr'], [data-lang='nomnoml'], [data-lang='bytefield']) svg text:not([fill^='var']) { */ | ||||||
|  | figure:is([data-lang='pikchr'], [data-lang='nomnoml'], [data-lang='bytefield']) svg text { | ||||||
|  | 	fill: var(--theme-text-body); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure:is([data-lang='pikchr'], [data-lang='nomnoml'], [data-lang='bytefield']) svg text:not([font-family~='Courier']) { | ||||||
|  | 	font-family: var(--font-body); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | [data-lang='bash:samp'] samp { | ||||||
|  | 	display: block; | ||||||
|  | 	margin-block-start: 0.5rem; | ||||||
|  | 	padding-block-start: 0.5rem; | ||||||
|  | 	border-block-start: 0.1rem solid var(--theme-line); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* === KaTeX === */ | ||||||
|  |  | ||||||
|  | .katex-display { | ||||||
|  | 	color: var(--theme-text-body); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .katex-display .katex { | ||||||
|  | 	font-size: 1.4rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* using "body" here to add specificity, to override styles from katex.min.css */ | ||||||
|  | body .katex-display { | ||||||
|  | 	margin: 2rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body figure.align-left .katex-display > .katex { | ||||||
|  | 	text-align: left; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | body figure.align-right .katex-display > .katex { | ||||||
|  | 	text-align: right; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :not(.katex-display) > .katex { | ||||||
|  | 	font-size: inherit; | ||||||
|  | 	margin-inline: 0.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .katex span[style~='color:transparent;'] { | ||||||
|  | 	user-select: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* === Pikchr */ | ||||||
|  |  | ||||||
|  | /* boxes */ | ||||||
|  | figure[data-lang='pikchr'] svg path[style*='fill:none;'] { | ||||||
|  | 	fill: var(--theme-bg-light) !important; | ||||||
|  | 	transition: fill linear .5s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* lines and boxes */ | ||||||
|  | figure[data-lang='pikchr'] svg path[style*='stroke:rgb(0,0,0);'] { | ||||||
|  | 	stroke: var(--theme-text-body) !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* circles */ | ||||||
|  | figure[data-lang='pikchr'] svg circle[style*='stroke:rgb(0,0,0);'] { | ||||||
|  | 	stroke: var(--theme-text-body) !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* arrow heads */ | ||||||
|  | figure[data-lang='pikchr'] svg polygon[style='fill:rgb(0,0,0)'] { | ||||||
|  | 	fill: var(--theme-text-body) !important; | ||||||
|  | 	transition: fill linear .5s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* === Bytefield === */ | ||||||
|  |  | ||||||
|  | figure[data-lang='clojure:bytefield'] svg line[stroke-width='1'] { | ||||||
|  | 	stroke-width: 2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-lang='clojure:bytefield'] svg :is(text, tspan)[font-size='11'] { | ||||||
|  | 	font-size: 14px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-lang='clojure:bytefield'] svg :is(text, tspan)[font-size='18'] { | ||||||
|  | 	font-size: 16px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-lang='clojure:bytefield'] svg :is(text, tspan)[font-family~='Courier'] { | ||||||
|  | 	font-family: var(--font-monospace); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-lang='clojure:bytefield'] svg :is(text, tspan)[font-family~='Times'] { | ||||||
|  | 	font-family: var(--font-body); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-lang='clojure:bytefield'] svg line[stroke-dasharray='1,1'] { | ||||||
|  | 	stroke-dasharray: 4px, 3px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | figure[data-lang='clojure:bytefield'] svg line[stroke-dasharray='1,3'] { | ||||||
|  | 	stroke-dasharray: 2px, 3px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
							
								
								
									
										58
									
								
								extras/svg-links.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								extras/svg-links.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | (() => { | ||||||
|  | 	 | ||||||
|  | 	if (document.readyState === 'complete') { | ||||||
|  | 		add_svg_links(); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	else { | ||||||
|  | 		window.addEventListener('DOMContentLoaded', add_svg_links); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	function add_svg_links() { | ||||||
|  | 		const svg_elems = document.querySelectorAll('figure > svg'); | ||||||
|  |  | ||||||
|  | 		for (const svg_elem of svg_elems) { | ||||||
|  | 			const figure_elem = svg_elem.parentElement; | ||||||
|  | 			const anchor_elem = document.createElement('a'); | ||||||
|  | 			const lang = figure_elem.getAttribute('data-lang') || ''; | ||||||
|  |  | ||||||
|  | 			let svg_html; | ||||||
|  |  | ||||||
|  | 			if ('svg_link_css' in window) { | ||||||
|  | 				const svg_clone = svg_elem.cloneNode(true); | ||||||
|  | 				const style_elem = document.createElement('style'); | ||||||
|  | 				svg_clone.insertBefore(style_elem, svg_clone.firstChild); | ||||||
|  |  | ||||||
|  | 				const css | ||||||
|  | 					= (window.svg_link_css['*'] || '') | ||||||
|  | 					+ (lang ? window.svg_link_css[lang] || '' : ''); | ||||||
|  | 	 | ||||||
|  | 				style_elem.setAttribute('type', 'text/css'); | ||||||
|  | 				style_elem.appendChild(document.createTextNode(css)); | ||||||
|  |  | ||||||
|  | 				svg_html = svg_clone.outerHTML; | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			else { | ||||||
|  | 				svg_html = svg_elem.outerHTML; | ||||||
|  | 			} | ||||||
|  | 			 | ||||||
|  | 			const box = document.createElement('div'); | ||||||
|  | 			const svg_xml = '<?xml version="1.0" standalone="no" ?>\r\n' | ||||||
|  | 				+ svg_html.replace(/(&(?!(amp|gt|lt|quot|apos))[^;]+;)/g, ($0, $1) => { | ||||||
|  | 					box.innerHTML = $0; | ||||||
|  | 					return box.textContent; | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 			const svg_blob = new Blob([ svg_xml ], { type: 'image/svg+xml; charset=utf-8' }); | ||||||
|  | 			const svg_object_url = URL.createObjectURL(svg_blob); | ||||||
|  |  | ||||||
|  | 			anchor_elem.href = svg_object_url; | ||||||
|  | 			anchor_elem.className = 'view-svg'; | ||||||
|  | 			anchor_elem.textContent = 'View / Download Graphic'; | ||||||
|  |  | ||||||
|  | 			figure_elem.insertBefore(anchor_elem, svg_elem); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | })(); | ||||||
| @@ -52,3 +52,50 @@ li { | |||||||
| 	line-height: 1.5; | 	line-height: 1.5; | ||||||
| 	margin-block: 0.25rem; | 	margin-block: 0.25rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | dl { | ||||||
|  | 	padding: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | dd + dt { | ||||||
|  | 	margin-block-start: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :is(dt, dd) > p { | ||||||
|  | 	line-height: 1.5; | ||||||
|  | 	margin-block-start: 0; | ||||||
|  | 	margin-block-end: 0.15rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* ===== Figure Captions ===== */ | ||||||
|  |  | ||||||
|  | figcaption { | ||||||
|  | 	margin-block-start: 1.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* ===== Pre-formatted Blocks ===== */ | ||||||
|  |  | ||||||
|  | pre { | ||||||
|  | 	padding: 0.5rem; | ||||||
|  | 	margin-block: 1.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* ===== Note Blocks ===== */ | ||||||
|  |  | ||||||
|  | :is(aside, section):is([role='note'], .info, .highlight, .warning, .problem) > :first-child { | ||||||
|  | 	margin-block-start: 0.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :is(aside, section):is([role='note'], .info, .highlight, .warning, .problem) > :last-child { | ||||||
|  | 	margin-block-end: 0.25rem; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -80,12 +80,23 @@ p { | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* ===== Inline Styles ===== */ | ||||||
|  |  | ||||||
|  | del { | ||||||
|  | 	text-decoration: line-through; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /* ===== Links ===== */ | /* ===== Links ===== */ | ||||||
|  |  | ||||||
| a { | a { | ||||||
| 	font-family: inherit; | 	font-family: inherit; | ||||||
| 	color: var(--theme-text-link); | 	color: var(--theme-text-link); | ||||||
|  | 	display: inline; | ||||||
|  | 	align-items: center; | ||||||
|  | 	column-gap: 0.2rem; | ||||||
| } | } | ||||||
|  |  | ||||||
| a:active, | a:active, | ||||||
| @@ -98,7 +109,8 @@ a:visited { | |||||||
| 	color: var(--theme-text-link-visited); | 	color: var(--theme-text-link-visited); | ||||||
| } | } | ||||||
|  |  | ||||||
| a.icon-link { | a.icon-link, | ||||||
|  | a.inline-flex { | ||||||
| 	display: inline-flex; | 	display: inline-flex; | ||||||
| 	column-gap: 0.3rem; | 	column-gap: 0.3rem; | ||||||
| 	align-items: center; | 	align-items: center; | ||||||
| @@ -108,6 +120,10 @@ a.icon-link svg.icon { | |||||||
| 	--icon-size: 1rem; | 	--icon-size: 1rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | del a { | ||||||
|  | 	text-decoration: line-through underline; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -137,38 +153,10 @@ ul[role='doc-endnotes'] { | |||||||
| 	background: var(--theme-bg-heavy); | 	background: var(--theme-bg-heavy); | ||||||
| } | } | ||||||
|  |  | ||||||
| dl { |  | ||||||
| 	padding: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| dt, dd { |  | ||||||
| 	/*  */ |  | ||||||
| } |  | ||||||
|  |  | ||||||
| dt { | dt { | ||||||
| 	font-weight: 700; | 	font-weight: 700; | ||||||
| } | } | ||||||
|  |  | ||||||
| dt p { |  | ||||||
| 	margin-block-end: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| dt:not(:first-of-type) { |  | ||||||
| 	margin-block-start: 2rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| /* dd { |  | ||||||
| 	margin-inline-start: 1rem; |  | ||||||
| 	padding-inline-start: 1rem; |  | ||||||
| 	padding-block: 0.125rem; |  | ||||||
| 	border-inline-start: 0.5rem var(--theme-line) solid; |  | ||||||
| 	background: var(--theme-bg-heavy) |  | ||||||
| } */ |  | ||||||
|  |  | ||||||
| dd p { |  | ||||||
| 	margin-block: 1rem; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .cite-label { | .cite-label { | ||||||
| 	font-weight: 600; | 	font-weight: 600; | ||||||
| } | } | ||||||
| @@ -205,64 +193,64 @@ mark { | |||||||
|  |  | ||||||
| /* ===== Tables ===== */ | /* ===== Tables ===== */ | ||||||
|  |  | ||||||
| table, .table, td, .td { | table, .faux-table, td, .faux-td { | ||||||
| 	color: var(--theme-text-body); | 	color: var(--theme-text-body); | ||||||
| 	font-family: var(--font-body); | 	font-family: var(--font-body); | ||||||
| } | } | ||||||
|  |  | ||||||
| table, .table { | table, .faux-table { | ||||||
| 	display: table; | 	display: table; | ||||||
| 	margin-block: 2rem; | 	margin-block: 2rem; | ||||||
| 	border-collapse: collapse; | 	border-collapse: collapse; | ||||||
| } | } | ||||||
|  |  | ||||||
| table, th, td, | table, th, td, | ||||||
| .table, .th, .td { | .faux-table, .faux-th, .faux-td { | ||||||
| 	font-size: 1rem; | 	font-size: 1rem; | ||||||
| 	line-height: 1.75; | 	line-height: 1.75; | ||||||
| } | } | ||||||
|  |  | ||||||
| thead, .thead { | thead, .faux-thead { | ||||||
| 	display: table-header-group; | 	display: table-header-group; | ||||||
| } | } | ||||||
|  |  | ||||||
| tbody, .tbody { | tbody, .faux-tbody { | ||||||
| 	display: table-row-group; | 	display: table-row-group; | ||||||
| } | } | ||||||
|  |  | ||||||
| tbody tr, | tbody tr, | ||||||
| .tbody .tr { | .faux-tbody .faux-tr { | ||||||
| 	border-top: 1px var(--theme-line) solid; | 	border-top: 1px var(--theme-line) solid; | ||||||
| } | } | ||||||
|  |  | ||||||
| tr, .tr { | tr, .faux-tr { | ||||||
| 	display: table-row; | 	display: table-row; | ||||||
| } | } | ||||||
|  |  | ||||||
| th, .th { | th, .faux-th { | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
| 	font-weight: 700; | 	font-weight: 700; | ||||||
| 	color: var(--theme-text-heading); | 	color: var(--theme-text-heading); | ||||||
| } | } | ||||||
|  |  | ||||||
| th, .th, | th, .faux-th, | ||||||
| td, .td { | td, .faux-td { | ||||||
| 	padding-block: 0.5rem; | 	padding-block: 0.5rem; | ||||||
| 	padding-inline: 1rem; | 	padding-inline: 1rem; | ||||||
| 	display: table-cell; | 	display: table-cell; | ||||||
| } | } | ||||||
|  |  | ||||||
| td, .td { | td, .faux-td { | ||||||
| 	display: table-cell; | 	display: table-cell; | ||||||
| 	font-weight: 300; | 	font-weight: 300; | ||||||
| 	vertical-align: top; | 	vertical-align: top; | ||||||
| } | } | ||||||
|  |  | ||||||
| :is(td, .td):not(:last-of-type) { | :is(td, .faux-td):not(:last-of-type) { | ||||||
| 	border-inline-end: 0.1rem solid var(--theme-line); | 	border-inline-end: 0.1rem solid var(--theme-line); | ||||||
| } | } | ||||||
|  |  | ||||||
| :is(table, .table) :is(input, select) { | :is(table, .faux-table) :is(input, select) { | ||||||
| 	margin-block: 0; | 	margin-block: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -280,24 +268,28 @@ table dl { | |||||||
| pre { | pre { | ||||||
| 	color: var(--theme-code-normal); | 	color: var(--theme-code-normal); | ||||||
| 	font-family: var(--font-monospace); | 	font-family: var(--font-monospace); | ||||||
| 	margin-block: 3rem; | 	margin-inline-start: 1rem; | ||||||
| 	margin-inline: 2rem; | 	margin-inline-end: 5rem; | ||||||
| 	padding-block: 0.5rem; |  | ||||||
| 	padding-inline: 1rem; |  | ||||||
| 	border: 0.1rem solid var(--theme-line); | 	border: 0.1rem solid var(--theme-line); | ||||||
| 	border-radius: 1rem; | 	border-radius: 0.5rem; | ||||||
| 	font-size: 1rem; | 	font-size: 1rem; | ||||||
| 	overflow: auto; | 	overflow: auto; | ||||||
| 	background: var(--theme-bg-light); | 	background: var(--theme-bg-light); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @media screen and (max-width: 60rem) { | ||||||
|  | 	pre { | ||||||
|  | 		margin-inline: 0; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /* ===== Code / Sample Output ===== */ | /* ===== Code / Sample Output ===== */ | ||||||
|  |  | ||||||
| code, samp { | code, samp { | ||||||
| 	font-size: 1rem; | 	font-size: inherit; | ||||||
| 	color: var(--theme-code-normal); | 	color: var(--theme-code-normal); | ||||||
| 	font-family: var(--font-monospace); | 	font-family: var(--font-monospace); | ||||||
| } | } | ||||||
| @@ -305,8 +297,11 @@ code, samp { | |||||||
| :not(pre) > :is(code, samp) { | :not(pre) > :is(code, samp) { | ||||||
| 	color: inherit; | 	color: inherit; | ||||||
| 	background: var(--theme-bg-light); | 	background: var(--theme-bg-light); | ||||||
|  | 	margin-inline: 0.15rem; | ||||||
|  | 	padding-block: 0.1rem; | ||||||
| 	padding-inline: 0.25rem; | 	padding-inline: 0.25rem; | ||||||
| 	border: 0.1rem solid var(--theme-line); | 	border: 0.1rem solid var(--theme-line); | ||||||
|  | 	border-radius: 0.2rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -351,7 +346,6 @@ figcaption { | |||||||
| 	font-family: var(--font-body); | 	font-family: var(--font-body); | ||||||
| 	text-align: center; | 	text-align: center; | ||||||
| 	font-size: 0.85rem; | 	font-size: 0.85rem; | ||||||
| 	margin-block-start: 1rem; |  | ||||||
| 	color: var(--theme-text-light); | 	color: var(--theme-text-light); | ||||||
| 	max-width: 60vw; | 	max-width: 60vw; | ||||||
| 	margin-inline: auto; | 	margin-inline: auto; | ||||||
|   | |||||||
| @@ -45,6 +45,20 @@ p { | |||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* ===== Note Blocks ===== */ | ||||||
|  |  | ||||||
|  | :is(aside, section):is([role='note'], .info, .highlight, .warning, .problem) > :first-child { | ||||||
|  | 	margin-block-start: 0.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :is(aside, section):is([role='note'], .info, .highlight, .warning, .problem) > :last-child { | ||||||
|  | 	margin-block-end: 0.5rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| /* ===== Lists ===== */ | /* ===== Lists ===== */ | ||||||
|  |  | ||||||
| @@ -52,3 +66,36 @@ li { | |||||||
| 	line-height: 1.75; | 	line-height: 1.75; | ||||||
| 	margin-block: 0.5rem; | 	margin-block: 0.5rem; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | dl { | ||||||
|  | 	padding: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | dd + dt { | ||||||
|  | 	margin-block-start: 1.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | :is(dt, dd) > p { | ||||||
|  | 	margin-block-start: 0; | ||||||
|  | 	margin-block-end: 0.25rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* ===== Figure Captions ===== */ | ||||||
|  |  | ||||||
|  | figcaption { | ||||||
|  | 	margin-block-start: 2rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* ===== Pre-formatted Blocks ===== */ | ||||||
|  |  | ||||||
|  | pre { | ||||||
|  | 	margin-block: 2rem; | ||||||
|  | 	padding-block: 0.5rem; | ||||||
|  | 	padding-inline: 1rem; | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										35
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,16 +1,16 @@ | |||||||
| { | { | ||||||
|   "name": "@doc-utils/docs2website", |   "name": "@doc-utils/docs2website", | ||||||
|   "version": "0.1.5", |   "version": "0.2.1", | ||||||
|   "lockfileVersion": 2, |   "lockfileVersion": 2, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "@doc-utils/docs2website", |       "name": "@doc-utils/docs2website", | ||||||
|       "version": "0.1.5", |       "version": "0.2.1", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@doc-utils/color-themes": "^0.1.14", |         "@doc-utils/color-themes": "^0.2.0", | ||||||
|         "@doc-utils/jsonschema2markdown": "^0.1.1", |         "@doc-utils/jsonschema2markdown": "^0.1.1", | ||||||
|         "@doc-utils/markdown2html": "^0.2.1", |         "@doc-utils/markdown2html": "^0.3.6", | ||||||
|         "glob": "^10.2.3", |         "glob": "^10.2.3", | ||||||
|         "ical": "^0.8.0", |         "ical": "^0.8.0", | ||||||
|         "ical-generator": "^4.1.0", |         "ical-generator": "^4.1.0", | ||||||
| @@ -33,9 +33,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@doc-utils/color-themes": { |     "node_modules/@doc-utils/color-themes": { | ||||||
|       "version": "0.1.14", |       "version": "0.2.0", | ||||||
|       "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fcolor-themes/-/0.1.14/color-themes-0.1.14.tgz", |       "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fcolor-themes/-/0.2.0/color-themes-0.2.0.tgz", | ||||||
|       "integrity": "sha512-j0U8v8Y+9zAm9D7pbCheTQYGEKt9FSpKSZQNGsogxWl95S9Z7QMjtmJns6QPgdOsSDss7sjMLFS5Gm50GCMzNA==" |       "integrity": "sha512-UtjY25B8m4qdMvrmTPK3I1JXckbc1cvCOmIygHFBexpSWBQmb+sdoXfdCsoSpgPncurz1kwlEXffgPELCGtP8g==" | ||||||
|     }, |     }, | ||||||
|     "node_modules/@doc-utils/jsonschema2markdown": { |     "node_modules/@doc-utils/jsonschema2markdown": { | ||||||
|       "version": "0.1.1", |       "version": "0.1.1", | ||||||
| @@ -51,9 +51,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@doc-utils/markdown2html": { |     "node_modules/@doc-utils/markdown2html": { | ||||||
|       "version": "0.2.6", |       "version": "0.3.6", | ||||||
|       "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.2.6/markdown2html-0.2.6.tgz", |       "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.3.6/markdown2html-0.3.6.tgz", | ||||||
|       "integrity": "sha512-6cQNzthYOOlkT6rr6E1lpSJ4Zq991+chblSRxv69SVT8y5cIs6c9tFu75MXoIKX4+H68Q4i10gtMrrsXD+bkaA==", |       "integrity": "sha512-LCt5HAAcHz6lE+IqV2igQP7bbTq1juNbyn4KWqn3xe9eSslIyl7EvSaEH68WQtqEzX/iVFOvZz7qSoqsQeggXA==", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "bytefield-svg": "^1.6.1", |         "bytefield-svg": "^1.6.1", | ||||||
|         "dompurify": "^2.3.6", |         "dompurify": "^2.3.6", | ||||||
| @@ -66,6 +66,9 @@ | |||||||
|         "qrcode": "^1.5.1", |         "qrcode": "^1.5.1", | ||||||
|         "vega": "^5.22.1", |         "vega": "^5.22.1", | ||||||
|         "yaml": "^2.2.2" |         "yaml": "^2.2.2" | ||||||
|  |       }, | ||||||
|  |       "bin": { | ||||||
|  |         "markdown2html": "bin/markdown2html" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/@isaacs/cliui": { |     "node_modules/@isaacs/cliui": { | ||||||
| @@ -2674,9 +2677,9 @@ | |||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@doc-utils/color-themes": { |     "@doc-utils/color-themes": { | ||||||
|       "version": "0.1.14", |       "version": "0.2.0", | ||||||
|       "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fcolor-themes/-/0.1.14/color-themes-0.1.14.tgz", |       "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fcolor-themes/-/0.2.0/color-themes-0.2.0.tgz", | ||||||
|       "integrity": "sha512-j0U8v8Y+9zAm9D7pbCheTQYGEKt9FSpKSZQNGsogxWl95S9Z7QMjtmJns6QPgdOsSDss7sjMLFS5Gm50GCMzNA==" |       "integrity": "sha512-UtjY25B8m4qdMvrmTPK3I1JXckbc1cvCOmIygHFBexpSWBQmb+sdoXfdCsoSpgPncurz1kwlEXffgPELCGtP8g==" | ||||||
|     }, |     }, | ||||||
|     "@doc-utils/jsonschema2markdown": { |     "@doc-utils/jsonschema2markdown": { | ||||||
|       "version": "0.1.1", |       "version": "0.1.1", | ||||||
| @@ -2692,9 +2695,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "@doc-utils/markdown2html": { |     "@doc-utils/markdown2html": { | ||||||
|       "version": "0.2.6", |       "version": "0.3.6", | ||||||
|       "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.2.6/markdown2html-0.2.6.tgz", |       "resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fmarkdown2html/-/0.3.6/markdown2html-0.3.6.tgz", | ||||||
|       "integrity": "sha512-6cQNzthYOOlkT6rr6E1lpSJ4Zq991+chblSRxv69SVT8y5cIs6c9tFu75MXoIKX4+H68Q4i10gtMrrsXD+bkaA==", |       "integrity": "sha512-LCt5HAAcHz6lE+IqV2igQP7bbTq1juNbyn4KWqn3xe9eSslIyl7EvSaEH68WQtqEzX/iVFOvZz7qSoqsQeggXA==", | ||||||
|       "requires": { |       "requires": { | ||||||
|         "bytefield-svg": "^1.6.1", |         "bytefield-svg": "^1.6.1", | ||||||
|         "dompurify": "^2.3.6", |         "dompurify": "^2.3.6", | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,15 +1,15 @@ | |||||||
| { | { | ||||||
|   "name": "@doc-utils/docs2website", |   "name": "@doc-utils/docs2website", | ||||||
|   "version": "0.1.5", |   "version": "0.2.1", | ||||||
|   "publishConfig": { |   "publishConfig": { | ||||||
|     "registry": "https://gitea.home.jbrumond.me/api/packages/doc-utils/npm/" |     "registry": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/" | ||||||
|   }, |   }, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "tsc": "tsc", |     "tsc": "tsc", | ||||||
|     "clean": "rm -rf ./build ./www" |     "clean": "rm -rf ./build ./www" | ||||||
|   }, |   }, | ||||||
|   "bin": { |   "bin": { | ||||||
|     "docs2website": "./bin/docs2website.js" |     "docs2website": "./bin/docs2website" | ||||||
|   }, |   }, | ||||||
|   "main": "./build/index.js", |   "main": "./build/index.js", | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
| @@ -22,9 +22,9 @@ | |||||||
|     "typescript": "^5.0.4" |     "typescript": "^5.0.4" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@doc-utils/color-themes": "^0.1.14", |     "@doc-utils/color-themes": "^0.2.0", | ||||||
|     "@doc-utils/jsonschema2markdown": "^0.1.1", |     "@doc-utils/jsonschema2markdown": "^0.1.1", | ||||||
|     "@doc-utils/markdown2html": "^0.2.1", |     "@doc-utils/markdown2html": "^0.3.6", | ||||||
|     "glob": "^10.2.3", |     "glob": "^10.2.3", | ||||||
|     "ical": "^0.8.0", |     "ical": "^0.8.0", | ||||||
|     "ical-generator": "^4.1.0", |     "ical-generator": "^4.1.0", | ||||||
|   | |||||||
| @@ -5,11 +5,12 @@ import { BuildState } from './state'; | |||||||
| import { mkdirp, write_text } from '../fs'; | import { mkdirp, write_text } from '../fs'; | ||||||
| import { CalendarConfig, RSSConfig } from '../conf'; | import { CalendarConfig, RSSConfig } from '../conf'; | ||||||
| import { render_theme_css_properties } from '../themes'; | import { render_theme_css_properties } from '../themes'; | ||||||
| import { load_partials, FrontMatter, Context, load_layout, render_template } from '../template'; | import { load_partials, FrontMatter, Context, load_layout, render_template, EventFrontmatter } from '../template'; | ||||||
| import { render_markdown_to_html, render_markdown_to_html_inline_sync } from '@doc-utils/markdown2html'; | import { render_markdown_to_html, render_markdown_to_html_inline_sync } from '@doc-utils/markdown2html'; | ||||||
| import { RSSEntry } from './rss'; | import { RSSEntry } from './rss'; | ||||||
| import { DateTime } from 'luxon'; |  | ||||||
| import { EventEntry } from './icalendar'; | import { EventEntry } from './icalendar'; | ||||||
|  | import { as_context_time, from_iso } from '../time'; | ||||||
|  | import { FileMetadata } from '../metadata'; | ||||||
|  |  | ||||||
| export interface OutFileURL { | export interface OutFileURL { | ||||||
| 	base_url: string; | 	base_url: string; | ||||||
| @@ -29,9 +30,9 @@ export function map_output_file_to_url(state: BuildState, out_file: string, inde | |||||||
| 		rel_path = '/' + rel_path; | 		rel_path = '/' + rel_path; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	const base_url = state.conf.base_url.endsWith('/') | 	const base_url = state.conf.base_url?.endsWith('/') | ||||||
| 		? state.conf.base_url.slice(0, -1) | 		? state.conf.base_url.slice(0, -1) | ||||||
| 		: state.conf.base_url; | 		: (state.conf.base_url ?? ''); | ||||||
|  |  | ||||||
| 	return { | 	return { | ||||||
| 		base_url, | 		base_url, | ||||||
| @@ -81,14 +82,42 @@ export async function build_partials(state: BuildState) { | |||||||
| 	Object.assign(state.partials, state.extras); | 	Object.assign(state.partials, state.extras); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function mustache_context(state: BuildState, page_url: string, frontmatter?: FrontMatter) : Context { | export function mustache_context(state: BuildState, page_url: string, metadata: FileMetadata, frontmatter?: FrontMatter) : Context { | ||||||
|  | 	let event: Context['event']; | ||||||
|  |  | ||||||
|  | 	if (frontmatter?.event) { | ||||||
|  | 		event = Array.isArray(frontmatter.event) | ||||||
|  | 			? frontmatter.event.map(to_context_event) | ||||||
|  | 			: to_context_event(frontmatter.event); | ||||||
|  |  | ||||||
|  | 		function to_context_event(event_fm: EventFrontmatter) { | ||||||
|  | 			const start = from_iso(event_fm.start, event_fm.time_zone); | ||||||
|  | 			const end = from_iso(event_fm.end, event_fm.time_zone); | ||||||
|  | 			 | ||||||
|  | 			return { | ||||||
|  | 				start: as_context_time(start), | ||||||
|  | 				end: as_context_time(end), | ||||||
|  | 				time_zone: event_fm.time_zone, | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	const has_been_updated = metadata.first_seen_time !== metadata.last_updated_time; | ||||||
|  |  | ||||||
| 	return { | 	return { | ||||||
| 		env: state.env, | 		env: state.env, | ||||||
| 		page: frontmatter, | 		page: frontmatter, | ||||||
| 		base_url: state.conf.base_url, | 		base_url: state.conf.base_url ?? '/', | ||||||
| 		page_url: page_url, | 		page_url: page_url, | ||||||
|  | 		page_published: as_context_time(from_iso(metadata.first_seen_time), 'dt-published'), | ||||||
|  | 		page_updated: has_been_updated ? as_context_time(from_iso(metadata.last_updated_time), 'dt-updated') : null, | ||||||
| 		site_title: state.conf.title, | 		site_title: state.conf.title, | ||||||
| 		author: get_author(state, frontmatter), | 		author: get_author(state, frontmatter), | ||||||
|  | 		event: event, | ||||||
|  | 		event_series: Array.isArray(event) && { | ||||||
|  | 			start: event[0].start, | ||||||
|  | 			end: event[event.length - 1].end, | ||||||
|  | 		}, | ||||||
| 		build_time: state.build_time, | 		build_time: state.build_time, | ||||||
| 		icons: icons, | 		icons: icons, | ||||||
| 		rss_feeds: state.conf.rss || [ ], | 		rss_feeds: state.conf.rss || [ ], | ||||||
| @@ -109,10 +138,10 @@ export function mustache_context(state: BuildState, page_url: string, frontmatte | |||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function render_page(state: BuildState, in_file: string, 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, hash: string, frontmatter?: any) { | ||||||
| 	if (render_as_markdown) { | 	if (render_as_markdown) { | ||||||
| 		const opts = Object.assign({ }, state.conf.markdown, { | 		const opts = Object.assign({ }, state.conf.markdown, { | ||||||
| 			base_url: out_url.abs_url | 			// base_url: out_url.abs_url, | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		text = await render_markdown_to_html(text, opts); | 		text = await render_markdown_to_html(text, opts); | ||||||
| @@ -129,11 +158,21 @@ export async function render_page(state: BuildState, in_file: string, out_file: | |||||||
| 		layout = state.layouts[layout_file]; | 		layout = state.layouts[layout_file]; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	const hash_matches = file_hash_matches(state, in_file, hash); | ||||||
|  | 	const rel_in_file = in_file.slice(state.conf.input.root.length); | ||||||
|  | 	const old_metadata = state.old_metadata?.files?.[rel_in_file]; | ||||||
|  | 	const new_metadata = hash_matches ? structuredClone(old_metadata) : { | ||||||
|  | 		first_seen_time: old_metadata?.first_seen_time || state.build_time.iso, | ||||||
|  | 		last_build_hash: hash, | ||||||
|  | 		last_updated_time: state.build_time.iso, | ||||||
|  | 	}; | ||||||
|  |  | ||||||
| 	const tags = state.conf.templates?.tags; | 	const tags = state.conf.templates?.tags; | ||||||
| 	const context = mustache_context(state, out_url.abs_url, frontmatter); | 	const context = mustache_context(state, out_url.abs_url, new_metadata, frontmatter); | ||||||
| 	const rendered = render_template(text, context, layout, structuredClone(state.partials), tags); | 	const rendered = render_template(text, context, layout, structuredClone(state.partials), tags); | ||||||
| 	await write_text(out_file, rendered); | 	await write_text(out_file, rendered); | ||||||
|  |  | ||||||
|  | 	state.new_metadata.files[rel_in_file] = new_metadata; | ||||||
| 	handle_page_side_effects(state, in_file, out_file, out_url, text, frontmatter); | 	handle_page_side_effects(state, in_file, out_file, out_url, text, frontmatter); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -173,8 +212,12 @@ function handle_page_side_effects(state: BuildState, in_file: string, out_file: | |||||||
| } | } | ||||||
|  |  | ||||||
| function handle_rss(state: BuildState, rss_conf: RSSConfig, entries: RSSEntry[], in_file: string, out_url: OutFileURL, text: string, frontmatter: FrontMatter) { | function handle_rss(state: BuildState, rss_conf: RSSConfig, entries: RSSEntry[], in_file: string, out_url: OutFileURL, text: string, frontmatter: FrontMatter) { | ||||||
|  | 	if (frontmatter?.rss?.skip) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	const author_or_authors = get_author(state, frontmatter); | 	const author_or_authors = get_author(state, frontmatter); | ||||||
| 	const author = Array.isArray(author_or_authors) ? author_or_authors[0] : author_or_authors; | 	const authors = author_or_authors && (Array.isArray(author_or_authors) ? author_or_authors : [ author_or_authors ]); | ||||||
|  |  | ||||||
| 	entries.push({ | 	entries.push({ | ||||||
| 		url: out_url.abs_url, | 		url: out_url.abs_url, | ||||||
| @@ -182,12 +225,16 @@ function handle_rss(state: BuildState, rss_conf: RSSConfig, entries: RSSEntry[], | |||||||
| 		html_content: text, | 		html_content: text, | ||||||
| 		title: frontmatter?.title, | 		title: frontmatter?.title, | ||||||
| 		description: frontmatter?.description, | 		description: frontmatter?.description, | ||||||
| 		author_name: author?.name, | 		authors: authors, | ||||||
| 		tags: frontmatter?.tags, | 		tags: frontmatter?.tags, | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
|  |  | ||||||
| function handle_sitemap(state: BuildState, out_url: OutFileURL, frontmatter: FrontMatter) { | function handle_sitemap(state: BuildState, out_url: OutFileURL, frontmatter: FrontMatter) { | ||||||
|  | 	if (frontmatter?.sitemap?.skip) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	state.sitemap.push({ | 	state.sitemap.push({ | ||||||
| 		url: out_url.abs_url, | 		url: out_url.abs_url, | ||||||
| 		lastmod: state.build_time.iso, | 		lastmod: state.build_time.iso, | ||||||
| @@ -204,16 +251,30 @@ function handle_event(state: BuildState, in_file: string, out_url: OutFileURL, f | |||||||
| 	const author_or_authors = get_author(state, frontmatter); | 	const author_or_authors = get_author(state, frontmatter); | ||||||
| 	const author = Array.isArray(author_or_authors) ? author_or_authors[0] : author_or_authors; | 	const author = Array.isArray(author_or_authors) ? author_or_authors[0] : author_or_authors; | ||||||
|  |  | ||||||
|  | 	if (Array.isArray(frontmatter.event)) { | ||||||
|  | 		state.event_series.push({ | ||||||
|  | 			url: out_url.abs_url, | ||||||
|  | 			in_file: in_file, | ||||||
|  | 			title: frontmatter.title, | ||||||
|  | 			description: frontmatter.description, | ||||||
|  | 			author_name: author?.name, | ||||||
|  | 			author_email: author?.email, | ||||||
|  | 			entries: frontmatter.event.slice(), | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	state.events.push({ | 	state.events.push({ | ||||||
| 		url: out_url.abs_url, | 		url: out_url.abs_url, | ||||||
| 		in_file: in_file, | 		in_file: in_file, | ||||||
| 		title: frontmatter.title, | 		title: frontmatter.event.title || frontmatter.title, | ||||||
| 		description: frontmatter.description, | 		description: frontmatter.description, | ||||||
| 		author_name: author?.name, | 		author_name: author?.name, | ||||||
| 		author_email: author?.email, | 		author_email: author?.email, | ||||||
| 		start_time: frontmatter.event?.start_time, | 		start: frontmatter.event.start, | ||||||
| 		end_time: frontmatter.event?.end_time, | 		end: frontmatter.event.end, | ||||||
| 		time_zone: frontmatter.event?.time_zone, | 		time_zone: frontmatter.event.time_zone, | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -221,6 +282,12 @@ function handle_calendar(state: BuildState, cal_conf: CalendarConfig, entries: E | |||||||
| 	const author_or_authors = get_author(state, frontmatter); | 	const author_or_authors = get_author(state, frontmatter); | ||||||
| 	const author = Array.isArray(author_or_authors) ? author_or_authors[0] : author_or_authors; | 	const author = Array.isArray(author_or_authors) ? author_or_authors[0] : author_or_authors; | ||||||
|  |  | ||||||
|  | 	if (Array.isArray(frontmatter.event)) { | ||||||
|  | 		// todo: add each event to the calendar | ||||||
|  |  | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	entries.push({ | 	entries.push({ | ||||||
| 		url: out_url.abs_url, | 		url: out_url.abs_url, | ||||||
| 		in_file: in_file, | 		in_file: in_file, | ||||||
| @@ -228,8 +295,8 @@ function handle_calendar(state: BuildState, cal_conf: CalendarConfig, entries: E | |||||||
| 		description: frontmatter.description, | 		description: frontmatter.description, | ||||||
| 		author_name: author?.name, | 		author_name: author?.name, | ||||||
| 		author_email: author?.email, | 		author_email: author?.email, | ||||||
| 		start_time: frontmatter.event?.start_time, | 		start: frontmatter.event?.start, | ||||||
| 		end_time: frontmatter.event?.end_time, | 		end: frontmatter.event?.end, | ||||||
| 		time_zone: frontmatter.event?.time_zone, | 		time_zone: frontmatter.event?.time_zone, | ||||||
| 	}); | 	}); | ||||||
| } | } | ||||||
| @@ -269,20 +336,6 @@ export function skip_file(state: BuildState, in_file: string, out_file: string, | |||||||
| 	handle_page_side_effects(state, in_file, 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) { |  | ||||||
| 	in_file = in_file.slice(state.conf.input.root.length); |  | ||||||
| 	state.new_metadata.files[in_file] = { |  | ||||||
| 		first_seen_time: state.old_metadata?.files?.[in_file]?.first_seen_time || state.build_time.iso, |  | ||||||
| 		last_build_hash: hash, |  | ||||||
| 		last_updated_time: state.build_time.iso, |  | ||||||
| 	}; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function get_author(state: BuildState, frontmatter?: FrontMatter) { | export function get_author(state: BuildState, frontmatter?: FrontMatter) { | ||||||
| 	if (! frontmatter?.author) { | 	if (! frontmatter?.author) { | ||||||
| 		return null; | 		return null; | ||||||
|   | |||||||
| @@ -2,11 +2,28 @@ | |||||||
| import { write_text } from '../fs'; | import { write_text } from '../fs'; | ||||||
| import { BuildState } from './state'; | import { BuildState } from './state'; | ||||||
| import { map_input_file_to_output_file } from './helpers'; | import { map_input_file_to_output_file } from './helpers'; | ||||||
| import { parseICS, CalendarComponent } from 'ical'; | // import { parseICS, CalendarComponent } from 'ical'; | ||||||
| import create_calendar, { ICalEventData, ICalCalendarData } from 'ical-generator'; | import create_calendar, { ICalEventData, ICalCalendarData } from 'ical-generator'; | ||||||
|  | import { FrontMatterLocation } from '../template'; | ||||||
|  |  | ||||||
| export type { ICalEventData, ICalCalendarData, ICalAttendeeData, ICalAttendeeStatus } from 'ical-generator'; | export type { ICalEventData, ICalCalendarData, ICalAttendeeData, ICalAttendeeStatus } from 'ical-generator'; | ||||||
|  |  | ||||||
|  | export interface EventSeries { | ||||||
|  | 	url: string; | ||||||
|  | 	in_file: string; | ||||||
|  | 	title?: string; | ||||||
|  | 	description?: string; | ||||||
|  | 	author_name?: string; | ||||||
|  | 	author_email?: string; | ||||||
|  | 	entries: { | ||||||
|  | 		title?: string; | ||||||
|  | 		start?: string; | ||||||
|  | 		end?: string; | ||||||
|  | 		time_zone?: `${string}/${string}`; | ||||||
|  | 		location?: FrontMatterLocation; | ||||||
|  | 	}[]; | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface EventEntry { | export interface EventEntry { | ||||||
| 	url: string; | 	url: string; | ||||||
| 	in_file: string; | 	in_file: string; | ||||||
| @@ -14,9 +31,10 @@ export interface EventEntry { | |||||||
| 	description?: string; | 	description?: string; | ||||||
| 	author_name?: string; | 	author_name?: string; | ||||||
| 	author_email?: string; | 	author_email?: string; | ||||||
| 	start_time?: string; | 	start?: string; | ||||||
| 	end_time?: string; | 	end?: string; | ||||||
| 	time_zone?: `${string}/${string}`; | 	time_zone?: `${string}/${string}`; | ||||||
|  | 	location?: FrontMatterLocation; | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function write_events_and_calendars_if_needed(state: BuildState) { | export async function write_events_and_calendars_if_needed(state: BuildState) { | ||||||
| @@ -33,7 +51,40 @@ export async function write_events_and_calendars_if_needed(state: BuildState) { | |||||||
| 			}; | 			}; | ||||||
|  |  | ||||||
| 			const calendar = create_icalendar(cal_data, event); | 			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'); | 			const out_file = await map_input_file_to_output_file(state, entry.in_file, [ '.html', '.md', '.markdown' ], '.ics'); | ||||||
|  | 			await write_text(out_file, calendar); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for (const series of state.event_series) { | ||||||
|  | 			const events = series.entries.map((event) => { | ||||||
|  | 				return icalendar_event(state, { | ||||||
|  | 					url: series.url, | ||||||
|  | 					in_file: series.in_file, | ||||||
|  | 					title: event.title || series.title, | ||||||
|  | 					description: series.description, | ||||||
|  | 					author_name: series.author_name, | ||||||
|  | 					author_email: series.author_email, | ||||||
|  | 					start: event.start, | ||||||
|  | 					end: event.end, | ||||||
|  | 					time_zone: event.time_zone, | ||||||
|  | 					location: event.location, | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 			 | ||||||
|  | 			const cal_data: ICalCalendarData = { | ||||||
|  | 				name: series.title, | ||||||
|  | 				description: series.description, | ||||||
|  | 				url: series.url, | ||||||
|  | 				prodId: { | ||||||
|  | 					company: 'jbrumond.me', | ||||||
|  | 					product: 'docs2website', | ||||||
|  | 					language: 'EN', | ||||||
|  | 				}, | ||||||
|  | 				// ... | ||||||
|  | 			}; | ||||||
|  |  | ||||||
|  | 			const calendar = create_icalendar(cal_data, events); | ||||||
|  | 			const out_file = await map_input_file_to_output_file(state, series.in_file, [ '.html', '.md', '.markdown' ], '.ics'); | ||||||
| 			await write_text(out_file, calendar); | 			await write_text(out_file, calendar); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -62,16 +113,16 @@ export async function write_events_and_calendars_if_needed(state: BuildState) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| export function parse_icalendar(contents: string) { | // export function parse_icalendar(contents: string) { | ||||||
| 	const parsed = parseICS(contents); | // 	const parsed = parseICS(contents); | ||||||
| 	const calendar: CalendarComponent[] = [ ]; | // 	const calendar: CalendarComponent[] = [ ]; | ||||||
|  |  | ||||||
| 	for (const data of Object.values(parsed)) { | // 	for (const data of Object.values(parsed)) { | ||||||
| 		calendar.push(data); | // 		calendar.push(data); | ||||||
| 	} | // 	} | ||||||
|  |  | ||||||
| 	return calendar; | // 	return calendar; | ||||||
| } | // } | ||||||
|  |  | ||||||
| export function create_icalendar(cal: ICalCalendarData, events: ICalEventData | ICalEventData[]) { | export function create_icalendar(cal: ICalCalendarData, events: ICalEventData | ICalEventData[]) { | ||||||
| 	const calendar = create_calendar(cal); | 	const calendar = create_calendar(cal); | ||||||
| @@ -97,8 +148,8 @@ export function icalendar_event(state: BuildState, entry: EventEntry) : ICalEven | |||||||
| 		id: entry.url, | 		id: entry.url, | ||||||
| 		summary: entry.title, | 		summary: entry.title, | ||||||
| 		description: entry.description || void 0, | 		description: entry.description || void 0, | ||||||
| 		start: entry.start_time, | 		start: entry.start, | ||||||
| 		end: entry.end_time, | 		end: entry.end, | ||||||
| 		url: entry.url, | 		url: entry.url, | ||||||
| 		timezone: entry.time_zone, | 		timezone: entry.time_zone, | ||||||
| 		created: metadata.first_seen_time, | 		created: metadata.first_seen_time, | ||||||
| @@ -107,6 +158,7 @@ export function icalendar_event(state: BuildState, entry: EventEntry) : ICalEven | |||||||
| 			name: entry.author_name || 'Unknown', | 			name: entry.author_name || 'Unknown', | ||||||
| 			email: entry.author_email, | 			email: entry.author_email, | ||||||
| 		}, | 		}, | ||||||
|  | 		location: format_location(entry.location), | ||||||
| 		// attendees: post.mentions.flatMap((mention) : ICalAttendeeData | ICalAttendeeData[] => { | 		// attendees: post.mentions.flatMap((mention) : ICalAttendeeData | ICalAttendeeData[] => { | ||||||
| 		// 	if (mention.is_rsvp && mention.is_reply_to_this) { | 		// 	if (mention.is_rsvp && mention.is_reply_to_this) { | ||||||
| 		// 		const ext = mention.external as ExternalEntry; | 		// 		const ext = mention.external as ExternalEntry; | ||||||
| @@ -129,3 +181,29 @@ export function icalendar_event(state: BuildState, entry: EventEntry) : ICalEven | |||||||
| 		// }), | 		// }), | ||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function format_location(loc: FrontMatterLocation) { | ||||||
|  | 	if (! loc) { | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (loc.description) { | ||||||
|  | 		if (loc.address) { | ||||||
|  | 			return `${loc.description} (${loc.address})`; | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		if (loc.lat && loc.long) { | ||||||
|  | 			return `${loc.description} [${loc.lat}, ${loc.long}]`; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return loc.description; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (loc.address) { | ||||||
|  | 		return loc.address; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if (loc.lat && loc.long) { | ||||||
|  | 		return `[${loc.lat}, ${loc.long}]`; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import { render_json_schema_files } from './jsonschema'; | |||||||
| import { write_sitemap_if_needed } from './sitemap'; | import { write_sitemap_if_needed } from './sitemap'; | ||||||
| import { write_rss_if_needed } from './rss'; | import { write_rss_if_needed } from './rss'; | ||||||
| import { write_events_and_calendars_if_needed } from './icalendar'; | import { write_events_and_calendars_if_needed } from './icalendar'; | ||||||
|  | import { as_context_time, as_html_time } from '../time'; | ||||||
|  |  | ||||||
| export { BuildState, ThemeGroups } from './state'; | export { BuildState, ThemeGroups } from './state'; | ||||||
|  |  | ||||||
| @@ -60,11 +61,9 @@ export async function build_docs_project(conf: Config) { | |||||||
| 		rss: [ ], | 		rss: [ ], | ||||||
| 		sitemap: [ ], | 		sitemap: [ ], | ||||||
| 		events: [ ], | 		events: [ ], | ||||||
|  | 		event_series: [ ], | ||||||
| 		calendars: [ ], | 		calendars: [ ], | ||||||
| 		build_time: { | 		build_time: as_context_time(now), | ||||||
| 			iso: now.toISO(), |  | ||||||
| 			rfc2822: now.toRFC2822(), |  | ||||||
| 		}, |  | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	for (const theme of Object.values(themes)) { | 	for (const theme of Object.values(themes)) { | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { BuildState } from './state'; | |||||||
| import { read_json, write_text, read_yaml } from '../fs'; | import { read_json, write_text, read_yaml } from '../fs'; | ||||||
| import { build_markdown_from_json_schema } from '@doc-utils/jsonschema2markdown'; | import { build_markdown_from_json_schema } from '@doc-utils/jsonschema2markdown'; | ||||||
| import { stringify as to_yaml } from 'yaml'; | import { stringify as to_yaml } from 'yaml'; | ||||||
| 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'; | import { build_partials, map_input_file_to_output_file, map_output_file_to_url, render_page } from './helpers'; | ||||||
|  |  | ||||||
| export async function render_json_schema_files(state: BuildState) { | export async function render_json_schema_files(state: BuildState) { | ||||||
| 	const promises: Promise<any>[] = [ ]; | 	const promises: Promise<any>[] = [ ]; | ||||||
| @@ -124,14 +124,8 @@ export async function render_json_schema(state: BuildState, schema: unknown, in_ | |||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	promises.push( | 	promises.push( | ||||||
| 		render_page(state, in_file, out_file, out_url, markdown, true, frontmatter) | 		render_page(state, in_file, out_file, out_url, markdown, true, hash, frontmatter) | ||||||
| 	); | 	); | ||||||
| 	 | 	 | ||||||
| 	await Promise.all(promises); | 	await Promise.all(promises); | ||||||
|  |  | ||||||
| 	if (file_hash_matches(state, in_file, hash)) { |  | ||||||
| 		return copy_metadata(state, in_file); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	update_metadata(state, in_file, hash); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| import { glob } from 'glob'; | import { glob } from 'glob'; | ||||||
| import { read_text } from '../fs'; | import { read_text } from '../fs'; | ||||||
| import { BuildState } from './state'; | import { BuildState } from './state'; | ||||||
| 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'; | import { build_partials, map_input_file_to_output_file, map_output_file_to_url, render_page } from './helpers'; | ||||||
|  |  | ||||||
| export async function render_markdown_files(state: BuildState) { | export async function render_markdown_files(state: BuildState) { | ||||||
| 	const promises: Promise<any>[] = [ ]; | 	const promises: Promise<any>[] = [ ]; | ||||||
| @@ -34,16 +34,10 @@ export async function render_markdown_file(state: BuildState, in_file: string) { | |||||||
| 	const out_file = await map_input_file_to_output_file(state, in_file, [ '.md', '.markdown' ], '.html'); | 	const out_file = await map_input_file_to_output_file(state, in_file, [ '.md', '.markdown' ], '.html'); | ||||||
| 	const out_url = map_output_file_to_url(state, out_file); | 	const out_url = map_output_file_to_url(state, out_file); | ||||||
| 	const { frontmatter, text, hash } = await read_text(in_file); | 	const { frontmatter, text, hash } = await read_text(in_file); | ||||||
|  | 	 | ||||||
| 	if (frontmatter?.skip) { | 	if (frontmatter?.skip) { | ||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
| 	 | 	 | ||||||
| 	await render_page(state, in_file, out_file, out_url, text, true, frontmatter); | 	await render_page(state, in_file, out_file, out_url, text, true, hash, frontmatter); | ||||||
|  |  | ||||||
| 	if (file_hash_matches(state, in_file, hash)) { |  | ||||||
| 		return copy_metadata(state, in_file); |  | ||||||
| 	} |  | ||||||
| 	 |  | ||||||
| 	update_metadata(state, in_file, hash); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| import { glob } from 'glob'; | import { glob } from 'glob'; | ||||||
| import { read_text } from '../fs'; | import { read_text } from '../fs'; | ||||||
| import { BuildState } from './state'; | import { BuildState } from './state'; | ||||||
| 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'; | import { build_partials, map_input_file_to_output_file, map_output_file_to_url, render_page } from './helpers'; | ||||||
|  |  | ||||||
| export async function render_text_file_templates(state: BuildState) { | export async function render_text_file_templates(state: BuildState) { | ||||||
| 	const promises: Promise<any>[] = [ ]; | 	const promises: Promise<any>[] = [ ]; | ||||||
| @@ -39,11 +39,5 @@ export async function render_text_file_template(state: BuildState, in_file: stri | |||||||
| 		return; | 		return; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	await render_page(state, in_file, out_file, out_url, text, false, frontmatter); | 	await render_page(state, in_file, out_file, out_url, text, false, hash, frontmatter); | ||||||
|  |  | ||||||
| 	if (file_hash_matches(state, in_file, hash)) { |  | ||||||
| 		return copy_metadata(state, in_file); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	update_metadata(state, in_file, hash); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,16 +3,17 @@ import type { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; | |||||||
| import { create as create_xml } from 'xmlbuilder2'; | import { create as create_xml } from 'xmlbuilder2'; | ||||||
| import { BuildState } from './state'; | import { BuildState } from './state'; | ||||||
| import { write_text } from '../fs'; | import { write_text } from '../fs'; | ||||||
| import { app_version } from '../conf'; | import { AuthorConfig, app_version } from '../conf'; | ||||||
| import { map_output_file_to_url } from './helpers'; | import { map_output_file_to_url } from './helpers'; | ||||||
| import { DateTime } from 'luxon'; | import { DateTime } from 'luxon'; | ||||||
|  | import { FileMetadata } from '../metadata'; | ||||||
|  |  | ||||||
| export interface RSSEntry { | export interface RSSEntry { | ||||||
| 	url: string; | 	url: string; | ||||||
| 	in_file: string; | 	in_file: string; | ||||||
| 	title?: string; | 	title?: string; | ||||||
| 	description?: string; | 	description?: string; | ||||||
| 	author_name?: string; | 	authors?: AuthorConfig[]; | ||||||
| 	html_content?: string; | 	html_content?: string; | ||||||
| 	tags?: string[]; | 	tags?: string[]; | ||||||
| } | } | ||||||
| @@ -84,11 +85,21 @@ export async function write_rss_if_needed(state: BuildState) { | |||||||
| 		// channel.ele('image').txt(''); | 		// channel.ele('image').txt(''); | ||||||
| 		// channel.ele('skipHours').txt(''); | 		// channel.ele('skipHours').txt(''); | ||||||
| 		// channel.ele('skipDays').txt(''); | 		// channel.ele('skipDays').txt(''); | ||||||
|  |  | ||||||
|  | 		let entries: (RSSEntry & { metadata: FileMetadata, first_seen: number })[] = [ ]; | ||||||
| 	 | 	 | ||||||
| 		for (const entry of state.rss[index] || [ ]) { | 		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 in_file = entry.in_file.slice(state.conf.input.root.length); | ||||||
| 			const metadata = state.new_metadata.files[in_file]; | 			const metadata = state.new_metadata.files[in_file]; | ||||||
|  | 			entries.push({ ...entry, metadata, first_seen: DateTime.fromISO(metadata.first_seen_time).toUnixInteger() }); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		entries = entries.sort((a, b) => { | ||||||
|  | 			return b.first_seen - a.first_seen; | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		for (const entry of entries) { | ||||||
|  | 			const item = channel.ele('item'); | ||||||
| 	 | 	 | ||||||
| 			item.ele('link').txt(entry.url); | 			item.ele('link').txt(entry.url); | ||||||
|  |  | ||||||
| @@ -100,8 +111,10 @@ export async function write_rss_if_needed(state: BuildState) { | |||||||
| 				item.ele('description').txt(entry.description); | 				item.ele('description').txt(entry.description); | ||||||
| 			} | 			} | ||||||
| 			 | 			 | ||||||
| 			if (entry.author_name) { | 			if (entry.authors) { | ||||||
| 				item.ele('dc:creator').ele({ $: entry.author_name }); | 				for (const author of entry.authors) { | ||||||
|  | 					item.ele('dc:creator').ele({ $: author.name || author.url || author.email }); | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 	 | 	 | ||||||
| 			if (entry.tags) { | 			if (entry.tags) { | ||||||
| @@ -109,7 +122,7 @@ export async function write_rss_if_needed(state: BuildState) { | |||||||
| 			} | 			} | ||||||
| 	 | 	 | ||||||
| 			item.ele('guid').txt(entry.url); | 			item.ele('guid').txt(entry.url); | ||||||
| 			item.ele('pubDate').txt(DateTime.fromISO(metadata.first_seen_time).toRFC2822()); | 			item.ele('pubDate').txt(DateTime.fromISO(entry.metadata.first_seen_time).toRFC2822()); | ||||||
|  |  | ||||||
| 			if (entry.html_content) { | 			if (entry.html_content) { | ||||||
| 				item.ele('content:encoded').ele({ $: entry.html_content }); | 				item.ele('content:encoded').ele({ $: entry.html_content }); | ||||||
|   | |||||||
| @@ -4,7 +4,8 @@ import type { Metadata } from '../metadata'; | |||||||
| import type { ColorTheme } from '@doc-utils/color-themes'; | import type { ColorTheme } from '@doc-utils/color-themes'; | ||||||
| import type { SitemapEntry } from './sitemap'; | import type { SitemapEntry } from './sitemap'; | ||||||
| import { RSSEntry } from './rss'; | import { RSSEntry } from './rss'; | ||||||
| import { EventEntry } from './icalendar'; | import { EventEntry, EventSeries } from './icalendar'; | ||||||
|  | import { ContextTime } from '../template'; | ||||||
|  |  | ||||||
| export interface BuildState { | export interface BuildState { | ||||||
| 	conf: Config; | 	conf: Config; | ||||||
| @@ -21,11 +22,9 @@ export interface BuildState { | |||||||
| 	rss: RSSEntry[][]; | 	rss: RSSEntry[][]; | ||||||
| 	sitemap: SitemapEntry[]; | 	sitemap: SitemapEntry[]; | ||||||
| 	events: EventEntry[]; | 	events: EventEntry[]; | ||||||
|  | 	event_series: EventSeries[]; | ||||||
| 	calendars: EventEntry[][]; | 	calendars: EventEntry[][]; | ||||||
| 	build_time: { | 	build_time: ContextTime; | ||||||
| 		iso: string; |  | ||||||
| 		rfc2822: string; |  | ||||||
| 	}; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface ThemeGroups { | export interface ThemeGroups { | ||||||
|   | |||||||
| @@ -14,22 +14,48 @@ export interface Context { | |||||||
| 	page?: FrontMatter; | 	page?: FrontMatter; | ||||||
| 	base_url: string; | 	base_url: string; | ||||||
| 	page_url: string; | 	page_url: string; | ||||||
|  | 	page_published: ContextTime; | ||||||
|  | 	page_updated: ContextTime; | ||||||
| 	site_title: string; | 	site_title: string; | ||||||
| 	author: AuthorConfig | AuthorConfig[]; | 	author: AuthorConfig | AuthorConfig[]; | ||||||
|  | 	event?: ContextEvent | ContextEvent[]; | ||||||
|  | 	event_series?: { | ||||||
|  | 		start?: ContextTime; | ||||||
|  | 		end?: ContextTime; | ||||||
|  | 	}; | ||||||
| 	icons: Record<string, string>; | 	icons: Record<string, string>; | ||||||
| 	themes: ColorTheme[]; | 	themes: ColorTheme[]; | ||||||
| 	theme_groups: ThemeGroups; | 	theme_groups: ThemeGroups; | ||||||
| 	rss_feeds: RSSConfig[]; | 	rss_feeds: RSSConfig[]; | ||||||
| 	calendars: CalendarConfig[]; | 	calendars: CalendarConfig[]; | ||||||
| 	build_time: { | 	build_time: ContextTime; | ||||||
| 		iso: string; |  | ||||||
| 		rfc2822: string; |  | ||||||
| 	}; |  | ||||||
| 	markdown: { | 	markdown: { | ||||||
| 		render_inline(): MustacheRenderer; | 		render_inline(): MustacheRenderer; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface ContextTime { | ||||||
|  | 	iso: string; | ||||||
|  | 	rfc2822: string; | ||||||
|  | 	html: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface ContextEvent { | ||||||
|  | 	title?: string; | ||||||
|  | 	start?: ContextTime; | ||||||
|  | 	end?: ContextTime; | ||||||
|  | 	time_zone?: `${string}/${string}`; | ||||||
|  | 	location?: ContextLocation; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface ContextLocation { | ||||||
|  | 	description?: string; | ||||||
|  | 	lat?: string; | ||||||
|  | 	long?: string; | ||||||
|  | 	// todo: represent this better? | ||||||
|  | 	address?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface FrontMatter { | export interface FrontMatter { | ||||||
| 	skip?: boolean; | 	skip?: boolean; | ||||||
| 	layout?: string; | 	layout?: string; | ||||||
| @@ -39,23 +65,34 @@ export interface FrontMatter { | |||||||
| 	author?: string | string[]; | 	author?: string | string[]; | ||||||
| 	rss?: RSSFrontmatter; | 	rss?: RSSFrontmatter; | ||||||
| 	sitemap?: SitemapFrontmatter; | 	sitemap?: SitemapFrontmatter; | ||||||
| 	event?: EventFrontmatter; | 	event?: EventFrontmatter | EventFrontmatter[]; | ||||||
| 	[key: string]: unknown; | 	[key: string]: unknown; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface SitemapFrontmatter { | export interface SitemapFrontmatter { | ||||||
|  | 	skip?: boolean; | ||||||
| 	change_freq?: ChangeFreq; | 	change_freq?: ChangeFreq; | ||||||
| 	priority?: number; | 	priority?: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface EventFrontmatter { | export interface EventFrontmatter { | ||||||
| 	start_time?: string; | 	title?: string; | ||||||
| 	end_time?: string; | 	start?: string; | ||||||
|  | 	end?: string; | ||||||
| 	time_zone?: `${string}/${string}`; | 	time_zone?: `${string}/${string}`; | ||||||
|  | 	location?: FrontMatterLocation; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface RSSFrontmatter { | export interface FrontMatterLocation { | ||||||
| 	//  | 	description?: string; | ||||||
|  | 	lat?: string; | ||||||
|  | 	long?: string; | ||||||
|  | 	// todo: represent this better? | ||||||
|  | 	address?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface RSSFrontmatter { | ||||||
|  | 	skip?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function render_template(template: string, context: Context, layout?: string, partials: Record<string, string> = { }, tags?: [ string, string ]) { | export function render_template(template: string, context: Context, layout?: string, partials: Record<string, string> = { }, tags?: [ string, string ]) { | ||||||
| @@ -67,6 +104,7 @@ export async function load_extras() { | |||||||
| 	const extras: Record<string, string> = Object.create(null); | 	const extras: Record<string, string> = Object.create(null); | ||||||
| 	const extras_dir = resolve_path(__dirname, '../extras'); | 	const extras_dir = resolve_path(__dirname, '../extras'); | ||||||
| 	const extras_files = [ | 	const extras_files = [ | ||||||
|  | 		'svg-links.js', | ||||||
| 		'components/color-scheme-toggle-button.js', | 		'components/color-scheme-toggle-button.js', | ||||||
| 		'components/outline-button.js', | 		'components/outline-button.js', | ||||||
| 		'components/outline-inline.js', | 		'components/outline-inline.js', | ||||||
| @@ -78,6 +116,7 @@ export async function load_extras() { | |||||||
| 		'forms-inputs/spacious.css', | 		'forms-inputs/spacious.css', | ||||||
| 		'forms-inputs/compact.css', | 		'forms-inputs/compact.css', | ||||||
| 		'forms-inputs/general.css', | 		'forms-inputs/general.css', | ||||||
|  | 		'figures.css', | ||||||
| 	]; | 	]; | ||||||
|  |  | ||||||
| 	const promises = extras_files.map((file) => load_from_dir(extras_dir, file)); | 	const promises = extras_files.map((file) => load_from_dir(extras_dir, file)); | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								src/time.ts
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								src/time.ts
									
									
									
									
									
								
							| @@ -13,42 +13,20 @@ export function from_iso(time: string, zone?: string) { | |||||||
| 		: DateTime.fromISO(time); | 		: DateTime.fromISO(time); | ||||||
| } | } | ||||||
|  |  | ||||||
| // function date_formatters(lang: string, time_zone: string) { | export function as_html_time(time: DateTime, classname = '', lang?: string, config?: Intl.DateTimeFormatOptions) { | ||||||
| // 	return { | 	if (lang && config) { | ||||||
| // 		date() { | 		const formatter = new Intl.DateTimeFormat(lang, config); | ||||||
| // 			return (text, render) => { | 		const formatted = formatter.format(new Date(time.toISO())); | ||||||
| // 				return format_with_config(render(text), { | 		return `<time datetime="${time.toISO()}">${formatted}</time>`; | ||||||
| // 					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); |  | ||||||
| // 	} |  | ||||||
| // } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 	return `<time class="${classname}" datetime="${time.toISO()}">${time.toRFC2822()}</time>`; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function as_context_time(time: DateTime, classname = '', lang?: string, config?: Intl.DateTimeFormatOptions) { | ||||||
|  | 	return { | ||||||
|  | 		iso: time.toISO(), | ||||||
|  | 		rfc2822: time.toRFC2822(), | ||||||
|  | 		html: as_html_time(time, classname, lang, config), | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user