project-logbook 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/build.js +54 -76
- package/dist/commands/init.js +11 -11
- package/dist/commands/lint.js +27 -10
- package/dist/commands/list.js +18 -18
- package/dist/commands/log.js +5 -5
- package/dist/commands/new.js +19 -20
- package/dist/commands/preview.js +9 -8
- package/dist/commands/release.js +6 -6
- package/dist/commands/start.js +26 -21
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +138 -0
- package/dist/commands/steer.js +9 -9
- package/dist/commands/upgrade.js +5 -5
- package/dist/index.js +9 -8
- package/dist/lib/build-helpers.js +30 -67
- package/dist/lib/build-steps.d.ts +20 -0
- package/dist/lib/build-steps.js +57 -0
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +27 -3
- package/dist/lib/entry-id.d.ts +22 -0
- package/dist/lib/entry-id.js +26 -0
- package/dist/lib/entry-paths.d.ts +23 -0
- package/dist/lib/entry-paths.js +55 -0
- package/dist/lib/git-helpers.d.ts +32 -1
- package/dist/lib/git-helpers.js +119 -26
- package/dist/lib/hast-helpers.d.ts +10 -0
- package/dist/lib/hast-helpers.js +22 -0
- package/dist/lib/html-attributes.d.ts +17 -0
- package/dist/lib/html-attributes.js +17 -0
- package/dist/lib/html-escape.d.ts +16 -0
- package/dist/lib/html-escape.js +38 -0
- package/dist/lib/image-helpers.js +26 -33
- package/dist/lib/lint-runner.js +5 -5
- package/dist/lib/markdown-processors.d.ts +22 -0
- package/dist/lib/markdown-processors.js +68 -0
- package/dist/lib/package-version.d.ts +5 -0
- package/dist/lib/package-version.js +16 -0
- package/dist/lib/rss.d.ts +29 -0
- package/dist/lib/rss.js +77 -0
- package/dist/lib/styles.js +5 -2
- package/dist/lib/template-helpers.d.ts +4 -3
- package/dist/lib/template-helpers.js +51 -27
- package/dist/lib/template-types.d.ts +2 -2
- package/dist/lib/templates.d.ts +12 -3
- package/dist/lib/templates.js +59 -52
- package/dist/lib/theme.d.ts +37 -0
- package/dist/lib/theme.js +50 -0
- package/dist/lib/url-helpers.d.ts +13 -0
- package/dist/lib/url-helpers.js +27 -0
- package/dist/linters/diff-to-narrative.d.ts +6 -0
- package/dist/linters/diff-to-narrative.js +114 -0
- package/dist/linters/index.js +4 -0
- package/dist/linters/technical-log.d.ts +7 -0
- package/dist/linters/technical-log.js +72 -0
- package/dist/templates/CONTRIBUTING.md +12 -3
- package/dist/templates/index.md +10 -6
- package/dist/templates/log.md +5 -0
- package/dist/templates/logbook-client.js +42 -16
- package/dist/templates/steer.txt +21 -5
- package/dist/templates/styles.css +121 -0
- package/dist/utils/date.d.ts +30 -1
- package/dist/utils/date.js +68 -15
- package/dist/utils/frontmatter.d.ts +26 -0
- package/dist/utils/frontmatter.js +37 -0
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.js +23 -0
- package/dist/utils/log-timeline.d.ts +69 -0
- package/dist/utils/log-timeline.js +218 -0
- package/package.json +4 -2
- package/src/templates/CONTRIBUTING.md +12 -3
- package/src/templates/index.md +10 -6
- package/src/templates/log.md +5 -0
- package/src/templates/logbook-client.js +42 -16
- package/src/templates/steer.txt +21 -5
- package/src/templates/styles.css +121 -0
package/dist/lib/lint-runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { error, info, warning } from './theme.js';
|
|
2
2
|
import { linters } from '../linters/index.js';
|
|
3
3
|
export async function runLinters(context) {
|
|
4
4
|
const issues = [];
|
|
@@ -8,23 +8,23 @@ export async function runLinters(context) {
|
|
|
8
8
|
issues.push(...result);
|
|
9
9
|
}
|
|
10
10
|
catch (err) {
|
|
11
|
-
console.error(
|
|
11
|
+
console.error(error(`Error running linter '${linter.name}':`), err);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
if (issues.length === 0) {
|
|
15
15
|
return true;
|
|
16
16
|
}
|
|
17
17
|
if (context.entryName) {
|
|
18
|
-
console.log(
|
|
18
|
+
console.log(info(`\nEntry: ${context.entryName}`));
|
|
19
19
|
}
|
|
20
20
|
let hasErrors = false;
|
|
21
21
|
for (const issue of issues) {
|
|
22
22
|
if (issue.level === 'error') {
|
|
23
|
-
console.error(
|
|
23
|
+
console.error(error(` [${issue.category.toUpperCase()}] ${issue.message}`));
|
|
24
24
|
hasErrors = true;
|
|
25
25
|
}
|
|
26
26
|
else {
|
|
27
|
-
console.warn(
|
|
27
|
+
console.warn(warning(` [${issue.category.toUpperCase()}] ${issue.message}`));
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
return !hasErrors;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import type { Plugin } from 'unified';
|
|
3
|
+
import type { Root } from 'hast';
|
|
4
|
+
/**
|
|
5
|
+
* Rehype plugin: rewrite relative .md links to /index.html equivalents.
|
|
6
|
+
*/
|
|
7
|
+
export declare const rehypeRewriteMdLinks: Plugin<[], Root>;
|
|
8
|
+
/**
|
|
9
|
+
* General markdown processor for project files (README.md, etc.)
|
|
10
|
+
* Used for files that don't need image path rewriting
|
|
11
|
+
*/
|
|
12
|
+
export declare function createGeneralMarkdownProcessor(rehypeRewriteMdLinks: Plugin<[], Root>): ReturnType<typeof unified.prototype.use>;
|
|
13
|
+
/**
|
|
14
|
+
* Entry markdown processor with image path rewriting
|
|
15
|
+
* Used for logbook entry content (index.md, ticket.md, log.md)
|
|
16
|
+
*/
|
|
17
|
+
export declare function createEntryMarkdownProcessor(rehypeRewriteMdLinks: Plugin<[], Root>, rehypeRewriteImagePaths: Plugin<[], Root>): ReturnType<typeof unified.prototype.use>;
|
|
18
|
+
/**
|
|
19
|
+
* Image collection processor
|
|
20
|
+
* Collects all image paths during markdown-to-html conversion
|
|
21
|
+
*/
|
|
22
|
+
export declare function createImageCollectionProcessor(imageCollectionPlugin: Plugin<[], Root>, rehypeRewriteImagePaths: Plugin<[], Root>, rehypeRewriteMdLinks?: Plugin<[], Root>): ReturnType<typeof unified.prototype.use>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import remarkParse from 'remark-parse';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import remarkRehype from 'remark-rehype';
|
|
5
|
+
import rehypeSlug from 'rehype-slug';
|
|
6
|
+
import rehypeFormat from 'rehype-format';
|
|
7
|
+
import rehypeStringify from 'rehype-stringify';
|
|
8
|
+
import { visitHastElements } from './hast-helpers.js';
|
|
9
|
+
import { isExternalUrlOrAnchor } from './url-helpers.js';
|
|
10
|
+
/**
|
|
11
|
+
* Rehype plugin: rewrite relative .md links to /index.html equivalents.
|
|
12
|
+
*/
|
|
13
|
+
export const rehypeRewriteMdLinks = () => {
|
|
14
|
+
return (tree) => {
|
|
15
|
+
visitHastElements(tree, 'a', (node) => {
|
|
16
|
+
const href = node.properties?.href;
|
|
17
|
+
if (typeof href !== 'string')
|
|
18
|
+
return;
|
|
19
|
+
if (isExternalUrlOrAnchor(href))
|
|
20
|
+
return;
|
|
21
|
+
const mdMatch = href.match(/^(.*?)\.md(#.*)?$/i);
|
|
22
|
+
if (!mdMatch)
|
|
23
|
+
return;
|
|
24
|
+
const base = mdMatch[1];
|
|
25
|
+
const fragment = mdMatch[2] ?? '';
|
|
26
|
+
node.properties.href = `${base}/index.html${fragment}`;
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Create a markdown processor with custom rehype plugins
|
|
32
|
+
*
|
|
33
|
+
* @param plugins - Array of rehype plugins to inject before formatting
|
|
34
|
+
* @returns A unified processor
|
|
35
|
+
*/
|
|
36
|
+
function createMarkdownProcessor(plugins = []) {
|
|
37
|
+
const processor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype);
|
|
38
|
+
// Apply custom plugins before formatting
|
|
39
|
+
for (const plugin of plugins) {
|
|
40
|
+
processor.use(plugin);
|
|
41
|
+
}
|
|
42
|
+
return processor.use(rehypeSlug).use(rehypeFormat).use(rehypeStringify);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* General markdown processor for project files (README.md, etc.)
|
|
46
|
+
* Used for files that don't need image path rewriting
|
|
47
|
+
*/
|
|
48
|
+
export function createGeneralMarkdownProcessor(rehypeRewriteMdLinks) {
|
|
49
|
+
return createMarkdownProcessor([rehypeRewriteMdLinks]);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Entry markdown processor with image path rewriting
|
|
53
|
+
* Used for logbook entry content (index.md, ticket.md, log.md)
|
|
54
|
+
*/
|
|
55
|
+
export function createEntryMarkdownProcessor(rehypeRewriteMdLinks, rehypeRewriteImagePaths) {
|
|
56
|
+
return createMarkdownProcessor([rehypeRewriteMdLinks, rehypeRewriteImagePaths]);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Image collection processor
|
|
60
|
+
* Collects all image paths during markdown-to-html conversion
|
|
61
|
+
*/
|
|
62
|
+
export function createImageCollectionProcessor(imageCollectionPlugin, rehypeRewriteImagePaths, rehypeRewriteMdLinks) {
|
|
63
|
+
const plugins = [imageCollectionPlugin, rehypeRewriteImagePaths];
|
|
64
|
+
if (rehypeRewriteMdLinks) {
|
|
65
|
+
plugins.unshift(rehypeRewriteMdLinks);
|
|
66
|
+
}
|
|
67
|
+
return createMarkdownProcessor(plugins);
|
|
68
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/**
|
|
5
|
+
* Get the package version from package.json
|
|
6
|
+
* This centralizes version reading across the CLI to avoid duplication and path inconsistencies
|
|
7
|
+
*/
|
|
8
|
+
export function getPackageVersion() {
|
|
9
|
+
// Get the directory of this file (src/lib/)
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
// Navigate from src/lib to dist/lib (compiled) or keep as-is for ESM
|
|
13
|
+
const pkgPath = join(__dirname, '../../package.json');
|
|
14
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
15
|
+
return pkg.version || '0.0.0';
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TimelineEntry } from './template-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for RSS feed generation.
|
|
4
|
+
* Used by generateRssFeed() to create properly formatted RSS 2.0 feeds.
|
|
5
|
+
*/
|
|
6
|
+
interface RssFeedConfig {
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
language: string;
|
|
10
|
+
feed_url: string;
|
|
11
|
+
site_url: string;
|
|
12
|
+
image_url?: string;
|
|
13
|
+
generator?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generate RSS 2.0 feed XML from logbook entries
|
|
17
|
+
* @param entries - Array of timeline entries (will use latest 50)
|
|
18
|
+
* @param config - RSS feed configuration
|
|
19
|
+
* @param baseUrl - Base URL for the site (used for absolute feed URLs)
|
|
20
|
+
* @returns RSS XML string
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateRssFeed(entries: TimelineEntry[], config: RssFeedConfig, baseUrl?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Validate RSS XML structure (basic validation)
|
|
25
|
+
* @param xml - RSS XML string
|
|
26
|
+
* @returns true if valid RSS structure
|
|
27
|
+
*/
|
|
28
|
+
export declare function validateRssXml(xml: string): boolean;
|
|
29
|
+
export {};
|
package/dist/lib/rss.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import RSS from 'rss';
|
|
2
|
+
import { formatDateTimeForRss } from '../utils/date.js';
|
|
3
|
+
/**
|
|
4
|
+
* Generate RSS 2.0 feed XML from logbook entries
|
|
5
|
+
* @param entries - Array of timeline entries (will use latest 50)
|
|
6
|
+
* @param config - RSS feed configuration
|
|
7
|
+
* @param baseUrl - Base URL for the site (used for absolute feed URLs)
|
|
8
|
+
* @returns RSS XML string
|
|
9
|
+
*/
|
|
10
|
+
export function generateRssFeed(entries, config, baseUrl = '') {
|
|
11
|
+
// Use only the latest 50 entries
|
|
12
|
+
const limitedEntries = entries.slice(0, 50);
|
|
13
|
+
const feed = new RSS({
|
|
14
|
+
title: config.title,
|
|
15
|
+
description: config.description,
|
|
16
|
+
language: config.language,
|
|
17
|
+
feed_url: config.feed_url,
|
|
18
|
+
site_url: config.site_url,
|
|
19
|
+
image_url: config.image_url,
|
|
20
|
+
generator: config.generator,
|
|
21
|
+
ttl: 60, // Time to live in minutes
|
|
22
|
+
});
|
|
23
|
+
for (const entry of limitedEntries) {
|
|
24
|
+
const itemUrl = `${baseUrl}${entry.slug}/`;
|
|
25
|
+
const pubDate = formatDateTimeForRss(entry.dateStart);
|
|
26
|
+
feed.item({
|
|
27
|
+
title: entry.title || entry.slug,
|
|
28
|
+
description: stripMarkdown(entry.summary),
|
|
29
|
+
url: itemUrl,
|
|
30
|
+
guid: entry.slug,
|
|
31
|
+
date: pubDate,
|
|
32
|
+
author: entry.prompter || undefined,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return feed.xml({ indent: true });
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Strip markdown formatting from text for RSS description
|
|
39
|
+
* Removes bold, italic, code, and heading markers
|
|
40
|
+
*/
|
|
41
|
+
function stripMarkdown(text) {
|
|
42
|
+
return text
|
|
43
|
+
.replace(/`([^`]+)`/g, '$1') // Remove inline code backticks
|
|
44
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1') // Remove bold
|
|
45
|
+
.replace(/\*([^*]+)\*/g, '$1') // Remove italic
|
|
46
|
+
.replace(/#{1,6}\s+/g, '') // Remove heading markers
|
|
47
|
+
.replace(/\n+/g, ' ') // Replace newlines with spaces
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate RSS XML structure (basic validation)
|
|
52
|
+
* @param xml - RSS XML string
|
|
53
|
+
* @returns true if valid RSS structure
|
|
54
|
+
*/
|
|
55
|
+
export function validateRssXml(xml) {
|
|
56
|
+
try {
|
|
57
|
+
// Basic structure checks
|
|
58
|
+
if (!xml.includes('<?xml version'))
|
|
59
|
+
return false;
|
|
60
|
+
if (!xml.includes('<rss'))
|
|
61
|
+
return false;
|
|
62
|
+
if (!xml.includes('<channel>'))
|
|
63
|
+
return false;
|
|
64
|
+
if (!xml.includes('</channel>'))
|
|
65
|
+
return false;
|
|
66
|
+
if (!xml.includes('<item>'))
|
|
67
|
+
return false;
|
|
68
|
+
// Check for required RSS 2.0 elements
|
|
69
|
+
const hasTitle = xml.includes('<title>');
|
|
70
|
+
const hasLink = xml.includes('<link>');
|
|
71
|
+
const hasDescription = xml.includes('<description>');
|
|
72
|
+
return hasTitle && hasLink && hasDescription;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/dist/lib/styles.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { isValidHexColor } from './html-escape.js';
|
|
4
5
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
6
|
export function getStyles(primaryColor) {
|
|
6
7
|
const cssPath = join(__dirname, '../templates/styles.css');
|
|
7
8
|
const staticCss = readFileSync(cssPath, 'utf8');
|
|
9
|
+
// Validate the primary color to prevent CSS injection
|
|
10
|
+
const validatedColor = isValidHexColor(primaryColor) ? primaryColor : '#2563eb';
|
|
8
11
|
const root = `:root {
|
|
9
|
-
--primary: ${
|
|
10
|
-
--primary-soft: ${
|
|
12
|
+
--primary: ${validatedColor};
|
|
13
|
+
--primary-soft: ${validatedColor}15;
|
|
11
14
|
--bg: #fdfdfd;
|
|
12
15
|
--text: #1a1a1a;
|
|
13
16
|
--text-muted: #666666;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { TimelineEntry, TimelineCommitItem, TimelineTagItem, EntryLink, GitCommit } from './template-types.js';
|
|
2
|
+
export declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => string;
|
|
2
3
|
export declare const navLink: (entry: EntryLink, dir: "prev" | "next") => string;
|
|
3
4
|
export declare const renderTagsAndWorkspaces: (tags: string[] | undefined, workspaces: string[] | undefined) => string;
|
|
4
|
-
export declare const renderCommits: (commits: GitCommit[] | undefined) => string;
|
|
5
|
+
export declare const renderCommits: (commits: GitCommit[] | undefined, repositoryUrl?: string) => string;
|
|
5
6
|
export declare const renderTimelineEntryItem: (e: TimelineEntry) => string;
|
|
6
|
-
export declare const renderTimelineCommitItem: (c: TimelineCommitItem, jiraBaseUrl?: string, jiraPrefix?: string) => string;
|
|
7
|
-
export declare const renderTimelineTagItem: (t: TimelineTagItem) => string;
|
|
7
|
+
export declare const renderTimelineCommitItem: (c: TimelineCommitItem, jiraBaseUrl?: string, jiraPrefix?: string, repositoryUrl?: string) => string;
|
|
8
|
+
export declare const renderTimelineTagItem: (t: TimelineTagItem, repositoryUrl?: string) => string;
|
|
8
9
|
export declare const paginationLinks: (currentPage: number, totalPages: number) => string;
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { linkJiraIds } from './jira-helpers.js';
|
|
2
|
-
|
|
2
|
+
import { formatAbsoluteDate } from '../utils/date.js';
|
|
3
|
+
import { escapeHtml } from './html-escape.js';
|
|
4
|
+
import { getCommitUrl, getTagUrl } from './git-helpers.js';
|
|
5
|
+
import { ATTR_ENTRY_SLUG, ATTR_DATA_DATE, CLASS_COMMIT_SHA, CLASS_COMMIT_TIME, CLASS_COMMIT_MESSAGE, } from './html-attributes.js';
|
|
6
|
+
export const html = (strings, ...values) => {
|
|
3
7
|
return strings.reduce((acc, str, i) => acc + str + (values[i] ?? ''), '');
|
|
4
8
|
};
|
|
5
9
|
export const navLink = (entry, dir) => {
|
|
6
10
|
const label = dir === 'prev' ? '← Previous' : 'Next →';
|
|
7
|
-
const
|
|
11
|
+
const ticket = entry.ticket ? escapeHtml(entry.ticket) : '';
|
|
12
|
+
const title = escapeHtml(entry.title);
|
|
13
|
+
const t = `${ticket ? `${ticket}: ` : ''}${title}`;
|
|
8
14
|
return `<a href="../${entry.slug}/index.html" class="post-nav-link"><span class="post-nav-label">${label}</span><span class="post-nav-title">${t}</span></a>`;
|
|
9
15
|
};
|
|
10
16
|
const renderWorkspaces = (workspaces) => {
|
|
11
17
|
if (!workspaces || workspaces.length === 0)
|
|
12
18
|
return '';
|
|
13
|
-
return workspaces.map((ws) => `<span class="tag-badge tag-workspace">${ws}</span>`).join('');
|
|
19
|
+
return workspaces.map((ws) => `<span class="tag-badge tag-workspace">${escapeHtml(ws)}</span>`).join('');
|
|
14
20
|
};
|
|
15
21
|
export const renderTagsAndWorkspaces = (tags, workspaces) => {
|
|
16
22
|
const tagsHtml = tags && tags.length > 0
|
|
17
|
-
? tags.map((tag) => `<span class="tag-badge tag-${tag.replace('#', '')}">${tag}</span>`).join('')
|
|
23
|
+
? tags.map((tag) => `<span class="tag-badge tag-${tag.replace('#', '')}">${escapeHtml(tag)}</span>`).join('')
|
|
18
24
|
: '';
|
|
19
25
|
const wsHtml = renderWorkspaces(workspaces);
|
|
20
26
|
if (!tagsHtml && !wsHtml)
|
|
@@ -22,47 +28,65 @@ export const renderTagsAndWorkspaces = (tags, workspaces) => {
|
|
|
22
28
|
return html `<div class="tags-wrapper">${tagsHtml}${wsHtml}</div>`;
|
|
23
29
|
};
|
|
24
30
|
// Renders a compact list of git commits to embed in the Technical Log tab.
|
|
25
|
-
export const renderCommits = (commits) => {
|
|
31
|
+
export const renderCommits = (commits, repositoryUrl) => {
|
|
26
32
|
if (!commits || commits.length === 0)
|
|
27
33
|
return '';
|
|
28
34
|
const rows = commits
|
|
29
|
-
.map((c) =>
|
|
35
|
+
.map((c) => {
|
|
36
|
+
const formatted = formatAbsoluteDate(c.timestamp);
|
|
37
|
+
const sha = escapeHtml(c.sha);
|
|
38
|
+
const message = escapeHtml(c.message);
|
|
39
|
+
const commitUrl = getCommitUrl(repositoryUrl, c.sha);
|
|
40
|
+
const shaHtml = commitUrl
|
|
41
|
+
? `<a href="${escapeHtml(commitUrl)}" class="${CLASS_COMMIT_SHA}" target="_blank" rel="noopener noreferrer">${sha}</a>`
|
|
42
|
+
: `<span class="${CLASS_COMMIT_SHA}">${sha}</span>`;
|
|
43
|
+
return `<div class="commit-item">${shaHtml}<span class="${CLASS_COMMIT_MESSAGE}">${message}</span><span class="${CLASS_COMMIT_TIME}" title="${c.timestamp}" ${ATTR_DATA_DATE}="${c.timestamp}">${formatted}</span></div>`;
|
|
44
|
+
})
|
|
30
45
|
.join('');
|
|
31
46
|
return `<div class="commit-list"><h3 class="commit-list-heading">Git Commits</h3>${rows}</div>`;
|
|
32
47
|
};
|
|
33
|
-
export const renderTimelineEntryItem = (e) =>
|
|
48
|
+
export const renderTimelineEntryItem = (e) => {
|
|
49
|
+
const ticket = e.ticket ? escapeHtml(e.ticket) : '';
|
|
50
|
+
const title = e.title ? escapeHtml(e.title) : '';
|
|
51
|
+
const llm = e.llm ? escapeHtml(e.llm) : '';
|
|
52
|
+
const harness = e.harness ? escapeHtml(e.harness) : '';
|
|
53
|
+
const prompter = e.prompter ? escapeHtml(e.prompter) : '';
|
|
54
|
+
const summary = e.summary ? escapeHtml(e.summary) : '';
|
|
55
|
+
return html `<div class="timeline-item" ${ATTR_ENTRY_SLUG}="${e.slug}">
|
|
34
56
|
<a class="timeline-card" href="./${e.slug}/index.html"
|
|
35
57
|
><div class="item-content">
|
|
36
58
|
<div class="item-meta">
|
|
37
|
-
<span
|
|
38
|
-
${
|
|
59
|
+
<span ${ATTR_DATA_DATE}="${e.dateStart}">${e.displayDate}</span> •
|
|
60
|
+
${llm ? ` ${llm} via ` : ''}${harness}${prompter ? ` / ${prompter}` : ''}
|
|
39
61
|
</div>
|
|
40
62
|
<h3 class="item-title">
|
|
41
|
-
<span class="new-badge" style="display:none;">NEW</span>${
|
|
63
|
+
<span class="new-badge" style="display:none;">NEW</span>${ticket ? `${ticket}: ` : ''}${title}
|
|
42
64
|
</h3>
|
|
43
|
-
<div class="item-summary">${
|
|
65
|
+
<div class="item-summary">${summary}</div>
|
|
44
66
|
${renderTagsAndWorkspaces(e.tags, e.workspaces)}
|
|
45
67
|
</div>
|
|
46
68
|
<span class="item-arrow">›</span></a
|
|
47
69
|
>
|
|
48
70
|
</div>`;
|
|
49
|
-
export const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix) => {
|
|
50
|
-
const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : c.message;
|
|
51
|
-
return `<div class="timeline-item timeline-item--commit"><div class="commit-chip"><span class="commit-sha">${c.sha}</span><span class="commit-message">${message}</span><span class="commit-time" title="${c.timestamp}" data-date="${c.timestamp}">${c.relativeTime}</span></div></div>`;
|
|
52
71
|
};
|
|
53
|
-
export const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
export const renderTimelineCommitItem = (c, jiraBaseUrl, jiraPrefix, repositoryUrl) => {
|
|
73
|
+
const message = jiraBaseUrl && jiraPrefix ? linkJiraIds(c.message, jiraBaseUrl, jiraPrefix) : escapeHtml(c.message);
|
|
74
|
+
const formatted = formatAbsoluteDate(c.timestamp);
|
|
75
|
+
const sha = escapeHtml(c.sha);
|
|
76
|
+
const commitUrl = getCommitUrl(repositoryUrl, c.sha);
|
|
77
|
+
const shaHtml = commitUrl
|
|
78
|
+
? `<a href="${escapeHtml(commitUrl)}" class="${CLASS_COMMIT_SHA}" target="_blank" rel="noopener noreferrer">${sha}</a>`
|
|
79
|
+
: `<span class="${CLASS_COMMIT_SHA}">${sha}</span>`;
|
|
80
|
+
return `<div class="timeline-item timeline-item--commit"><div class="commit-chip">${shaHtml}<span class="${CLASS_COMMIT_MESSAGE}">${message}</span><span class="${CLASS_COMMIT_TIME}" title="${c.timestamp}" ${ATTR_DATA_DATE}="${c.timestamp}">${formatted}</span></div></div>`;
|
|
81
|
+
};
|
|
82
|
+
export const renderTimelineTagItem = (t, repositoryUrl) => {
|
|
83
|
+
const formatted = formatAbsoluteDate(t.timestamp);
|
|
84
|
+
const name = escapeHtml(t.name);
|
|
85
|
+
const tagUrl = getTagUrl(repositoryUrl, t.name);
|
|
86
|
+
const tagHtml = tagUrl
|
|
87
|
+
? `<a href="${escapeHtml(tagUrl)}" class="tag-chip-name" target="_blank" rel="noopener noreferrer">${name}</a>`
|
|
88
|
+
: `<span class="tag-chip-name">${name}</span>`;
|
|
89
|
+
return `<div class="timeline-item timeline-item--tag"><div class="tag-chip"><span class="tag-chip-icon">🏷</span>${tagHtml}<span class="tag-chip-date" title="${t.timestamp}" ${ATTR_DATA_DATE}="${t.timestamp}">${formatted}</span></div></div>`;
|
|
66
90
|
};
|
|
67
91
|
export const paginationLinks = (currentPage, totalPages) => {
|
|
68
92
|
let links = '';
|
|
@@ -18,6 +18,7 @@ export interface TimelineTemplateProps {
|
|
|
18
18
|
aboutHtml: string;
|
|
19
19
|
jiraBaseUrl?: string;
|
|
20
20
|
jiraPrefix?: string;
|
|
21
|
+
repositoryUrl?: string;
|
|
21
22
|
currentPage?: number;
|
|
22
23
|
totalPages?: number;
|
|
23
24
|
}
|
|
@@ -60,7 +61,6 @@ export interface TimelineCommitItem {
|
|
|
60
61
|
sha: string;
|
|
61
62
|
message: string;
|
|
62
63
|
timestamp: string;
|
|
63
|
-
relativeTime: string;
|
|
64
64
|
displayDate: string;
|
|
65
65
|
monthGroup: string;
|
|
66
66
|
sortTime: string;
|
|
@@ -83,7 +83,6 @@ export interface GitCommit {
|
|
|
83
83
|
sha: string;
|
|
84
84
|
message: string;
|
|
85
85
|
timestamp: string;
|
|
86
|
-
relativeTime: string;
|
|
87
86
|
}
|
|
88
87
|
/**
|
|
89
88
|
* Configuration for the post template
|
|
@@ -103,6 +102,7 @@ export interface PostTemplateProps {
|
|
|
103
102
|
commits?: GitCommit[];
|
|
104
103
|
version: string;
|
|
105
104
|
jiraUrl?: string;
|
|
105
|
+
repositoryUrl?: string;
|
|
106
106
|
prevEntry: EntryLink | null;
|
|
107
107
|
nextEntry: EntryLink | null;
|
|
108
108
|
slug?: string;
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TabConfig, TimelineTemplateProps, PostTemplateProps } from './template-types.js';
|
|
2
2
|
export declare const tabsComponent: (tabs: TabConfig[], defaultActive?: number) => string;
|
|
3
|
-
export declare const layout: ({ title, header, content, projectName, basePath, description, bodySlug, buildMeta, }: {
|
|
3
|
+
export declare const layout: ({ title, header, content, projectName, basePath, description, bodySlug, buildMeta, includeRssLink, }: {
|
|
4
4
|
title: string;
|
|
5
5
|
header: string;
|
|
6
6
|
content: string;
|
|
@@ -9,6 +9,15 @@ export declare const layout: ({ title, header, content, projectName, basePath, d
|
|
|
9
9
|
description?: string;
|
|
10
10
|
bodySlug?: string;
|
|
11
11
|
buildMeta?: string;
|
|
12
|
+
includeRssLink?: boolean;
|
|
12
13
|
}) => string;
|
|
13
|
-
export declare const timelineTemplate: (props: TimelineTemplateProps
|
|
14
|
-
|
|
14
|
+
export declare const timelineTemplate: (props: TimelineTemplateProps & {
|
|
15
|
+
includeRssLink?: boolean;
|
|
16
|
+
}) => {
|
|
17
|
+
header: string;
|
|
18
|
+
content: string;
|
|
19
|
+
};
|
|
20
|
+
export declare const postTemplate: (props: PostTemplateProps) => {
|
|
21
|
+
header: string;
|
|
22
|
+
content: string;
|
|
23
|
+
};
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,35 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
1
|
+
import { ATTR_PAGE_SLUG, ATTR_DATA_DATE } from './html-attributes.js';
|
|
2
|
+
import { escapeHtml, validateUrlScheme } from './html-escape.js';
|
|
3
|
+
import { html, navLink, renderTagsAndWorkspaces, renderCommits, renderTimelineEntryItem, renderTimelineCommitItem, renderTimelineTagItem, paginationLinks, } from './template-helpers.js';
|
|
5
4
|
export const tabsComponent = (tabs, defaultActive = 0) => {
|
|
6
5
|
const tabsHtml = tabs
|
|
7
|
-
.map((tab, index) => `<div class="tab ${index === defaultActive ? 'active' : ''}" role="tab" aria-selected="${index === defaultActive}" aria-controls="${tab.id}" tabindex="${index === defaultActive ? '0' : '-1'}" onclick="showTab(event, '${tab.id}')">${tab.title}</div>`)
|
|
6
|
+
.map((tab, index) => `<div class="tab ${index === defaultActive ? 'active' : ''}" role="tab" aria-selected="${index === defaultActive}" aria-controls="${escapeHtml(tab.id)}" tabindex="${index === defaultActive ? '0' : '-1'}" onclick="showTab(event, '${escapeHtml(tab.id)}')">${escapeHtml(tab.title)}</div>`)
|
|
8
7
|
.join('');
|
|
9
8
|
const sectionsHtml = tabs
|
|
10
|
-
.map((tab, index) => `<div id="${tab.id}" class="content-section" role="tabpanel" style="${index === defaultActive ? '' : 'display:none;'}">${tab.content}</div>`)
|
|
9
|
+
.map((tab, index) => `<div id="${escapeHtml(tab.id)}" class="content-section" role="tabpanel" style="${index === defaultActive ? '' : 'display:none;'}">${tab.content}</div>`)
|
|
11
10
|
.join('');
|
|
12
11
|
return html `<div class="tabs-wrapper">
|
|
13
12
|
<div class="tabs" id="tabs" role="tablist" aria-label="Navigation tabs">${tabsHtml}</div>
|
|
14
13
|
${sectionsHtml}
|
|
15
14
|
</div>`;
|
|
16
15
|
};
|
|
17
|
-
export const layout = ({ title, header, content, projectName, basePath = './', description, bodySlug, buildMeta, }) => html `<!DOCTYPE html>
|
|
16
|
+
export const layout = ({ title, header, content, projectName, basePath = './', description, bodySlug, buildMeta, includeRssLink = false, }) => html `<!DOCTYPE html>
|
|
18
17
|
<html lang="en">
|
|
19
18
|
<head>
|
|
20
19
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
21
|
-
<title>${title} | ${projectName}</title>
|
|
22
|
-
${description ? `<meta name="description" content="${description.replace(/"/g, '
|
|
20
|
+
<title>${escapeHtml(title)} | ${escapeHtml(projectName)}</title>
|
|
21
|
+
${description ? `<meta name="description" content="${escapeHtml(description).replace(/"/g, '"').substring(0, 160)}">` : ''}
|
|
22
|
+
${includeRssLink ? `<link rel="alternate" type="application/rss+xml" title="Subscribe to Project RSS Feed" href="${basePath}rss.xml">` : ''}
|
|
23
23
|
<link rel="icon" type="image/svg+xml" href="${basePath}favicon.svg">
|
|
24
24
|
<link rel="stylesheet" href="${basePath}style.css">
|
|
25
25
|
</head>
|
|
26
|
-
<body${bodySlug ? `
|
|
26
|
+
<body${bodySlug ? ` ${ATTR_PAGE_SLUG}="${bodySlug}"` : ''}>
|
|
27
27
|
${header}<main>${content}</main>
|
|
28
|
-
<footer
|
|
28
|
+
<footer>
|
|
29
|
+
${buildMeta ? `<p class="build-time">${buildMeta}${includeRssLink ? ` • <a href="${basePath}rss.xml">RSS Feed</a>` : ''}</p>` : ''}
|
|
30
|
+
</footer>
|
|
29
31
|
<script src="${basePath}logbook.js"></script>
|
|
30
32
|
</body></html>`;
|
|
31
33
|
export const timelineTemplate = (props) => {
|
|
32
|
-
const { projectName, groups, readmeHtml, aboutHtml, jiraBaseUrl, jiraPrefix, currentPage, totalPages } = props;
|
|
34
|
+
const { projectName, groups, readmeHtml, aboutHtml, jiraBaseUrl, jiraPrefix, repositoryUrl, currentPage, totalPages, } = props;
|
|
33
35
|
const timelineTab = html `<div class="timeline">
|
|
34
36
|
${groups
|
|
35
37
|
.map((g) => html `<section class="month-group">
|
|
@@ -39,59 +41,64 @@ export const timelineTemplate = (props) => {
|
|
|
39
41
|
if (item.kind === 'entry')
|
|
40
42
|
return renderTimelineEntryItem(item);
|
|
41
43
|
if (item.kind === 'tag')
|
|
42
|
-
return renderTimelineTagItem(item);
|
|
43
|
-
return renderTimelineCommitItem(item, jiraBaseUrl, jiraPrefix);
|
|
44
|
+
return renderTimelineTagItem(item, repositoryUrl);
|
|
45
|
+
return renderTimelineCommitItem(item, jiraBaseUrl, jiraPrefix, repositoryUrl);
|
|
44
46
|
})
|
|
45
47
|
.join('')}
|
|
46
48
|
</section>`)
|
|
47
49
|
.join('')}
|
|
48
50
|
${paginationLinks(currentPage, totalPages)}
|
|
49
51
|
</div>`;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const header = html `<header>
|
|
53
|
+
<h1><a href="./index.html">${escapeHtml(projectName)}</a></h1>
|
|
54
|
+
<p class="tagline">A brief summary of the recent changes to the project.</p>
|
|
55
|
+
</header>`;
|
|
56
|
+
const content = tabsComponent([
|
|
55
57
|
{ id: 'timeline', title: 'Timeline', content: timelineTab },
|
|
56
58
|
{ id: 'readme', title: 'Project Readme', content: readmeHtml || '<p>No README available.</p>' },
|
|
57
59
|
{ id: 'about', title: 'About this Logbook', content: aboutHtml || '<p>No information available.</p>' },
|
|
58
|
-
], 0)
|
|
60
|
+
], 0);
|
|
61
|
+
return { header, content };
|
|
59
62
|
};
|
|
60
63
|
export const postTemplate = (props) => {
|
|
61
|
-
const { title, displayDate, dateStart, harness, llm, prompter, content, ticketHtml, logHtml, commits, version, jiraUrl, prevEntry, nextEntry, tags, workspaces } = props; // prettier-ignore
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
const { title, displayDate, dateStart, harness, llm, prompter, content, ticketHtml, logHtml, commits, version, jiraUrl, repositoryUrl, prevEntry, nextEntry, tags, workspaces } = props; // prettier-ignore
|
|
65
|
+
// Validate jiraUrl scheme to prevent javascript: URLs
|
|
66
|
+
const validatedJiraUrl = jiraUrl ? validateUrlScheme(jiraUrl) : undefined;
|
|
67
|
+
const jiraBtn = validatedJiraUrl
|
|
68
|
+
? `<a href="${escapeHtml(validatedJiraUrl)}" class="jira-link-btn" target="_blank" rel="noopener noreferrer">View in Jira ↗</a>`
|
|
64
69
|
: '';
|
|
65
|
-
const tabBtn = (id, label, active = false) => `<div class="tab${active ? ' active' : ''}" role="tab" aria-selected="${active}" aria-controls="${id}" tabindex="${active ? '0' : '-1'}" onclick="showTab(event, '${id}')">${label}</div>`;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
70
|
+
const tabBtn = (id, label, active = false) => `<div class="tab${active ? ' active' : ''}" role="tab" aria-selected="${active}" aria-controls="${escapeHtml(id)}" tabindex="${active ? '0' : '-1'}" onclick="showTab(event, '${escapeHtml(id)}')">${escapeHtml(label)}</div>`;
|
|
71
|
+
const header = html `<header>
|
|
72
|
+
<h1>${escapeHtml(title)}</h1>
|
|
73
|
+
<div class="tagline">
|
|
74
|
+
<span ${ATTR_DATA_DATE}="${dateStart}">${displayDate}</span> •
|
|
75
|
+
${llm ? `${escapeHtml(llm)} via ` : ''}${escapeHtml(harness)}${prompter ? ` / ${escapeHtml(prompter)}` : ''} •
|
|
76
|
+
v${version}
|
|
77
|
+
</div>
|
|
78
|
+
</header>`;
|
|
79
|
+
const mainContent = html `<article class="tabs-wrapper">
|
|
80
|
+
<div class="action-bar">
|
|
81
|
+
<a href="../index.html" class="back-link">← Back to Timeline</a>
|
|
82
|
+
<div class="action-tabs-wrapper">
|
|
83
|
+
<div class="tabs" role="tablist" aria-label="Entry sections">
|
|
84
|
+
${tabBtn('story', 'Story', true)}${tabBtn('spec', 'Original Spec')}${tabBtn('log', 'Technical Log')}
|
|
80
85
|
</div>
|
|
81
|
-
${jiraBtn ? `${jiraBtn}` : ''}
|
|
82
86
|
</div>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
${jiraBtn ? `${jiraBtn}` : ''}
|
|
88
|
+
</div>
|
|
89
|
+
<div class="content-sections">
|
|
90
|
+
<div id="story" class="content-section" role="tabpanel">
|
|
91
|
+
${renderTagsAndWorkspaces(tags, workspaces)}${content}
|
|
92
|
+
</div>
|
|
93
|
+
<div id="spec" class="content-section" role="tabpanel" style="display:none;">${ticketHtml}</div>
|
|
94
|
+
<div id="log" class="content-section" role="tabpanel" style="display:none;">
|
|
95
|
+
${logHtml}${renderCommits(commits, repositoryUrl)}
|
|
91
96
|
</div>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
</
|
|
97
|
+
</div>
|
|
98
|
+
<nav class="post-nav">
|
|
99
|
+
<div class="post-nav-prev">${prevEntry ? navLink(prevEntry, 'prev') : '<span></span>'}</div>
|
|
100
|
+
<div class="post-nav-next">${nextEntry ? navLink(nextEntry, 'next') : '<span></span>'}</div>
|
|
101
|
+
</nav>
|
|
102
|
+
</article>`;
|
|
103
|
+
return { header, content: mainContent };
|
|
97
104
|
};
|