From 765f92dca3b5aa0e4de2b15d3cbc73bcdeaea967 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Thu, 24 Jul 2025 21:40:52 -0700 Subject: [PATCH 1/5] add goggle fonts plugin --- .../assets/examples/google-fonts-demo.idoc.md | 54 ++++ packages/markdown/src/plugins/google-fonts.ts | 244 ++++++++++++++++++ packages/markdown/src/plugins/index.ts | 2 + 3 files changed, 300 insertions(+) create mode 100644 docs/assets/examples/google-fonts-demo.idoc.md create mode 100644 packages/markdown/src/plugins/google-fonts.ts diff --git a/docs/assets/examples/google-fonts-demo.idoc.md b/docs/assets/examples/google-fonts-demo.idoc.md new file mode 100644 index 00000000..9d833349 --- /dev/null +++ b/docs/assets/examples/google-fonts-demo.idoc.md @@ -0,0 +1,54 @@ +# Google Fonts Demo + +This document demonstrates the Google Fonts plugin with cascading font behavior and semantic mapping. + +```json google-fonts +{ + "googleFontsUrl": "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Source+Code+Pro:wght@400;600&family=Inter:wght@300;400;500&family=Roboto+Slab:wght@400;700&display=swap", + "mapping": { + "body": "Inter", + "hero": "Playfair Display", + "headings": "Roboto Slab", + "code": "Source Code Pro" + } +} +``` + +# Hero Headline Level 1 +## Hero Headline Level 2 +### Hero Headline Level 3 + +#### Regular Heading Level 4 +##### Regular Heading Level 5 +###### Regular Heading Level 6 + +This is regular paragraph text that demonstrates the body font. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +- This is a bulleted list item +- Another list item +- Third item to show consistency + +1. Numbered list item one +2. Second numbered item +3. Third numbered item + +**Bold text** and *italic text* also inherit the body font. + +Here's some `inline code` and a code block: + +```javascript +// Code blocks use the code font mapping +function example() { + console.log("Hello, World!"); +} +``` + +## Tables + +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Cell 1 | Cell 2 | Cell 3 | +| Data A | Data B | Data C | +| Numbers | 123.45 | 678.90 | + +This content lets you see how different fonts apply to different semantic elements without any distracting references to the font names. \ No newline at end of file diff --git a/packages/markdown/src/plugins/google-fonts.ts b/packages/markdown/src/plugins/google-fonts.ts new file mode 100644 index 00000000..425a6392 --- /dev/null +++ b/packages/markdown/src/plugins/google-fonts.ts @@ -0,0 +1,244 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { definePlugin, IInstance, Plugin } from '../factory.js'; +import { sanitizedHTML } from '../sanitize.js'; + +export interface GoogleFontsSpec { + googleFontsUrl: string; + mapping?: { + body?: string; // Base font - applies to everything if others not specified + hero?: string; // H1 only - the main hero title + headings?: string; // H2, H3, H4, H5, H6 - all other headings + code?: string; // Code blocks and inline code + table?: string; // Table cells - overrides body + }; +} + +interface GoogleFontsInstance { + id: string; + spec: GoogleFontsSpec; + styleElement: HTMLStyleElement; + linkElement?: HTMLLinkElement; +} + +interface FontFamily { + name: string; + fallback: string; +} + +// Helper function to extract font families from Google Fonts URL +function extractFontFamilies(googleFontsUrl: string): FontFamily[] { + const families: FontFamily[] = []; + + try { + const url = new URL(googleFontsUrl); + const params = url.searchParams; + + // Extract family parameters - Google Fonts uses 'family' parameter multiple times + const familyParams = params.getAll('family'); + for (const value of familyParams) { + // Parse: family=Cascadia+Code:ital,wght@0,200..700;1,200..700 + const familyName = value.split(':')[0].replace(/\+/g, ' '); + + // Determine fallback based on font name patterns + let fallback = 'sans-serif'; + const lowerName = familyName.toLowerCase(); + if (lowerName.includes('mono') || lowerName.includes('code') || lowerName.includes('consola')) { + fallback = 'monospace'; + } else if (lowerName.includes('serif') && !lowerName.includes('sans')) { + fallback = 'serif'; + } + + families.push({ name: familyName, fallback }); + } + } catch (error) { + console.error('Failed to parse Google Fonts URL:', error); + } + + return families; +} + +// Helper function to generate scoped CSS for semantic elements +function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scopeId: string): string { + const cssRules: string[] = []; + + console.log('CSS Generation Debug:', { + families, + mapping: spec.mapping + }); + + if (spec.mapping) { + // Base/Body font - applies to everything first (cascading base) + if (spec.mapping.body) { + const family = families.find(f => f.name === spec.mapping.body); + console.log('Body font lookup:', { requested: spec.mapping.body, found: family }); + if (family) { + cssRules.push(`body { + font-family: '${family.name}'; +}`); + } + } + + // Headings font (h1, h2, h3, h4, h5, h6) - overrides body + if (spec.mapping.headings) { + const family = families.find(f => f.name === spec.mapping.headings); + console.log('Headings font lookup:', { requested: spec.mapping.headings, found: family }); + if (family) { + cssRules.push(`h1, h2, h3, h4, h5, h6 { + font-family: '${family.name}'; +}`); + } + } + + // Code font - applies to code blocks and inline code + if (spec.mapping.code) { + const family = families.find(f => f.name === spec.mapping.code); + console.log('Code font lookup:', { requested: spec.mapping.code, found: family }); + if (family) { + cssRules.push(`code, pre, kbd, samp, tt, .hljs, table { + font-family: '${family.name}'; +}`); + } + } + + // Table font - overrides code for table cells (if you want different from code) + if (spec.mapping.table) { + const family = families.find(f => f.name === spec.mapping.table); + console.log('Table font lookup:', { requested: spec.mapping.table, found: family }); + if (family) { + cssRules.push(`table { + font-family: '${family.name}'; +}`); + } + } + + // Hero font (h1 only) - overrides headings (most specific, goes last) + if (spec.mapping.hero) { + const family = families.find(f => f.name === spec.mapping.hero); + console.log('Hero font lookup:', { requested: spec.mapping.hero, found: family }); + if (family) { + cssRules.push(`h1 { + font-family: '${family.name}'; +}`); + } + } + } + + const finalCSS = cssRules.join('\n\n'); + console.log('Generated CSS:', finalCSS); + return finalCSS; +} + +// Helper function to generate unique document ID +function generateDocumentId(): string { + return Math.random().toString(36).substr(2, 8); +} + +export const googleFontsPlugin: Plugin = { + name: 'google-fonts', + initializePlugin: (md) => definePlugin(md, 'google-fonts'), + fence: (token, idx) => { + const googleFontsId = `google-fonts-${idx}`; + return sanitizedHTML('div', { id: googleFontsId, class: 'google-fonts' }, token.content.trim()); + }, + hydrateComponent: async (renderer, errorHandler) => { + const googleFontsInstances: GoogleFontsInstance[] = []; + const containers = renderer.element.querySelectorAll('.google-fonts'); + + // Enforce one Google Fonts block per page + if (containers.length > 1) { + for (let i = 1; i < containers.length; i++) { + containers[i].innerHTML = ''; + } + } + + // Only process the first container + const container = containers[0]; + if (!container || !container.textContent) { + return []; + } + + try { + const spec: GoogleFontsSpec = JSON.parse(container.textContent); + + if (!spec.googleFontsUrl) { + container.innerHTML = ''; + return []; + } + + // Extract font families from the URL + const families = extractFontFamilies(spec.googleFontsUrl); + + console.log('Google Fonts Debug:', { + url: spec.googleFontsUrl, + extractedFamilies: families, + mapping: spec.mapping + }); + + if (families.length === 0) { + container.innerHTML = ''; + return []; + } + + // Generate unique instance ID for scoping + const instanceId = `gf-${Date.now()}-0`; + + // Create font import (using @import for now, could switch to tags) + const importCSS = `@import url('${spec.googleFontsUrl}');`; + + // Generate semantic CSS with instance-specific scoping + const semanticCSS = generateSemanticCSS(spec, families, instanceId); + + // Combine import and semantic CSS + const fullCSS = importCSS + '\n\n' + semanticCSS; + + // Create style element + const styleElement = document.createElement('style'); + styleElement.type = 'text/css'; + styleElement.id = `idocs-google-fonts-${container.id}`; + styleElement.textContent = fullCSS; + + // Apply to shadow DOM if available, otherwise document + const target = renderer.shadowRoot || document.head; + target.appendChild(styleElement); + + const googleFontsInstance: GoogleFontsInstance = { + id: container.id, + spec, + styleElement + }; + + googleFontsInstances.push(googleFontsInstance); + + // Replace container content with summary + const fontsList = families.map(f => f.name).join(', '); + container.innerHTML = ``; + + } catch (e) { + container.innerHTML = ``; + errorHandler(e, 'Google Fonts', 0, 'parse', container); + } + + const instances: IInstance[] = googleFontsInstances.map((googleFontsInstance) => { + return { + id: googleFontsInstance.id, + initialSignals: [], // Google Fonts doesn't need signals + destroy: () => { + // Remove the style element when the instance is destroyed + if (googleFontsInstance.styleElement && googleFontsInstance.styleElement.parentNode) { + googleFontsInstance.styleElement.parentNode.removeChild(googleFontsInstance.styleElement); + } + // Remove link element if it exists + if (googleFontsInstance.linkElement && googleFontsInstance.linkElement.parentNode) { + googleFontsInstance.linkElement.parentNode.removeChild(googleFontsInstance.linkElement); + } + }, + }; + }); + + return instances; + }, +}; diff --git a/packages/markdown/src/plugins/index.ts b/packages/markdown/src/plugins/index.ts index c947d2c4..5ec406dc 100644 --- a/packages/markdown/src/plugins/index.ts +++ b/packages/markdown/src/plugins/index.ts @@ -8,6 +8,7 @@ import { registerMarkdownPlugin } from '../factory.js'; import { checkboxPlugin } from './checkbox.js'; import { cssPlugin } from './css.js'; import { dropdownPlugin } from './dropdown.js'; +import { googleFontsPlugin } from './google-fonts.js'; import { imagePlugin } from './image.js'; import { placeholdersPlugin } from './placeholders.js'; import { presetsPlugin } from './presets.js'; @@ -19,6 +20,7 @@ export function registerNativePlugins() { registerMarkdownPlugin(checkboxPlugin); registerMarkdownPlugin(cssPlugin); registerMarkdownPlugin(dropdownPlugin); + registerMarkdownPlugin(googleFontsPlugin); registerMarkdownPlugin(imagePlugin); registerMarkdownPlugin(placeholdersPlugin); registerMarkdownPlugin(presetsPlugin); From 6a292efd009ad7c898eab611bd33464467bbb87c Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Thu, 24 Jul 2025 21:46:19 -0700 Subject: [PATCH 2/5] refine Google Fonts mapping comments and CSS selectors for clarity --- packages/markdown/src/plugins/google-fonts.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/markdown/src/plugins/google-fonts.ts b/packages/markdown/src/plugins/google-fonts.ts index 425a6392..d6fc45c5 100644 --- a/packages/markdown/src/plugins/google-fonts.ts +++ b/packages/markdown/src/plugins/google-fonts.ts @@ -11,9 +11,9 @@ export interface GoogleFontsSpec { mapping?: { body?: string; // Base font - applies to everything if others not specified hero?: string; // H1 only - the main hero title - headings?: string; // H2, H3, H4, H5, H6 - all other headings + headings?: string; // all headings code?: string; // Code blocks and inline code - table?: string; // Table cells - overrides body + table?: string; // Tables and tabulator }; } @@ -98,7 +98,7 @@ function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scop const family = families.find(f => f.name === spec.mapping.code); console.log('Code font lookup:', { requested: spec.mapping.code, found: family }); if (family) { - cssRules.push(`code, pre, kbd, samp, tt, .hljs, table { + cssRules.push(`code, pre, kbd, samp, tt, .hljs { font-family: '${family.name}'; }`); } @@ -109,7 +109,7 @@ function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scop const family = families.find(f => f.name === spec.mapping.table); console.log('Table font lookup:', { requested: spec.mapping.table, found: family }); if (family) { - cssRules.push(`table { + cssRules.push(`table, .tabulator { font-family: '${family.name}'; }`); } From c47970270f5c59e1ee271b0d7bf9f9d4ebcddbb5 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Thu, 24 Jul 2025 21:55:55 -0700 Subject: [PATCH 3/5] enhance Google Fonts plugin with URL validation and sanitization for font family names --- packages/markdown/src/plugins/google-fonts.ts | 76 +++++++++++++------ 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/packages/markdown/src/plugins/google-fonts.ts b/packages/markdown/src/plugins/google-fonts.ts index d6fc45c5..ffcb0802 100644 --- a/packages/markdown/src/plugins/google-fonts.ts +++ b/packages/markdown/src/plugins/google-fonts.ts @@ -29,11 +29,36 @@ interface FontFamily { fallback: string; } +// Helper function to validate Google Fonts URL +function isValidGoogleFontsUrl(url: string): boolean { + try { + const parsed = new URL(url); + return parsed.protocol === 'https:' && + parsed.hostname === 'fonts.googleapis.com' && + parsed.pathname === '/css2'; + } catch { + return false; + } +} + +// Helper function to sanitize font family names for CSS safety +function sanitizeFontFamily(fontName: string): string { + // Remove any characters that could be used for CSS injection + // Allow only letters, numbers, spaces, hyphens, and basic punctuation + return fontName.replace(/[^a-zA-Z0-9\s\-_]/g, '').trim(); +} + // Helper function to extract font families from Google Fonts URL function extractFontFamilies(googleFontsUrl: string): FontFamily[] { const families: FontFamily[] = []; try { + // Security: Validate this is a Google Fonts URL + if (!isValidGoogleFontsUrl(googleFontsUrl)) { + console.error('Invalid Google Fonts URL - only https://fonts.googleapis.com/css2 URLs are allowed'); + return families; + } + const url = new URL(googleFontsUrl); const params = url.searchParams; @@ -41,7 +66,15 @@ function extractFontFamilies(googleFontsUrl: string): FontFamily[] { const familyParams = params.getAll('family'); for (const value of familyParams) { // Parse: family=Cascadia+Code:ital,wght@0,200..700;1,200..700 - const familyName = value.split(':')[0].replace(/\+/g, ' '); + const rawFamilyName = value.split(':')[0].replace(/\+/g, ' '); + + // Security: Sanitize font family name to prevent CSS injection + const familyName = sanitizeFontFamily(rawFamilyName); + + if (!familyName) { + console.warn('Skipped invalid font family name:', rawFamilyName); + continue; + } // Determine fallback based on font name patterns let fallback = 'sans-serif'; @@ -65,16 +98,11 @@ function extractFontFamilies(googleFontsUrl: string): FontFamily[] { function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scopeId: string): string { const cssRules: string[] = []; - console.log('CSS Generation Debug:', { - families, - mapping: spec.mapping - }); - if (spec.mapping) { // Base/Body font - applies to everything first (cascading base) if (spec.mapping.body) { - const family = families.find(f => f.name === spec.mapping.body); - console.log('Body font lookup:', { requested: spec.mapping.body, found: family }); + const sanitizedName = sanitizeFontFamily(spec.mapping.body); + const family = families.find(f => f.name === sanitizedName); if (family) { cssRules.push(`body { font-family: '${family.name}'; @@ -84,8 +112,8 @@ function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scop // Headings font (h1, h2, h3, h4, h5, h6) - overrides body if (spec.mapping.headings) { - const family = families.find(f => f.name === spec.mapping.headings); - console.log('Headings font lookup:', { requested: spec.mapping.headings, found: family }); + const sanitizedName = sanitizeFontFamily(spec.mapping.headings); + const family = families.find(f => f.name === sanitizedName); if (family) { cssRules.push(`h1, h2, h3, h4, h5, h6 { font-family: '${family.name}'; @@ -95,8 +123,8 @@ function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scop // Code font - applies to code blocks and inline code if (spec.mapping.code) { - const family = families.find(f => f.name === spec.mapping.code); - console.log('Code font lookup:', { requested: spec.mapping.code, found: family }); + const sanitizedName = sanitizeFontFamily(spec.mapping.code); + const family = families.find(f => f.name === sanitizedName); if (family) { cssRules.push(`code, pre, kbd, samp, tt, .hljs { font-family: '${family.name}'; @@ -106,8 +134,8 @@ function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scop // Table font - overrides code for table cells (if you want different from code) if (spec.mapping.table) { - const family = families.find(f => f.name === spec.mapping.table); - console.log('Table font lookup:', { requested: spec.mapping.table, found: family }); + const sanitizedName = sanitizeFontFamily(spec.mapping.table); + const family = families.find(f => f.name === sanitizedName); if (family) { cssRules.push(`table, .tabulator { font-family: '${family.name}'; @@ -117,8 +145,8 @@ function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scop // Hero font (h1 only) - overrides headings (most specific, goes last) if (spec.mapping.hero) { - const family = families.find(f => f.name === spec.mapping.hero); - console.log('Hero font lookup:', { requested: spec.mapping.hero, found: family }); + const sanitizedName = sanitizeFontFamily(spec.mapping.hero); + const family = families.find(f => f.name === sanitizedName); if (family) { cssRules.push(`h1 { font-family: '${family.name}'; @@ -127,9 +155,7 @@ function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scop } } - const finalCSS = cssRules.join('\n\n'); - console.log('Generated CSS:', finalCSS); - return finalCSS; + return cssRules.join('\n\n'); } // Helper function to generate unique document ID @@ -169,15 +195,15 @@ export const googleFontsPlugin: Plugin = { return []; } + // Security: Validate Google Fonts URL before processing + if (!isValidGoogleFontsUrl(spec.googleFontsUrl)) { + container.innerHTML = ''; + return []; + } + // Extract font families from the URL const families = extractFontFamilies(spec.googleFontsUrl); - console.log('Google Fonts Debug:', { - url: spec.googleFontsUrl, - extractedFamilies: families, - mapping: spec.mapping - }); - if (families.length === 0) { container.innerHTML = ''; return []; From 1c0bbdbe212cc485e553359252d710de2a179f07 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Thu, 24 Jul 2025 22:16:15 -0700 Subject: [PATCH 4/5] add sizing support and sanitization for Google Fonts plugin --- packages/markdown/src/plugins/google-fonts.ts | 98 ++++++++++--------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/packages/markdown/src/plugins/google-fonts.ts b/packages/markdown/src/plugins/google-fonts.ts index ffcb0802..3115eeb7 100644 --- a/packages/markdown/src/plugins/google-fonts.ts +++ b/packages/markdown/src/plugins/google-fonts.ts @@ -15,6 +15,13 @@ export interface GoogleFontsSpec { code?: string; // Code blocks and inline code table?: string; // Tables and tabulator }; + sizing?: { + body?: number; // 1.0 = normal, 1.1 = 10% larger, 0.9 = 10% smaller + hero?: number; // Relative size multiplier for H1 + headings?: number; // Relative size multiplier for all headings + code?: number; // Relative size multiplier for code elements + table?: number; // Relative size multiplier for tables + }; } interface GoogleFontsInstance { @@ -48,6 +55,24 @@ function sanitizeFontFamily(fontName: string): string { return fontName.replace(/[^a-zA-Z0-9\s\-_]/g, '').trim(); } +// Helper function to sanitize sizing values for CSS safety +function sanitizeSizing(sizeValue: number): number | null { + // Ensure it's a valid number + if (typeof sizeValue !== 'number' || isNaN(sizeValue) || !isFinite(sizeValue)) { + console.warn('Invalid sizing value - must be a finite number:', sizeValue); + return null; + } + + // Reasonable bounds: 0.1 (10%) to 9.999... (just under 1000%) + if (sizeValue < 0.1 || sizeValue >= 10) { + console.warn('Sizing value out of safe range (0.1 to 9.999):', sizeValue); + return null; + } + + // Round to 3 decimal places to prevent excessive precision + return Math.round(sizeValue * 1000) / 1000; +} + // Helper function to extract font families from Google Fonts URL function extractFontFamilies(googleFontsUrl: string): FontFamily[] { const families: FontFamily[] = []; @@ -98,62 +123,43 @@ function extractFontFamilies(googleFontsUrl: string): FontFamily[] { function generateSemanticCSS(spec: GoogleFontsSpec, families: FontFamily[], scopeId: string): string { const cssRules: string[] = []; - if (spec.mapping) { - // Base/Body font - applies to everything first (cascading base) - if (spec.mapping.body) { - const sanitizedName = sanitizeFontFamily(spec.mapping.body); - const family = families.find(f => f.name === sanitizedName); - if (family) { - cssRules.push(`body { - font-family: '${family.name}'; -}`); - } - } + // Helper to generate CSS rule for a specific element type + const generateRule = (elementType: keyof NonNullable, selectors: string) => { + const fontFamily = spec.mapping?.[elementType]; + const sizeValue = spec.sizing?.[elementType]; - // Headings font (h1, h2, h3, h4, h5, h6) - overrides body - if (spec.mapping.headings) { - const sanitizedName = sanitizeFontFamily(spec.mapping.headings); - const family = families.find(f => f.name === sanitizedName); - if (family) { - cssRules.push(`h1, h2, h3, h4, h5, h6 { - font-family: '${family.name}'; -}`); - } - } + // Skip if neither font nor sizing is specified + if (!fontFamily && !sizeValue) return; - // Code font - applies to code blocks and inline code - if (spec.mapping.code) { - const sanitizedName = sanitizeFontFamily(spec.mapping.code); - const family = families.find(f => f.name === sanitizedName); - if (family) { - cssRules.push(`code, pre, kbd, samp, tt, .hljs { - font-family: '${family.name}'; -}`); - } - } + let css = `${selectors} {`; - // Table font - overrides code for table cells (if you want different from code) - if (spec.mapping.table) { - const sanitizedName = sanitizeFontFamily(spec.mapping.table); + // Add font-family if mapping exists + if (fontFamily) { + const sanitizedName = sanitizeFontFamily(fontFamily); const family = families.find(f => f.name === sanitizedName); if (family) { - cssRules.push(`table, .tabulator { - font-family: '${family.name}'; -}`); + css += `\n font-family: '${family.name}';`; } } - // Hero font (h1 only) - overrides headings (most specific, goes last) - if (spec.mapping.hero) { - const sanitizedName = sanitizeFontFamily(spec.mapping.hero); - const family = families.find(f => f.name === sanitizedName); - if (family) { - cssRules.push(`h1 { - font-family: '${family.name}'; -}`); + // Add font-size if sizing exists + if (sizeValue) { + const sanitizedSize = sanitizeSizing(sizeValue); + if (sanitizedSize !== null) { + css += `\n font-size: ${sanitizedSize}em;`; } } - } + + css += '\n}'; + cssRules.push(css); + }; + + // Generate rules in cascading order (most general to most specific) + generateRule('body', 'body'); + generateRule('headings', 'h1, h2, h3, h4, h5, h6'); + generateRule('code', 'code, pre, kbd, samp, tt, .hljs'); + generateRule('table', 'table, .tabulator'); + generateRule('hero', 'h1'); // Most specific, goes last return cssRules.join('\n\n'); } From 0f85af0b49918b85c48a45abbf2b75f1a9f823b0 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Thu, 24 Jul 2025 22:27:52 -0700 Subject: [PATCH 5/5] refine documentation for Chartifact by clarifying Markdown usage and enhancing styling section --- README.md | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 788dca0d..06cc269c 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ Chartifact is a low-code document format for creating interactive, data-driven p * A **library of components** — charts, inputs, tables, images, text, and layout elements — defined declaratively and wired together with reactive variables: + * **Markdown** – With dynamic {{variable}} placeholders * **Inputs** – Textboxes, checkboxes, sliders, dropdowns * **Charts** – Vega and Vega-Lite visualizations * **Tables** – Sortable, filterable, and selectable data grids * **Images** – Dynamic image URLs based on variables - * **Text** – Markdown with dynamic placeholders * **Presets** – Named sets of variable values for quick scenario switching * A **VS Code extension** for editing, previewing, and exporting documents, with optional AI assistance. @@ -40,8 +40,6 @@ The format is designed with AI assistance in mind: * In-editor tools like Ctrl+I and agent mode available in VS Code * HTML exports retain semantic structure for downstream AI tools -This enables both authoring and remixing workflows with language models and agent-based tooling. - ## Data Flow The document runtime is reactive. Components stay in sync through a shared set of variables: @@ -52,17 +50,10 @@ The document runtime is reactive. Components stay in sync through a shared set o * **Vega transforms** provide built-in tools for reshaping data * **Signal bus** coordinates state across all components -Chartifact documents behave like small reactive systems — without custom JavaScript. - ## Styling -Styling is done using scoped CSS blocks embedded in the document. This allows flexible layout and visual design without global side effects: - -* Style documents as articles, dashboards, or slides -* Use CSS to control layout and theming -* No raw HTML injection or global styles - -The predictable, declarative nature of the styling model makes it easy for both humans and LLMs to work with. +* Use CSS to style documents as articles, dashboards, or slides +* Load and apply Google Fonts with semantic font mapping ## Security @@ -71,6 +62,5 @@ Chartifact is designed to be safe by default: * No custom JavaScript execution * CSP-compliant via Vega expression language * No raw HTML in Markdown +* Sanitized CSS * Rendered in sandboxed iframes to isolate execution - -These constraints help ensure portability and safe embedding in various environments.