From 62cadf4819c58f01fb0f6fc904b545947978fa6c Mon Sep 17 00:00:00 2001 From: BittuBarnwal7479 Date: Thu, 2 Jul 2026 16:04:13 +0530 Subject: [PATCH 1/3] feat(docs): add 'Copy page as Markdown' action --- docs-site/app/docs/[[...slug]]/page.tsx | 22 +- docs-site/components/docs-page-actions.tsx | 507 +++++++++++++++++- docs-site/tests/docs-search-behavior.test.mjs | 17 + 3 files changed, 530 insertions(+), 16 deletions(-) diff --git a/docs-site/app/docs/[[...slug]]/page.tsx b/docs-site/app/docs/[[...slug]]/page.tsx index e0677c9e9..9e09493b2 100644 --- a/docs-site/app/docs/[[...slug]]/page.tsx +++ b/docs-site/app/docs/[[...slug]]/page.tsx @@ -9,10 +9,10 @@ import { notFound, redirect } from "next/navigation"; import defaultMdxComponents from "fumadocs-ui/mdx"; import { CodeBlock } from "@/components/code-block"; import { DocsPageActions } from "@/components/docs-page-actions"; -import { readDocsPageMarkdown } from "@/lib/docs-markdown"; const docsIndexPath = "/docs/getting-started/introduction"; const docsIndexSlug = ["getting-started", "introduction"] as const; +const docsContentId = "ktx-docs-page-content"; function isDocsIndex(slug: string[] | undefined) { return slug === undefined || slug.length === 0 || slug.join("/") === ""; @@ -34,7 +34,6 @@ export default async function Page(props: { if (!page) notFound(); const MDX = page.data.body; - const mdxSource = await readDocsPageMarkdown(page.slugs); const hero = isHeroPage(params.slug); @@ -51,14 +50,29 @@ export default async function Page(props: { <>
{page.data.title} - +
{page.data.description} )} - + {hero && ( +
+ +
+ )} + diff --git a/docs-site/components/docs-page-actions.tsx b/docs-site/components/docs-page-actions.tsx index ce85aca05..3c19ea54a 100644 --- a/docs-site/components/docs-page-actions.tsx +++ b/docs-site/components/docs-page-actions.tsx @@ -1,38 +1,521 @@ "use client"; -import { useState } from "react"; +import { type ReactNode, useEffect, useId, useRef, useState } from "react"; type Props = { - mdxSource: string; + title: string; + description?: string; + contentId: string; + markdownHref: string; }; -function stripFrontmatter(source: string) { - return source.trim().replace(/^---\n[\s\S]*?\n---\n?/, "").trim(); -} - -export function DocsPageActions({ mdxSource }: Props) { +export function DocsPageActions({ + title, + description, + contentId, + markdownHref, +}: Props) { const [copied, setCopied] = useState(false); + const [open, setOpen] = useState(false); + const menuId = useId(); + const rootRef = useRef(null); + + useEffect(() => { + if (!open) return; + + const closeOnOutsidePointer = (event: PointerEvent) => { + if (!rootRef.current?.contains(event.target as Node)) { + setOpen(false); + } + }; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setOpen(false); + } + }; + + document.addEventListener("pointerdown", closeOnOutsidePointer); + document.addEventListener("keydown", closeOnEscape); + return () => { + document.removeEventListener("pointerdown", closeOnOutsidePointer); + document.removeEventListener("keydown", closeOnEscape); + }; + }, [open]); const onCopy = async () => { + const content = document.getElementById(contentId); + if (!content) return; + try { - await navigator.clipboard.writeText(stripFrontmatter(mdxSource)); + await navigator.clipboard.writeText( + buildPageMarkdown({ title, description, content }), + ); setCopied(true); + setOpen(false); setTimeout(() => setCopied(false), 1500); } catch { // Clipboard denied - fail silently } }; + const absoluteMarkdownUrl = `https://docs.kaelio.com/ktx${markdownHref}`; + const assistantPrompt = () => + `Use this documentation page to answer questions about ${title}: ${absoluteMarkdownUrl}`; + const chatGptHref = () => + `https://chatgpt.com/?q=${encodeURIComponent(assistantPrompt())}`; + const claudeHref = () => + `https://claude.ai/new?q=${encodeURIComponent(assistantPrompt())}`; + return ( -
+
+ {open && ( + + )}
); } + +function MenuButton({ + icon, + title, + description, + onClick, +}: { + icon: ReactNode; + title: string; + description: string; + onClick: () => void; +}) { + return ( + + ); +} + +function MenuLink({ + icon, + title, + description, + href, +}: { + icon: ReactNode; + title: string; + description: string; + href: string; +}) { + return ( + + + {icon} + + + + {title} + + + + {description} + + + + ); +} + +function buildPageMarkdown({ + title, + description, + content, +}: { + title: string; + description?: string; + content: HTMLElement; +}) { + const parts = [`# ${title}`]; + if (description) { + parts.push(`> ${description}`); + } + + const body = childrenToMarkdown(content).trim(); + if (body) { + parts.push(body); + } + + return `${parts.join("\n\n")}\n`; +} + +function childrenToMarkdown(parent: Element) { + return Array.from(parent.childNodes) + .map((node) => nodeToMarkdown(node)) + .filter(Boolean) + .join("\n\n") + .replace(/\n{3,}/g, "\n\n"); +} + +function nodeToMarkdown(node: Node): string { + if (node.nodeType === Node.TEXT_NODE) { + return normalizeText(node.textContent ?? ""); + } + + if (!(node instanceof HTMLElement)) { + return ""; + } + + const tagName = node.tagName.toLowerCase(); + if (["button", "script", "style", "svg"].includes(tagName)) { + return ""; + } + + if (tagName === "br") { + return "\n"; + } + + if (/^h[1-6]$/.test(tagName)) { + const level = Number(tagName.slice(1)); + return `${"#".repeat(level)} ${inlineChildrenToMarkdown(node)}`.trim(); + } + + if (tagName === "pre") { + const code = node.querySelector("code"); + const language = getCodeLanguage(code); + return `\`\`\`${language}\n${(code ?? node).textContent?.trimEnd() ?? ""}\n\`\`\``; + } + + if (tagName === "table") { + return tableToMarkdown(node); + } + + if (tagName === "ul" || tagName === "ol") { + return listToMarkdown(node, tagName === "ol"); + } + + if (tagName === "blockquote") { + return childrenToMarkdown(node) + .split("\n") + .map((line) => (line ? `> ${line}` : ">")) + .join("\n"); + } + + if (tagName === "hr") { + return "---"; + } + + if (tagName === "p" || tagName === "figcaption") { + return inlineChildrenToMarkdown(node); + } + + return childrenToMarkdown(node) || inlineChildrenToMarkdown(node); +} + +function inlineChildrenToMarkdown(parent: Element) { + return Array.from(parent.childNodes) + .map((node) => inlineNodeToMarkdown(node)) + .join("") + .replace(/[ \t\n]+/g, " ") + .trim(); +} + +function inlineNodeToMarkdown(node: Node): string { + if (node.nodeType === Node.TEXT_NODE) { + return normalizeText(node.textContent ?? ""); + } + + if (!(node instanceof HTMLElement)) { + return ""; + } + + const tagName = node.tagName.toLowerCase(); + if (["button", "script", "style", "svg"].includes(tagName)) { + return ""; + } + + const text = inlineChildrenToMarkdown(node); + if (!text) { + return ""; + } + + if (tagName === "a") { + const href = node.getAttribute("href"); + return href ? `[${text}](${href})` : text; + } + + if (tagName === "code") { + return `\`${text}\``; + } + + if (tagName === "strong" || tagName === "b") { + return `**${text}**`; + } + + if (tagName === "em" || tagName === "i") { + return `_${text}_`; + } + + if (tagName === "br") { + return "\n"; + } + + return text; +} + +function listToMarkdown(list: HTMLElement, ordered: boolean) { + return Array.from(list.children) + .filter((child) => child.tagName.toLowerCase() === "li") + .map((item, index) => { + const marker = ordered ? `${index + 1}.` : "-"; + const content = + childrenToMarkdown(item).trim() || inlineChildrenToMarkdown(item); + const [firstLine = "", ...restLines] = content.split("\n"); + const continuationIndent = " ".repeat(marker.length + 1); + + return [ + `${marker} ${firstLine}`, + ...restLines.map((line) => + line ? `${continuationIndent}${line}` : "", + ), + ].join("\n"); + }) + .join("\n"); +} + +function tableToMarkdown(table: HTMLElement) { + const rows = Array.from(table.querySelectorAll("tr")).map((row) => + Array.from(row.children).map((cell) => inlineChildrenToMarkdown(cell)), + ); + const [header, ...body] = rows; + if (!header) return ""; + + return [ + markdownTableRow(header), + markdownTableRow(header.map(() => "---")), + ...body.map(markdownTableRow), + ].join("\n"); +} + +function markdownTableRow(cells: string[]) { + return `| ${cells.map((cell) => cell.replace(/\|/g, "\\|")).join(" | ")} |`; +} + +function getCodeLanguage(code: Element | null) { + const className = code?.className; + if (typeof className !== "string") return ""; + + return ( + className + .split(/\s+/) + .find((name) => name.startsWith("language-")) + ?.slice("language-".length) ?? "" + ); +} + +function normalizeText(text: string) { + return text.replace(/\s+/g, " "); +} + +function CopyIcon() { + return ( + + ); +} + +function CheckIcon() { + return ( + + ); +} + +function ChevronIcon({ open }: { open: boolean }) { + return ( + + ); +} + +function MarkdownIcon() { + return ( + + ); +} + +function ChatGptIcon() { + return ( + + ); +} + +function ClaudeIcon() { + return ( + + ); +} + +function ExternalIcon() { + return ( + + ); +} diff --git a/docs-site/tests/docs-search-behavior.test.mjs b/docs-site/tests/docs-search-behavior.test.mjs index ece514776..fb3b7eb3c 100644 --- a/docs-site/tests/docs-search-behavior.test.mjs +++ b/docs-site/tests/docs-search-behavior.test.mjs @@ -34,6 +34,23 @@ test("markdown negotiation uses the Next proxy convention", async () => { assert.doesNotMatch(proxy, /export function middleware/); }); +test("docs pages expose a rendered-content Markdown copy action", async () => { + const page = await readDocsFile("app/docs/[[...slug]]/page.tsx"); + const action = await readDocsFile("components/docs-page-actions.tsx"); + + assert.match(page, /const docsContentId = "ktx-docs-page-content"/); + assert.match(page, / { const css = await readDocsFile("app/global.css"); From 66a7659830583677dcf29977e1d0df1ac061c383 Mon Sep 17 00:00:00 2001 From: BittuBarnwal7479 Date: Fri, 3 Jul 2026 18:00:32 +0530 Subject: [PATCH 2/3] refactor: simplify DocsPageActions component and update Markdown copy functionality --- docs-site/app/docs/[[...slug]]/page.tsx | 7 +- docs-site/components/docs-page-actions.tsx | 359 ++++-------------- docs-site/tests/docs-search-behavior.test.mjs | 13 +- 3 files changed, 88 insertions(+), 291 deletions(-) diff --git a/docs-site/app/docs/[[...slug]]/page.tsx b/docs-site/app/docs/[[...slug]]/page.tsx index 9e09493b2..ab3af901a 100644 --- a/docs-site/app/docs/[[...slug]]/page.tsx +++ b/docs-site/app/docs/[[...slug]]/page.tsx @@ -12,7 +12,6 @@ import { DocsPageActions } from "@/components/docs-page-actions"; const docsIndexPath = "/docs/getting-started/introduction"; const docsIndexSlug = ["getting-started", "introduction"] as const; -const docsContentId = "ktx-docs-page-content"; function isDocsIndex(slug: string[] | undefined) { return slug === undefined || slug.length === 0 || slug.join("/") === ""; @@ -52,8 +51,6 @@ export default async function Page(props: { {page.data.title}
@@ -66,13 +63,11 @@ export default async function Page(props: {
)} - + diff --git a/docs-site/components/docs-page-actions.tsx b/docs-site/components/docs-page-actions.tsx index 3c19ea54a..2263153d6 100644 --- a/docs-site/components/docs-page-actions.tsx +++ b/docs-site/components/docs-page-actions.tsx @@ -4,20 +4,13 @@ import { type ReactNode, useEffect, useId, useRef, useState } from "react"; type Props = { title: string; - description?: string; - contentId: string; markdownHref: string; }; -export function DocsPageActions({ - title, - description, - contentId, - markdownHref, -}: Props) { +export function DocsPageActions({ title, markdownHref }: Props) { const [copied, setCopied] = useState(false); const [open, setOpen] = useState(false); - const menuId = useId(); + const panelId = useId(); const rootRef = useRef(null); useEffect(() => { @@ -43,80 +36,76 @@ export function DocsPageActions({ }, [open]); const onCopy = async () => { - const content = document.getElementById(contentId); - if (!content) return; - try { - await navigator.clipboard.writeText( - buildPageMarkdown({ title, description, content }), - ); + const response = await fetch(markdownHref, { + headers: { Accept: "text/markdown" }, + }); + if (!response.ok) { + throw new Error(`Markdown request failed: ${response.status}`); + } + + await navigator.clipboard.writeText(await response.text()); setCopied(true); setOpen(false); setTimeout(() => setCopied(false), 1500); } catch { - // Clipboard denied - fail silently + // Clipboard denied or Markdown unavailable - fail silently. } }; - const absoluteMarkdownUrl = `https://docs.kaelio.com/ktx${markdownHref}`; - const assistantPrompt = () => - `Use this documentation page to answer questions about ${title}: ${absoluteMarkdownUrl}`; - const chatGptHref = () => - `https://chatgpt.com/?q=${encodeURIComponent(assistantPrompt())}`; - const claudeHref = () => - `https://claude.ai/new?q=${encodeURIComponent(assistantPrompt())}`; + const publicMarkdownUrl = `https://docs.kaelio.com/ktx${markdownHref}`; + const assistantPrompt = + `Use this documentation page to answer questions about ${title}: ${publicMarkdownUrl}`; + const chatGptHref = + `https://chatgpt.com/?q=${encodeURIComponent(assistantPrompt)}`; + const claudeHref = + `https://claude.ai/new?q=${encodeURIComponent(assistantPrompt)}`; return ( -
+
{open && ( )} @@ -124,7 +113,7 @@ export function DocsPageActions({ ); } -function MenuButton({ +function ActionButton({ icon, title, description, @@ -138,253 +127,77 @@ function MenuButton({ return ( ); } -function MenuLink({ +function ActionLink({ icon, title, description, href, + onClick, }: { icon: ReactNode; title: string; description: string; href: string; + onClick: () => void; }) { return ( - - {icon} - - - - {title} - - - - {description} - - + {icon} + ); } -function buildPageMarkdown({ +function ActionIcon({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} + +function ActionText({ title, description, - content, + external, }: { title: string; - description?: string; - content: HTMLElement; + description: string; + external?: boolean; }) { - const parts = [`# ${title}`]; - if (description) { - parts.push(`> ${description}`); - } - - const body = childrenToMarkdown(content).trim(); - if (body) { - parts.push(body); - } - - return `${parts.join("\n\n")}\n`; -} - -function childrenToMarkdown(parent: Element) { - return Array.from(parent.childNodes) - .map((node) => nodeToMarkdown(node)) - .filter(Boolean) - .join("\n\n") - .replace(/\n{3,}/g, "\n\n"); -} - -function nodeToMarkdown(node: Node): string { - if (node.nodeType === Node.TEXT_NODE) { - return normalizeText(node.textContent ?? ""); - } - - if (!(node instanceof HTMLElement)) { - return ""; - } - - const tagName = node.tagName.toLowerCase(); - if (["button", "script", "style", "svg"].includes(tagName)) { - return ""; - } - - if (tagName === "br") { - return "\n"; - } - - if (/^h[1-6]$/.test(tagName)) { - const level = Number(tagName.slice(1)); - return `${"#".repeat(level)} ${inlineChildrenToMarkdown(node)}`.trim(); - } - - if (tagName === "pre") { - const code = node.querySelector("code"); - const language = getCodeLanguage(code); - return `\`\`\`${language}\n${(code ?? node).textContent?.trimEnd() ?? ""}\n\`\`\``; - } - - if (tagName === "table") { - return tableToMarkdown(node); - } - - if (tagName === "ul" || tagName === "ol") { - return listToMarkdown(node, tagName === "ol"); - } - - if (tagName === "blockquote") { - return childrenToMarkdown(node) - .split("\n") - .map((line) => (line ? `> ${line}` : ">")) - .join("\n"); - } - - if (tagName === "hr") { - return "---"; - } - - if (tagName === "p" || tagName === "figcaption") { - return inlineChildrenToMarkdown(node); - } - - return childrenToMarkdown(node) || inlineChildrenToMarkdown(node); -} - -function inlineChildrenToMarkdown(parent: Element) { - return Array.from(parent.childNodes) - .map((node) => inlineNodeToMarkdown(node)) - .join("") - .replace(/[ \t\n]+/g, " ") - .trim(); -} - -function inlineNodeToMarkdown(node: Node): string { - if (node.nodeType === Node.TEXT_NODE) { - return normalizeText(node.textContent ?? ""); - } - - if (!(node instanceof HTMLElement)) { - return ""; - } - - const tagName = node.tagName.toLowerCase(); - if (["button", "script", "style", "svg"].includes(tagName)) { - return ""; - } - - const text = inlineChildrenToMarkdown(node); - if (!text) { - return ""; - } - - if (tagName === "a") { - const href = node.getAttribute("href"); - return href ? `[${text}](${href})` : text; - } - - if (tagName === "code") { - return `\`${text}\``; - } - - if (tagName === "strong" || tagName === "b") { - return `**${text}**`; - } - - if (tagName === "em" || tagName === "i") { - return `_${text}_`; - } - - if (tagName === "br") { - return "\n"; - } - - return text; -} - -function listToMarkdown(list: HTMLElement, ordered: boolean) { - return Array.from(list.children) - .filter((child) => child.tagName.toLowerCase() === "li") - .map((item, index) => { - const marker = ordered ? `${index + 1}.` : "-"; - const content = - childrenToMarkdown(item).trim() || inlineChildrenToMarkdown(item); - const [firstLine = "", ...restLines] = content.split("\n"); - const continuationIndent = " ".repeat(marker.length + 1); - - return [ - `${marker} ${firstLine}`, - ...restLines.map((line) => - line ? `${continuationIndent}${line}` : "", - ), - ].join("\n"); - }) - .join("\n"); -} - -function tableToMarkdown(table: HTMLElement) { - const rows = Array.from(table.querySelectorAll("tr")).map((row) => - Array.from(row.children).map((cell) => inlineChildrenToMarkdown(cell)), - ); - const [header, ...body] = rows; - if (!header) return ""; - - return [ - markdownTableRow(header), - markdownTableRow(header.map(() => "---")), - ...body.map(markdownTableRow), - ].join("\n"); -} - -function markdownTableRow(cells: string[]) { - return `| ${cells.map((cell) => cell.replace(/\|/g, "\\|")).join(" | ")} |`; -} - -function getCodeLanguage(code: Element | null) { - const className = code?.className; - if (typeof className !== "string") return ""; - return ( - className - .split(/\s+/) - .find((name) => name.startsWith("language-")) - ?.slice("language-".length) ?? "" + + + {title} + {external && } + + + {description} + + ); } -function normalizeText(text: string) { - return text.replace(/\s+/g, " "); -} - function CopyIcon() { return ( ); } @@ -485,18 +290,10 @@ function ClaudeIcon() { width="18" height="18" viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="1.7" - strokeLinecap="round" - strokeLinejoin="round" + fill="currentColor" aria-hidden="true" > - - - - - + ); } diff --git a/docs-site/tests/docs-search-behavior.test.mjs b/docs-site/tests/docs-search-behavior.test.mjs index fb3b7eb3c..907794c76 100644 --- a/docs-site/tests/docs-search-behavior.test.mjs +++ b/docs-site/tests/docs-search-behavior.test.mjs @@ -34,21 +34,26 @@ test("markdown negotiation uses the Next proxy convention", async () => { assert.doesNotMatch(proxy, /export function middleware/); }); -test("docs pages expose a rendered-content Markdown copy action", async () => { +test("docs pages expose a canonical Markdown copy disclosure", async () => { const page = await readDocsFile("app/docs/[[...slug]]/page.tsx"); const action = await readDocsFile("components/docs-page-actions.tsx"); - assert.match(page, /const docsContentId = "ktx-docs-page-content"/); assert.match(page, / { From de85e12f6184b9ec6b734e4ce2e4c8bcfee6d746 Mon Sep 17 00:00:00 2001 From: BittuBarnwal7479 Date: Fri, 3 Jul 2026 18:16:25 +0530 Subject: [PATCH 3/3] test: enhance query validation for semantic-layer sources in sl.test.ts --- packages/cli/test/sl.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/cli/test/sl.test.ts b/packages/cli/test/sl.test.ts index ca37747ea..1c8015053 100644 --- a/packages/cli/test/sl.test.ts +++ b/packages/cli/test/sl.test.ts @@ -717,7 +717,18 @@ joins: [] expect(query).toHaveBeenCalledWith( expect.objectContaining({ - query: { measures: ['orders.order_count'], dimensions: [] }, + dialect: 'postgres', + query: expect.objectContaining({ + measures: ['orders.order_count'], + dimensions: [], + }), + sources: [ + expect.objectContaining({ + name: 'orders', + table: 'public.orders', + measures: [expect.objectContaining({ name: 'order_count', expr: 'count(*)' })], + }), + ], }), ); expect(JSON.parse(String(stdout.write.mock.calls[0][0]))).toMatchObject({