mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Add llms docs exports
This commit is contained in:
parent
e2aec62b0f
commit
c2bdcfda36
2 changed files with 205 additions and 0 deletions
|
|
@ -180,6 +180,199 @@ function flattenNav(items) {
|
|||
return pages;
|
||||
}
|
||||
|
||||
// ── LLM-friendly documentation exports ─────────────────────────────
|
||||
|
||||
function normalizeSiteUrl(siteUrl) {
|
||||
const fallback = 'https://tasknotes.dev/';
|
||||
const url = String(siteUrl || fallback).trim() || fallback;
|
||||
return url.endsWith('/') ? url : `${url}/`;
|
||||
}
|
||||
|
||||
function absoluteSiteUrl(url, siteUrl) {
|
||||
return new URL(url, normalizeSiteUrl(siteUrl)).toString();
|
||||
}
|
||||
|
||||
function markdownContent(raw) {
|
||||
return matter(raw).content.trim();
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(text) {
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function markdownInlineToText(markdown) {
|
||||
return decodeHtmlEntities(markdown
|
||||
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1')
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
||||
.replace(/`([^`]+)`/g, '$1')
|
||||
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
||||
.replace(/\*([^*]+)\*/g, '$1')
|
||||
.replace(/<kbd>([\s\S]*?)<\/kbd>/gi, '$1')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim());
|
||||
}
|
||||
|
||||
function truncateSummary(summary) {
|
||||
const maxLength = 220;
|
||||
if (summary.length <= maxLength) return summary;
|
||||
return `${summary.slice(0, maxLength - 3).trimEnd()}...`;
|
||||
}
|
||||
|
||||
function extractLlmsSummary(raw) {
|
||||
const content = markdownContent(raw)
|
||||
.replace(/```[\s\S]*?```/g, '')
|
||||
.replace(/~~~[\s\S]*?~~~/g, '');
|
||||
const paragraph = [];
|
||||
|
||||
for (const line of content.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
if (paragraph.length) break;
|
||||
continue;
|
||||
}
|
||||
if (/^!!!\s+/.test(trimmed) || /^:::\s+/.test(trimmed)) continue;
|
||||
if (/^#\s+/.test(trimmed)) continue;
|
||||
if (/^#{2,}\s+/.test(trimmed)) {
|
||||
if (paragraph.length) break;
|
||||
continue;
|
||||
}
|
||||
if (/^[-*]\s+/.test(trimmed) || /^\d+\.\s+/.test(trimmed)) {
|
||||
if (paragraph.length) break;
|
||||
continue;
|
||||
}
|
||||
if (/^<\/?(div|a|span)\b/i.test(trimmed)) {
|
||||
if (paragraph.length) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
paragraph.push(trimmed);
|
||||
}
|
||||
|
||||
const summary = markdownInlineToText(paragraph.join(' '));
|
||||
return summary ? truncateSummary(summary) : 'TaskNotes documentation page.';
|
||||
}
|
||||
|
||||
function escapeMarkdownLinkText(text) {
|
||||
return String(text).replace(/[[\]]/g, '\\$&');
|
||||
}
|
||||
|
||||
function resolveDocsUrl(href, mdPath, siteUrl) {
|
||||
if (!href) return href;
|
||||
if (href.startsWith('//')) return href;
|
||||
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(href)) return href;
|
||||
|
||||
const { pathPart, suffix } = splitHref(href);
|
||||
if (!pathPart) {
|
||||
return absoluteSiteUrl(`${mdPathToUrl(mdPath)}${suffix}`, siteUrl);
|
||||
}
|
||||
|
||||
if (/\.md$/i.test(pathPart)) {
|
||||
return absoluteSiteUrl(`${mdLinkPathToUrl(pathPart, mdPath)}${suffix}`, siteUrl);
|
||||
}
|
||||
|
||||
if (pathPart.startsWith('/')) {
|
||||
return absoluteSiteUrl(`${pathPart}${suffix}`, siteUrl);
|
||||
}
|
||||
|
||||
const pageDir = '/' + path.posix.dirname(mdPath).replace(/^\.(?:\/|$)/, '');
|
||||
const resolved = path.posix.resolve(pageDir || '/', pathPart);
|
||||
return absoluteSiteUrl(`${resolved}${suffix}`, siteUrl);
|
||||
}
|
||||
|
||||
function rewriteMarkdownLinksForLlms(markdown, mdPath, siteUrl) {
|
||||
return markdown
|
||||
.replace(/(!?\[[^\]]*\]\()([^)]+)(\))/g, (_, pre, href, post) => {
|
||||
return pre + resolveDocsUrl(href.trim(), mdPath, siteUrl) + post;
|
||||
})
|
||||
.replace(/(<a\b[^>]*\shref=")([^"]+)(")/g, (_, pre, href, post) => {
|
||||
return pre + resolveDocsUrl(href, mdPath, siteUrl) + post;
|
||||
})
|
||||
.replace(/(<(?:img|source|video)\b[^>]*\ssrc=")([^"]+)(")/g, (_, pre, src, post) => {
|
||||
return pre + resolveDocsUrl(src, mdPath, siteUrl) + post;
|
||||
});
|
||||
}
|
||||
|
||||
function stripLeadingH1(markdown) {
|
||||
return markdown.replace(/^#\s+.+(?:\r?\n)+/, '').trim();
|
||||
}
|
||||
|
||||
function normalizeLlmsWhitespace(text) {
|
||||
return text
|
||||
.split('\n')
|
||||
.map(line => line.trimEnd())
|
||||
.join('\n')
|
||||
.replace(/\n{4,}/g, '\n\n\n')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function buildLlmsTxt(pages, siteUrl, siteDescription) {
|
||||
const lines = [
|
||||
'# TaskNotes',
|
||||
`> ${siteDescription || 'Task and note management for Obsidian.'}`,
|
||||
'',
|
||||
'TaskNotes is an Obsidian plugin for note-based task management. This file lists the primary documentation pages in a form suitable for LLM context windows.',
|
||||
'',
|
||||
'## Documentation',
|
||||
'',
|
||||
];
|
||||
|
||||
for (const page of pages) {
|
||||
const title = escapeMarkdownLinkText(decodeHtmlEntities(page.title));
|
||||
const url = absoluteSiteUrl(page.url, siteUrl);
|
||||
const summary = extractLlmsSummary(page.raw);
|
||||
lines.push(`- [${title}](${url}): ${summary}`);
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'## Full Context',
|
||||
'',
|
||||
`- [Complete documentation bundle](${absoluteSiteUrl('/llms-full.txt', siteUrl)}): Primary documentation pages concatenated as Markdown for larger context windows.`,
|
||||
''
|
||||
);
|
||||
|
||||
return normalizeLlmsWhitespace(lines.join('\n')) + '\n';
|
||||
}
|
||||
|
||||
function buildLlmsFullTxt(pages, siteUrl, siteDescription) {
|
||||
const lines = [
|
||||
'# TaskNotes Documentation',
|
||||
'',
|
||||
`> ${siteDescription || 'Task and note management for Obsidian.'}`,
|
||||
'',
|
||||
`Source: ${absoluteSiteUrl('/', siteUrl)}`,
|
||||
'',
|
||||
'This file is generated from the primary documentation navigation and excludes release-note archives.',
|
||||
'',
|
||||
];
|
||||
|
||||
for (const page of pages) {
|
||||
const content = stripLeadingH1(
|
||||
rewriteMarkdownLinksForLlms(markdownContent(page.raw), page.mdPath, siteUrl)
|
||||
);
|
||||
|
||||
lines.push(
|
||||
'---',
|
||||
'',
|
||||
`# ${decodeHtmlEntities(page.title)}`,
|
||||
'',
|
||||
`Source: ${absoluteSiteUrl(page.url, siteUrl)}`,
|
||||
'',
|
||||
content,
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
return normalizeLlmsWhitespace(lines.join('\n')) + '\n';
|
||||
}
|
||||
|
||||
// ── Utilities ───────────────────────────────────────────────────────
|
||||
|
||||
function escHtml(s) {
|
||||
|
|
@ -235,6 +428,8 @@ async function main() {
|
|||
const mkdocsRaw = await fs.readFile(path.resolve(__dir, '../mkdocs.yml'), 'utf-8');
|
||||
const mkdocs = yaml.load(mkdocsRaw.replace(/!!python\/name:\S+/g, 'null'));
|
||||
const nav = mkdocs.nav;
|
||||
const siteUrl = normalizeSiteUrl(mkdocs.site_url);
|
||||
const siteDescription = mkdocs.site_description;
|
||||
|
||||
const template = await fs.readFile(path.join(SRC, 'template.html'), 'utf-8');
|
||||
|
||||
|
|
@ -262,8 +457,10 @@ async function main() {
|
|||
|
||||
// Build every markdown doc so cross-links outside the nav still resolve
|
||||
const navPages = flattenNav(nav);
|
||||
const navPageSet = new Set(navPages);
|
||||
const allDocPages = await listMarkdownFiles(DOCS);
|
||||
const pages = [...new Set([...navPages, ...allDocPages])];
|
||||
const llmsPages = [];
|
||||
let built = 0, skipped = 0;
|
||||
|
||||
for (const mdPath of pages) {
|
||||
|
|
@ -278,6 +475,10 @@ async function main() {
|
|||
const toc = buildToc(body);
|
||||
const navHtml = buildNavHtml(nav, url);
|
||||
|
||||
if (navPageSet.has(mdPath)) {
|
||||
llmsPages.push({ mdPath, raw, title, url });
|
||||
}
|
||||
|
||||
const page = template
|
||||
.replaceAll('{{title}}', escHtml(title))
|
||||
.replaceAll('{{site_title}}', 'TaskNotes')
|
||||
|
|
@ -293,6 +494,9 @@ async function main() {
|
|||
built++;
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(DIST, 'llms.txt'), buildLlmsTxt(llmsPages, siteUrl, siteDescription));
|
||||
await fs.writeFile(path.join(DIST, 'llms-full.txt'), buildLlmsFullTxt(llmsPages, siteUrl, siteDescription));
|
||||
|
||||
console.log(`Built ${built} pages, skipped ${skipped} (${Date.now() - start}ms)`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ Example:
|
|||
- ([#1327](https://github.com/callumalpass/tasknotes/issues/1327)) Made TaskNotes Bases search match any word in a multi-word search instead of requiring the whole phrase. Thanks to @craziedde for suggesting this.
|
||||
- ([#1318](https://github.com/callumalpass/tasknotes/issues/1318)) Made aliased inline task links use the alias text inside the task widget, so `[[Task title|short label]]` can display as `short label`. Thanks to @3zra47 for suggesting this and @kazerniel for the follow-up.
|
||||
- ([#1325](https://github.com/callumalpass/tasknotes/issues/1325)) Added `./` and `../` support to Include folders filters for project and file autosuggest, resolved from the active note's folder. Thanks to @EpolPers for suggesting this.
|
||||
- ([#1313](https://github.com/callumalpass/tasknotes/issues/1313)) Added generated `llms.txt` and `llms-full.txt` files to the documentation site, so the primary docs can be shared with LLM tools more easily. Thanks to @JacksonMcDonaldDev for suggesting this.
|
||||
|
||||
## Fixed
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue