diff --git a/.gitignore b/.gitignore index 8f36331cab..ca0b618803 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .DS_store .vitest .vitest-attachments +.angular node_modules dist .lighthouse diff --git a/projects/code/src/codeblock/codeblock.test.ts b/projects/code/src/codeblock/codeblock.test.ts index f661feaba3..8988faabf9 100644 --- a/projects/code/src/codeblock/codeblock.test.ts +++ b/projects/code/src/codeblock/codeblock.test.ts @@ -83,6 +83,24 @@ function getTime(): number { expect(element.shadowRoot!.querySelector('.hljs-title')).toBeTruthy(); }); + it('should render source code if slotted within a
block', async () => {
+ element.language = 'typescript';
+ element.innerHTML = 'const answer = 42;
';
+ await elementIsStable(element);
+
+ expect(element.shadowRoot!.querySelector('pre.hljs > code')!.textContent).toBe('const answer = 42;');
+ expect(element.shadowRoot!.querySelector('.hljs-keyword')).toBeTruthy();
+ });
+
+ it('should decode escaped HTML source from a slotted block', async () => {
+ element.language = 'html';
+ element.innerHTML = '<nve-button>Save</nve-button>
';
+ await elementIsStable(element);
+
+ expect(element.shadowRoot!.querySelector('pre.hljs > code')!.textContent).toBe('Save ');
+ expect(element.shadowRoot!.querySelector('.hljs-name')!.textContent).toBe('nve-button');
+ });
+
it('should render HTML source code if slotted HTML content', async () => {
element.language = 'typescript';
const div = document.createElement('div');
diff --git a/projects/code/src/codeblock/codeblock.ts b/projects/code/src/codeblock/codeblock.ts
index 417b048b41..75bba22657 100644
--- a/projects/code/src/codeblock/codeblock.ts
+++ b/projects/code/src/codeblock/codeblock.ts
@@ -22,7 +22,7 @@ hljs.registerLanguage('shell', shell);
* @documentation https://nvidia.github.io/elements/docs/code/codeblock/
* @since 0.1.0
* @entrypoint \@nvidia-elements/code/codeblock
- * @slot - for declarative slotting of source code and not using the `code` property
+ * @slot - source code as text, a ``, or a `` block
* @slot actions - slot for action bar
* @cssprop --background
* @cssprop --padding
@@ -92,6 +92,9 @@ export class CodeBlock extends LitElement implements ContainerElement {
let template = '';
if (n instanceof HTMLTemplateElement) {
template = n.content.textContent ?? '';
+ } else if (n instanceof HTMLPreElement) {
+ const code = n.querySelector('code');
+ template = code ? (code.textContent ?? '') : n.innerHTML;
} else if (n instanceof HTMLElement) {
template = n.innerHTML;
} else {
@@ -107,8 +110,8 @@ export class CodeBlock extends LitElement implements ContainerElement {
render() {
return html`
-
- ${unsafeHTML(this.formattedCode)}
+
+ ${unsafeHTML(this.formattedCode)}
`;
diff --git a/projects/site/src/_11ty/layouts/common.js b/projects/site/src/_11ty/layouts/common.js
index 795d0fded5..a6b4c55c94 100644
--- a/projects/site/src/_11ty/layouts/common.js
+++ b/projects/site/src/_11ty/layouts/common.js
@@ -78,6 +78,7 @@ export const renderBaseHead = data => {
nve-tree:not(:defined),
nve-grid:not(:defined),
nvd-canvas:not(:defined),
+ nve-badge:not(:defined),
nve-button:not(:defined) {
visibility: hidden !important;
}
diff --git a/projects/site/src/_11ty/layouts/docs.css b/projects/site/src/_11ty/layouts/docs.css
index fd578dab2a..d1887925e4 100644
--- a/projects/site/src/_11ty/layouts/docs.css
+++ b/projects/site/src/_11ty/layouts/docs.css
@@ -217,11 +217,41 @@ pre {
nve-codeblock {
--padding: var(--nve-ref-space-lg);
anchor-name: --codeblock;
- min-height: 65px;
+ line-height: 1;
width: 100%;
}
}
+.markdown-codeblock nve-codeblock:not(:defined) {
+ pre {
+ padding: var(--nve-ref-space-lg);
+ background: var(--nve-sys-layer-container-background);
+ border-radius: var(--nve-ref-border-radius-md);
+ font-family: var(--nve-ref-font-family-roboto-mono);
+ overflow: hidden;
+ width: 100%;
+ line-height: 1;
+ scrollbar-color: var(--nve-sys-scrollbar-thumb-color) var(--nve-sys-scrollbar-track-color);
+ scrollbar-width: var(--nve-sys-scrollbar-width);
+ text-wrap-mode: nowrap;
+ }
+
+ code {
+ white-space: var(--white-space);
+ display: block;
+ }
+}
+
+nvd-canvas > pre {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+}
+
.markdown-copy-button {
position: absolute;
position-anchor: --codeblock;
@@ -239,6 +269,7 @@ pre {
/* Canvas Styles */
nvd-canvas {
min-height: 120px;
+ width: 100%;
}
nvd-canvas:has(nve-page),
@@ -256,6 +287,10 @@ nvd-canvas:has(iframe[src*='page']) {
--background: var(--nve-sys-layer-container-background);
}
+nve-grid:not(:defined) {
+ min-height: 220px;
+}
+
/* Page Panel Styles */
nve-page-panel[slot='left-aside'] {
nve-page-panel-content {
diff --git a/projects/site/src/_11ty/layouts/docs.ts b/projects/site/src/_11ty/layouts/docs.ts
index 5dde21a158..cb70e363ec 100644
--- a/projects/site/src/_11ty/layouts/docs.ts
+++ b/projects/site/src/_11ty/layouts/docs.ts
@@ -225,7 +225,7 @@ function addHeadingAnchors() {
const anchor = globalThis.document.createElement('a');
anchor.className = 'heading-anchor';
anchor.href = `${globalThis.window.parent.location.pathname}#${id}`;
- anchor.setAttribute('aria-label', 'Copy link to this section');
+ anchor.setAttribute('aria-label', 'copy permalink');
anchor.innerHTML = ' ';
// Add click handler
diff --git a/projects/site/src/_11ty/layouts/metadata.js b/projects/site/src/_11ty/layouts/metadata.js
index a10550b137..b3f3f9fa78 100644
--- a/projects/site/src/_11ty/layouts/metadata.js
+++ b/projects/site/src/_11ty/layouts/metadata.js
@@ -401,7 +401,7 @@ export function resolvePageMeta(data) {
}
function jsonLdEncode(value) {
- return JSON.stringify(value).replace(/<\//g, '<\\/');
+ return JSON.stringify(value).replaceAll('<', '\\u003c');
}
function isApiReferencePage(data, meta) {
diff --git a/projects/site/src/_11ty/libraries/markdown.js b/projects/site/src/_11ty/libraries/markdown.js
index d62988ce3f..5a631c1ede 100644
--- a/projects/site/src/_11ty/libraries/markdown.js
+++ b/projects/site/src/_11ty/libraries/markdown.js
@@ -1,27 +1,75 @@
import markdownIt from 'markdown-it';
import markdownItLink from 'markdown-it-link-attributes';
+const LANGUAGE_NAMES = {
+ bash: 'Shell',
+ css: 'CSS',
+ go: 'Go',
+ html: 'HTML',
+ javascript: 'JavaScript',
+ js: 'JavaScript',
+ json: 'JSON',
+ markdown: 'Markdown',
+ md: 'Markdown',
+ python: 'Python',
+ shell: 'Shell',
+ sh: 'Shell',
+ toml: 'TOML',
+ ts: 'TypeScript',
+ tsx: 'TypeScript',
+ typescript: 'TypeScript',
+ xml: 'XML',
+ yaml: 'YAML',
+ yml: 'YAML',
+ zsh: 'Shell'
+};
+
const markdown = markdownIt({
html: true,
breaks: false,
linkify: true,
highlight: function (str, lang) {
- lang = lang === 'javascript' ? 'typescript' : lang; // alias javascript to typescript
+ const structuredData = getCodeStructuredData(str, lang);
+ const codeblockLanguage = markdown.utils.escapeHtml(lang === 'javascript' ? 'typescript' : lang); // alias javascript to typescript
return /* html */ `
-
- ${markdown.utils.escapeHtml(str)}
+
+
`;
}
});
+function getCodeStructuredData(code, language) {
+ const languageName = language ? (LANGUAGE_NAMES[language.toLowerCase()] ?? language) : null;
+
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'SoftwareSourceCode',
+ ...(languageName
+ ? {
+ programmingLanguage: {
+ '@type': 'ComputerLanguage',
+ name: languageName
+ }
+ }
+ : {}),
+ codeSampleType: 'code snippet',
+ encodingFormat: 'text/plain',
+ text: code
+ };
+}
+
+function jsonLdEncode(value) {
+ return JSON.stringify(value).replaceAll('<', '\\u003c');
+}
+
markdown.renderer.rules.fence = function (tokens, idx, options, env, slf) {
const token = tokens[idx];
const info = token.info ? markdown.utils.unescapeAll(token.info).trim() : '';
diff --git a/projects/site/src/_11ty/shortcodes/api.js b/projects/site/src/_11ty/shortcodes/api.js
index 199451ae35..229e1aedeb 100644
--- a/projects/site/src/_11ty/shortcodes/api.js
+++ b/projects/site/src/_11ty/shortcodes/api.js
@@ -56,7 +56,7 @@ export function renderAPINameTable(apiValue) {
.render(apiValue.descriptionText ?? apiValue.description ?? '')
.trim()
.replaceAll('', '
')}
-
+
${apiValue.name.charAt(0).toUpperCase() + apiValue.name.slice(1)}
Description
@@ -95,7 +95,7 @@ export function renderAPITable(element, type, options = { container: 'flat' }) {
const noItems = items.length === 0;
return /* html */ `
-
+
${type.charAt(0).toUpperCase() + type.slice(1)}
${type === 'property' ? 'Attribute ' : ''}
diff --git a/projects/site/src/_11ty/shortcodes/example.js b/projects/site/src/_11ty/shortcodes/example.js
index bb0dd1aaf1..1865cb60cd 100644
--- a/projects/site/src/_11ty/shortcodes/example.js
+++ b/projects/site/src/_11ty/shortcodes/example.js
@@ -31,7 +31,6 @@ export async function exampleShortcode(
const defaultConfig = {
inline: true,
- height: '95%',
resizable: true,
summary: true,
align: 'start',
@@ -77,9 +76,8 @@ export async function exampleShortcode(
${formattedSummary}
-
-
- ${md.utils?.escapeHtml(templateContent)}${template}${editButton}
+
+ ${template}${editButton}
`
.trim()
@@ -145,7 +143,7 @@ function getExampleStructuredData(example, templateContent, summary, canvasId, p
}
function jsonLdEncode(value) {
- return JSON.stringify(value).replace(/<\//gi, '<\\/');
+ return JSON.stringify(value).replaceAll('<', '\\u003c');
}
export async function exampleTagsShortcode(ref, exampleName) {
@@ -183,7 +181,7 @@ function reloadScript(example, canvasId) {
const rawTemplate = examples?.items?.find(s => s.id === '${example.id}')?.template ?? '';
const container = document.querySelector('#${canvasId}_content:not(:has(iframe))');
if (container) {
- // parse the template to extract script tags since innerHTML does not execute scripts
+ /* Parse the template to extract script tags since innerHTML does not execute scripts. */
${rewriteDevImports.toString()}
const template = rewriteDevImports(rawTemplate);
const parser = new DOMParser();
diff --git a/projects/site/src/_11ty/shortcodes/example.test.ts b/projects/site/src/_11ty/shortcodes/example.test.ts
index 4901e9b008..16e101f13a 100644
--- a/projects/site/src/_11ty/shortcodes/example.test.ts
+++ b/projects/site/src/_11ty/shortcodes/example.test.ts
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
+import { transformWithOxc } from 'vite';
const patternExample = {
id: 'pattern-chat-popover-chat',
@@ -24,11 +25,18 @@ const structuredDataExample = {
permalink: '@internals/patterns/chat-pattern-chat-structured-data/'
};
+const quotedNameExample = {
+ ...patternExample,
+ id: 'pattern-chat-quoted-name',
+ name: 'Quoted "',
+ permalink: '@internals/patterns/chat-pattern-chat-quoted-name/'
+};
+
async function importShortcode() {
vi.resetModules();
vi.doMock('../../index.11tydata.js', () => ({
siteData: {
- examples: [patternExample, structuredDataExample]
+ examples: [patternExample, structuredDataExample, quotedNameExample]
}
}));
vi.doMock('@internals/tools/playground', () => ({
@@ -58,6 +66,25 @@ describe('exampleShortcode', () => {
expect(html).not.toContain('/docs/patterns/chat/examples/');
});
+ it('should slot its source block directly into the canvas', async () => {
+ const { exampleShortcode } = await importShortcode();
+
+ const html = await exampleShortcode('@internals/patterns/chat.examples.json', 'PopoverChat');
+
+ expect(html).toContain('
');
+ });
+
+ it('should escape the example name in the canvas label', async () => {
+ const { exampleShortcode } = await importShortcode();
+
+ const html = await exampleShortcode('@internals/patterns/chat.examples.json', quotedNameExample.name);
+
+ expect(html).toContain('aria-label="example \'Quoted "<Example>\'"');
+ expect(html).not.toContain('aria-label="example \'Quoted "\'"');
+ });
+
it('should render valid and safely encoded SoftwareSourceCode metadata', async () => {
vi.stubEnv('ELEMENTS_SITE_URL', 'https://nvidia.github.io');
vi.stubEnv('ELEMENTS_REPO_BASE_URL', 'https://github.com/NVIDIA/elements');
@@ -74,7 +101,8 @@ describe('exampleShortcode', () => {
const canonicalPageUrl = 'https://nvidia.github.io/elements/docs/patterns/chat/';
const canonicalExampleUrl = `${canonicalPageUrl}#internals-patterns-chat-examples-json_pattern-chat-structured-data`;
- expect(script?.[1]).toContain('<\\/SCRIPT>');
+ expect(script?.[1]).toContain('\\u003c/SCRIPT>');
+ expect(script?.[1]).not.toContain('<');
expect(structuredData).toMatchObject({
'@context': 'https://schema.org',
'@id': canonicalExampleUrl,
@@ -114,6 +142,20 @@ describe('exampleShortcode', () => {
});
});
+ it('should render a reload module that remains valid when whitespace collapses', async () => {
+ vi.stubEnv('ELEVENTY_RUN_MODE', 'serve');
+ const { exampleShortcode } = await importShortcode();
+
+ const html = await exampleShortcode('@internals/patterns/chat.examples.json', 'PopoverChat');
+ const reloadModule = [...html.matchAll(/`;
@@ -34,14 +41,18 @@ async function minifyHTML(html) {
// Replace template contents with markers
const htmlWithMarkers = scriptSpacing.replace(/([\s\S]*?)<\/template>/g, (match, content) => {
- const marker = `__TEMPLATE_${counter}__`;
- templateContents[counter] = content;
- counter++;
- return `${marker}`;
+ return `${protectContent(content)}`;
});
+ const htmlWithJsonLdMarkers = htmlWithMarkers.replace(
+ /`;
+ }
+ );
+
// Minify the HTML
- const minifiedHtml = await htmlMinify.minify(htmlWithMarkers, {
+ const minifiedHtml = await htmlMinify.minify(htmlWithJsonLdMarkers, {
includeAutoGeneratedTags: true,
removeAttributeQuotes: true,
removeComments: true,
@@ -55,8 +66,8 @@ async function minifyHTML(html) {
minifyCSS: true
});
- // Restore template contents
- return minifiedHtml.replace(/__TEMPLATE_(\d+)__/g, (match, index) => {
- return templateContents[parseInt(index)];
+ // Restore protected contents
+ return minifiedHtml.replace(new RegExp(`${markerPrefix}(\\d+)__`, 'g'), (match, index) => {
+ return protectedContents[Number(index)] ?? match;
});
}
diff --git a/projects/site/src/_11ty/transforms/html-minify.test.ts b/projects/site/src/_11ty/transforms/html-minify.test.ts
index f96dd6765f..62777ae859 100644
--- a/projects/site/src/_11ty/transforms/html-minify.test.ts
+++ b/projects/site/src/_11ty/transforms/html-minify.test.ts
@@ -22,4 +22,32 @@ describe('htmlMinifyTransform', () => {
await expect(htmlMinifyTransform.call({ page: {} }, xml, '/atom.xml')).resolves.toBe(xml);
});
+
+ it('should preserve safely encoded JSON-LD contents', async () => {
+ const structuredData = {
+ '@context': 'https://schema.org',
+ '@type': 'SoftwareSourceCode',
+ text: ''
+ };
+ const jsonLd = JSON.stringify(structuredData).replaceAll('<', '\\u003c');
+ const html = ``;
+ const result = await htmlMinifyTransform.call({ page: {} }, html, '/index.html');
+ const script = result.match(/
+