From bb8c72624ff04675dee548b6f398206a037e8126 Mon Sep 17 00:00:00 2001 From: Dan <37590801+DanOnCall@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:43:30 -0400 Subject: [PATCH 1/9] refactor: Update styling and add copy as Markdown feature --- src/app/homepage/homepage.component.scss | 36 +++++++++++++++---- src/app/homepage/pages/page/page.component.ts | 10 ++++++ tools/transforms/content-package/index.ts | 4 +++ .../content-package/readers/content.ts | 9 +++-- .../rendering/filters/base64.ts | 8 +++++ .../processors/convertToJson.ts | 9 +++-- .../templates/content.template.html | 30 +++++++++++----- 7 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 tools/transforms/content-package/rendering/filters/base64.ts diff --git a/src/app/homepage/homepage.component.scss b/src/app/homepage/homepage.component.scss index fbfccb1346..31ca187b8a 100644 --- a/src/app/homepage/homepage.component.scss +++ b/src/app/homepage/homepage.component.scss @@ -77,7 +77,9 @@ } } - .content blockquote strong, .content #carbonads a, .content blockquote a { + .content blockquote strong, + .content #carbonads a, + .content blockquote a { -webkit-text-fill-color: unset !important; -webkit-background-clip: unset !important; background: none; @@ -258,17 +260,16 @@ } &::before { - content: ""; + content: ''; position: absolute; inset: 0; border-radius: 2px; padding: 2px; background: var(--primary-gradient); - -webkit-mask: - linear-gradient(#fff 0 0) content-box, + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); -webkit-mask-composite: xor; - mask-composite: exclude; + mask-composite: exclude; pointer-events: none; z-index: 2; } @@ -432,10 +433,14 @@ } } -.github-links { +.content-actions { float: right; margin-top: 24px; + display: flex; + gap: 8px; +} +.github-links { svg { font-size: 17px; color: var(--color-1dp); @@ -446,6 +451,23 @@ } } +.content-buttons { + button { + background: none; + border: none; + cursor: pointer; + } + + svg { + font-size: 17px; + color: var(--color-1dp); + } + + button:hover svg { + color: var(--primary); + } +} + .toc-wrapper { @include utils.media(large) { display: none; @@ -465,7 +487,7 @@ -webkit-filter: var(--company-filter); filter: var(--company-filter); opacity: var(--company-logo-opacity); - + &:hover { -webkit-filter: var(--company-filter-hover); filter: var(--company-filter-hover); diff --git a/src/app/homepage/pages/page/page.component.ts b/src/app/homepage/pages/page/page.component.ts index bda9ecc98f..882e30cea9 100644 --- a/src/app/homepage/pages/page/page.component.ts +++ b/src/app/homepage/pages/page/page.component.ts @@ -32,6 +32,16 @@ export class BasePageComponent implements AfterViewChecked { return this.isHljsInitialized; } + copyAsMarkdown() { + const rawMarkdownEl = this.el.nativeElement.querySelector('#raw-markdown'); + if (!rawMarkdownEl) { + return; + } + const encoded = rawMarkdownEl.textContent; + const decoded = atob(encoded); + navigator.clipboard.writeText(decoded); + } + ngAfterViewChecked() { this.initHljs(); } diff --git a/tools/transforms/content-package/index.ts b/tools/transforms/content-package/index.ts index 7c1dc19685..eec47ca6e0 100644 --- a/tools/transforms/content-package/index.ts +++ b/tools/transforms/content-package/index.ts @@ -6,6 +6,7 @@ import { extractContentTitleProcessor, } from './processors'; import { ContentFileReader, contentFileReader } from './readers'; +import { base64NunjucksFilter } from './rendering/filters/base64'; import { nestjsMarkedNunjucksFilter } from './rendering/filters/nestjs-marked'; import { nestjsMarkedNunjucksTag } from './rendering/tags/nestjs-marked'; import { renderNestJSMarkdown } from './services'; @@ -15,6 +16,7 @@ export default new Package('content', []) .factory(renderNestJSMarkdown) .factory(nestjsMarkedNunjucksTag) .factory(nestjsMarkedNunjucksFilter) + .factory(base64NunjucksFilter) .processor(extractContentTitleProcessor) .processor(computeOutputPathProcessor) .processor(computeWhoUsesProcessor) @@ -26,9 +28,11 @@ export default new Package('content', []) templateEngine: any, nestjsMarkedNunjucksTag: any, nestjsMarkedNunjucksFilter: any, + base64NunjucksFilter: any, ) => { templateEngine.tags.push(nestjsMarkedNunjucksTag); templateEngine.filters.push(nestjsMarkedNunjucksFilter); + templateEngine.filters.push(base64NunjucksFilter); }, ) .config((computeIdsProcessor) => { diff --git a/tools/transforms/content-package/readers/content.ts b/tools/transforms/content-package/readers/content.ts index 9aa208c926..0d302d1eb5 100644 --- a/tools/transforms/content-package/readers/content.ts +++ b/tools/transforms/content-package/readers/content.ts @@ -9,11 +9,10 @@ export class ContentFileReader implements FileReader { getDocs(fileInfo: FileInfo) { return [ { - docType: fileInfo.baseName === 'who-uses' - ? 'who-uses' - : 'content', - content: fileInfo.content - } + docType: fileInfo.baseName === 'who-uses' ? 'who-uses' : 'content', + content: fileInfo.content, + rawContent: fileInfo.content, + }, ]; } } diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts new file mode 100644 index 0000000000..11af0269d9 --- /dev/null +++ b/tools/transforms/content-package/rendering/filters/base64.ts @@ -0,0 +1,8 @@ +export function base64NunjucksFilter() { + return { + name: 'base64', + process(str: string) { + return Buffer.from(str).toString('base64'); + }, + }; +} diff --git a/tools/transforms/nestjs-base-package/processors/convertToJson.ts b/tools/transforms/nestjs-base-package/processors/convertToJson.ts index af0a44d7a6..5228b89893 100644 --- a/tools/transforms/nestjs-base-package/processors/convertToJson.ts +++ b/tools/transforms/nestjs-base-package/processors/convertToJson.ts @@ -4,10 +4,13 @@ import { CreateDocMessage, DgeniLogger } from '../../shared'; /** * Converts doc types set by the `docTypes` property to JSON documents. * A document requires `doc.renderContent` and `doc.title` or `doc.name` - * property. + * property. */ export class ConvertToJsonProcessor implements Processor { - constructor(private log: DgeniLogger, private createDocMessage: CreateDocMessage) {} + constructor( + private log: DgeniLogger, + private createDocMessage: CreateDocMessage, + ) {} $runAfter = ['postProcessHtml']; $runBefore = ['writeFilesProcessor']; docTypes = []; @@ -32,7 +35,7 @@ export class ConvertToJsonProcessor implements Processor { doc.renderedContent = JSON.stringify( { id: doc.path, title, contents }, null, - 2 + 2, ); } }); diff --git a/tools/transforms/templates/content.template.html b/tools/transforms/templates/content.template.html index d4d8833475..eebf28c9dc 100644 --- a/tools/transforms/templates/content.template.html +++ b/tools/transforms/templates/content.template.html @@ -1,13 +1,27 @@ {% block content %}
- From 493f997209d17263f2fd826fd03c3613a0c27809 Mon Sep 17 00:00:00 2001 From: Dan <37590801+DanOnCall@users.noreply.github.com> Date: Thu, 3 Jul 2025 20:55:36 -0400 Subject: [PATCH 2/9] refactor: add states to copy button --- src/app/homepage/homepage.component.scss | 26 ++++++- src/app/homepage/pages/page/page.component.ts | 78 ++++++++++++++++++- src/scss/theme.scss | 18 ++++- .../templates/content.template.html | 5 +- 4 files changed, 117 insertions(+), 10 deletions(-) diff --git a/src/app/homepage/homepage.component.scss b/src/app/homepage/homepage.component.scss index 31ca187b8a..d6a313d2e3 100644 --- a/src/app/homepage/homepage.component.scss +++ b/src/app/homepage/homepage.component.scss @@ -437,7 +437,7 @@ float: right; margin-top: 24px; display: flex; - gap: 8px; + gap: 16px; } .github-links { @@ -456,18 +456,38 @@ background: none; border: none; cursor: pointer; + padding: 0; } svg { font-size: 17px; - color: var(--color-1dp); } +} - button:hover svg { +.content-action { + color: var(--color-1dp); + + &:hover { color: var(--primary); } } +.content-action--success { + color: var(--success); + + &:hover { + color: var(--success); + } +} + +.content-action--error { + color: var(--error); + + &:hover { + color: var(--error); + } +} + .toc-wrapper { @include utils.media(large) { display: none; diff --git a/src/app/homepage/pages/page/page.component.ts b/src/app/homepage/pages/page/page.component.ts index 882e30cea9..3316884391 100644 --- a/src/app/homepage/pages/page/page.component.ts +++ b/src/app/homepage/pages/page/page.component.ts @@ -3,6 +3,7 @@ import { ApplicationRef, Component, ElementRef, + OnDestroy, } from '@angular/core'; import * as Prism from 'prismjs'; import 'prismjs/prism'; @@ -16,8 +17,10 @@ import 'prismjs/components/prism-bash'; selector: 'app-base-page', template: ``, }) -export class BasePageComponent implements AfterViewChecked { +export class BasePageComponent implements AfterViewChecked, OnDestroy { private isHljsInitialized = false; + public copyButtonState: 'idle' | 'copied' | 'error' = 'idle'; + private copyResetTimeout: number | null = null; constructor( private readonly applicationRef: ApplicationRef, @@ -32,14 +35,83 @@ export class BasePageComponent implements AfterViewChecked { return this.isHljsInitialized; } - copyAsMarkdown() { + private isElement(target: EventTarget | null): target is Element { + return target instanceof Element; + } + + async copyAsMarkdown(event: Event) { + if (!this.isElement(event.target)) { + return; + } + + const buttonElement = event.target.closest('button'); + if (!buttonElement) { + return; + } + const rawMarkdownEl = this.el.nativeElement.querySelector('#raw-markdown'); if (!rawMarkdownEl) { return; } + const encoded = rawMarkdownEl.textContent; const decoded = atob(encoded); - navigator.clipboard.writeText(decoded); + + if (!navigator.clipboard) { + this.copyButtonState = 'error'; + buttonElement.classList.add('content-action--error'); + buttonElement.innerHTML = ''; + buttonElement.setAttribute('aria-label', 'Copy failed'); + buttonElement.setAttribute('title', 'Copy failed'); + return; + } + + await navigator.clipboard.writeText(decoded); + + this.copyButtonState = 'copied'; + + // Add success state class and replace with checkmark + buttonElement.classList.add('content-action--success'); + buttonElement.innerHTML = ''; + buttonElement.setAttribute('aria-label', 'Copied'); + buttonElement.setAttribute('title', 'Copied'); + } + + onCopyButtonMouseLeave(event: Event) { + if (this.copyButtonState === 'idle') { + return; + } + + if (this.copyResetTimeout) { + clearTimeout(this.copyResetTimeout); + } + + this.copyResetTimeout = setTimeout(() => { + this.copyButtonState = 'idle'; + + if (!this.isElement(event.target)) { + return; + } + + const buttonElement = event.target.closest('button'); + if (!buttonElement) { + return; + } + + // Reset to copy icon + buttonElement.className = 'content-action'; + buttonElement.innerHTML = ''; + buttonElement.setAttribute('aria-label', 'Copy as Markdown'); + buttonElement.setAttribute('title', 'Copy as Markdown'); + this.copyResetTimeout = null; + }, 500) as unknown as number; + } + + ngOnDestroy() { + // Clean up timeout if component is destroyed + if (this.copyResetTimeout) { + clearTimeout(this.copyResetTimeout); + } } ngAfterViewChecked() { diff --git a/src/scss/theme.scss b/src/scss/theme.scss index d0a3f29414..27eb88b251 100644 --- a/src/scss/theme.scss +++ b/src/scss/theme.scss @@ -3,10 +3,14 @@ --primary-accent: #ea2868; --primary-1dp: #d71e38; --primary-2dp: #da2640; - --primary-3dp:#db2840; + --primary-3dp: #db2840; --primary-4dp: #e40020; --primary-5dp: #ff0023; - --primary-gradient: linear-gradient(90deg, var(--primary) 0%, var(--primary-accent) 100%); + --primary-gradient: linear-gradient( + 90deg, + var(--primary) 0%, + var(--primary-accent) 100% + ); --color: #404040; --color-1dp: #151515; @@ -38,6 +42,8 @@ --error: #ed2945; --error-background: #f9eff1; + --success: #28a745; + --company-filter: grayscale(100%); --company-filter-hover: grayscale(0%); --company-logo-filter: grayscale(1); @@ -56,7 +62,11 @@ --primary-3dp: #f1455f; --primary-4dp: #f23c57; --primary-5dp: #f23551; - --primary-gradient: linear-gradient(90deg, var(--primary) 0%, var(--primary-accent) 100%); + --primary-gradient: linear-gradient( + 90deg, + var(--primary) 0%, + var(--primary-accent) 100% + ); --color: #dfdfe3; --color-1dp: #d0d0d4; @@ -88,6 +98,8 @@ --error: #ff677c; --error-background: #3a2f30; + --success: #28a745; + --company-filter: contrast(0.5); --company-filter-hover: opacity(1); --company-logo-filter: contrast(0.5) grayscale(100%); diff --git a/tools/transforms/templates/content.template.html b/tools/transforms/templates/content.template.html index eebf28c9dc..99a5aa8eb3 100644 --- a/tools/transforms/templates/content.template.html +++ b/tools/transforms/templates/content.template.html @@ -6,9 +6,12 @@
From ecafa25fcdd9ac4fc224d747e6b2ad8ee3b41d85 Mon Sep 17 00:00:00 2001 From: Dan <37590801+DanOnCall@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:12:22 -0400 Subject: [PATCH 3/9] feat: add domain to all images to make content portable --- .../content-package/rendering/filters/base64.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts index 11af0269d9..93f9a5322e 100644 --- a/tools/transforms/content-package/rendering/filters/base64.ts +++ b/tools/transforms/content-package/rendering/filters/base64.ts @@ -2,7 +2,14 @@ export function base64NunjucksFilter() { return { name: 'base64', process(str: string) { - return Buffer.from(str).toString('base64'); + // Fix relative URLs to absolute URLs for copying + const fixedMarkdown = str + // Fix image sources: /assets/... → https://docs.nestjs.com/assets/... + .replace(/src="(\/[^"]+)"/g, 'src="https://docs.nestjs.com$1"') + // Fix documentation links: [text](/some/path) → [text](https://docs.nestjs.com/some/path) + .replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)'); + + return Buffer.from(fixedMarkdown).toString('base64'); }, }; } From 0ad6b659e50745d8dcd4bd8bbfed9f970721a912 Mon Sep 17 00:00:00 2001 From: Dan <37590801+DanOnCall@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:19:17 -0400 Subject: [PATCH 4/9] feat: remove angular comps from markdown --- .../transforms/content-package/rendering/filters/base64.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts index 93f9a5322e..d5e5aa26df 100644 --- a/tools/transforms/content-package/rendering/filters/base64.ts +++ b/tools/transforms/content-package/rendering/filters/base64.ts @@ -7,7 +7,11 @@ export function base64NunjucksFilter() { // Fix image sources: /assets/... → https://docs.nestjs.com/assets/... .replace(/src="(\/[^"]+)"/g, 'src="https://docs.nestjs.com$1"') // Fix documentation links: [text](/some/path) → [text](https://docs.nestjs.com/some/path) - .replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)'); + .replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)') + // Remove custom Angular components for cleaner portable markdown + .replace(/]*><\/app-[^>]*>/g, '') + // Remove any empty lines left by component removal + .replace(/\n\s*\n\s*\n/g, '\n\n'); return Buffer.from(fixedMarkdown).toString('base64'); }, From 208f8d10cfc19232ae57419fdea4b38259f3f05c Mon Sep 17 00:00:00 2001 From: Dan <37590801+DanOnCall@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:22:55 -0400 Subject: [PATCH 5/9] feat: rename filter --- tools/transforms/content-package/index.ts | 8 ++++---- .../content-package/rendering/filters/base64.ts | 4 ++-- tools/transforms/templates/content.template.html | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/transforms/content-package/index.ts b/tools/transforms/content-package/index.ts index eec47ca6e0..7a870335bd 100644 --- a/tools/transforms/content-package/index.ts +++ b/tools/transforms/content-package/index.ts @@ -6,7 +6,7 @@ import { extractContentTitleProcessor, } from './processors'; import { ContentFileReader, contentFileReader } from './readers'; -import { base64NunjucksFilter } from './rendering/filters/base64'; +import { cleanMarkdownNunjucksFilter } from './rendering/filters/base64'; import { nestjsMarkedNunjucksFilter } from './rendering/filters/nestjs-marked'; import { nestjsMarkedNunjucksTag } from './rendering/tags/nestjs-marked'; import { renderNestJSMarkdown } from './services'; @@ -16,7 +16,7 @@ export default new Package('content', []) .factory(renderNestJSMarkdown) .factory(nestjsMarkedNunjucksTag) .factory(nestjsMarkedNunjucksFilter) - .factory(base64NunjucksFilter) + .factory(cleanMarkdownNunjucksFilter) .processor(extractContentTitleProcessor) .processor(computeOutputPathProcessor) .processor(computeWhoUsesProcessor) @@ -28,11 +28,11 @@ export default new Package('content', []) templateEngine: any, nestjsMarkedNunjucksTag: any, nestjsMarkedNunjucksFilter: any, - base64NunjucksFilter: any, + cleanMarkdownNunjucksFilter: any, ) => { templateEngine.tags.push(nestjsMarkedNunjucksTag); templateEngine.filters.push(nestjsMarkedNunjucksFilter); - templateEngine.filters.push(base64NunjucksFilter); + templateEngine.filters.push(cleanMarkdownNunjucksFilter); }, ) .config((computeIdsProcessor) => { diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts index d5e5aa26df..68f49e9243 100644 --- a/tools/transforms/content-package/rendering/filters/base64.ts +++ b/tools/transforms/content-package/rendering/filters/base64.ts @@ -1,6 +1,6 @@ -export function base64NunjucksFilter() { +export function cleanMarkdownNunjucksFilter() { return { - name: 'base64', + name: 'cleanMarkdown', process(str: string) { // Fix relative URLs to absolute URLs for copying const fixedMarkdown = str diff --git a/tools/transforms/templates/content.template.html b/tools/transforms/templates/content.template.html index 99a5aa8eb3..eb5e2d957b 100644 --- a/tools/transforms/templates/content.template.html +++ b/tools/transforms/templates/content.template.html @@ -1,7 +1,7 @@ {% block content %}
From 638dfd61a400d1507c5d3ca94530bf4b710b1643 Mon Sep 17 00:00:00 2001 From: Dan <37590801+DanOnCall@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:58:16 -0400 Subject: [PATCH 6/9] feat: parse @@filename and @@switch --- .../rendering/filters/base64.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tools/transforms/content-package/rendering/filters/base64.ts b/tools/transforms/content-package/rendering/filters/base64.ts index 68f49e9243..e7590035d7 100644 --- a/tools/transforms/content-package/rendering/filters/base64.ts +++ b/tools/transforms/content-package/rendering/filters/base64.ts @@ -2,18 +2,34 @@ export function cleanMarkdownNunjucksFilter() { return { name: 'cleanMarkdown', process(str: string) { - // Fix relative URLs to absolute URLs for copying - const fixedMarkdown = str + const cleanMarkdown = str + // Fix relative URLs to absolute URLs for copying // Fix image sources: /assets/... → https://docs.nestjs.com/assets/... .replace(/src="(\/[^"]+)"/g, 'src="https://docs.nestjs.com$1"') // Fix documentation links: [text](/some/path) → [text](https://docs.nestjs.com/some/path) .replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)') // Remove custom Angular components for cleaner portable markdown .replace(/]*><\/app-[^>]*>/g, '') + // Convert NestJS-specific syntax to standard markdown + .replace(/@@filename\(([^)]+)\)/g, (match, filename) => { + // Add .ts extension if filename doesn't already have a proper file extension + return filename.match( + /\.(ts|js|json|html|css|scss|yaml|yml|xml|dockerfile|md)$/i, + ) + ? `// ${filename}` + : `// ${filename}.ts`; + }) + // Remove empty filename placeholders + .replace(/@@filename\(\)/g, '') + // Split TypeScript/JavaScript versions into separate code blocks + .replace( + /@@switch\n?/g, + '```\n\nJavaScript version:\n\n```javascript\n', + ) // Remove any empty lines left by component removal .replace(/\n\s*\n\s*\n/g, '\n\n'); - return Buffer.from(fixedMarkdown).toString('base64'); + return Buffer.from(cleanMarkdown).toString('base64'); }, }; } From bd533eb10e76b57738b180f8f13a68e331cc36ee Mon Sep 17 00:00:00 2001 From: Dan <37590801+DanOnCall@users.noreply.github.com> Date: Fri, 4 Jul 2025 00:20:29 -0400 Subject: [PATCH 7/9] feat: load md files on demand from static assets --- .gitignore | 3 + src/app/homepage/pages/page/page.component.ts | 52 ++++++++++------ tools/transforms/content-package/index.ts | 2 + .../content-package/processors/index.ts | 3 +- .../processors/outputCleanMarkdown.ts | 61 +++++++++++++++++++ .../templates/content.template.html | 3 - 6 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 tools/transforms/content-package/processors/outputCleanMarkdown.ts diff --git a/.gitignore b/.gitignore index b30333ad72..333630ae3d 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ Thumbs.db # compiled templates src/app/homepage/pages/**/*.html + +# generated markdown content +src/assets/content/ diff --git a/src/app/homepage/pages/page/page.component.ts b/src/app/homepage/pages/page/page.component.ts index 3316884391..6b169973c5 100644 --- a/src/app/homepage/pages/page/page.component.ts +++ b/src/app/homepage/pages/page/page.component.ts @@ -4,7 +4,11 @@ import { Component, ElementRef, OnDestroy, + inject, } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Router } from '@angular/router'; +import { firstValueFrom } from 'rxjs'; import * as Prism from 'prismjs'; import 'prismjs/prism'; import 'prismjs/components/prism-typescript'; @@ -19,6 +23,8 @@ import 'prismjs/components/prism-bash'; }) export class BasePageComponent implements AfterViewChecked, OnDestroy { private isHljsInitialized = false; + private readonly http = inject(HttpClient); + private readonly router = inject(Router); public copyButtonState: 'idle' | 'copied' | 'error' = 'idle'; private copyResetTimeout: number | null = null; @@ -49,34 +55,46 @@ export class BasePageComponent implements AfterViewChecked, OnDestroy { return; } - const rawMarkdownEl = this.el.nativeElement.querySelector('#raw-markdown'); - if (!rawMarkdownEl) { - return; - } + try { + // Get the current route path (matches doc.id from Dgeni) + const currentPath = this.router.url.slice(1); + const markdownUrl = `/assets/content/${currentPath}.md`; - const encoded = rawMarkdownEl.textContent; - const decoded = atob(encoded); + // Fetch the cleaned Markdown file + const markdown = await firstValueFrom( + this.http.get(markdownUrl, { responseType: 'text' }), + ); - if (!navigator.clipboard) { - this.copyButtonState = 'error'; - buttonElement.classList.add('content-action--error'); - buttonElement.innerHTML = ''; - buttonElement.setAttribute('aria-label', 'Copy failed'); - buttonElement.setAttribute('title', 'Copy failed'); - return; - } + const canCopy = markdown && navigator.clipboard; + if (!canCopy) { + this.setCopyError(buttonElement); + return; + } - await navigator.clipboard.writeText(decoded); + await navigator.clipboard.writeText(markdown); + this.setCopySuccess(buttonElement); + } catch (error) { + console.error('Failed to copy markdown:', error); + this.setCopyError(buttonElement); + } + } + private setCopySuccess(buttonElement: Element) { this.copyButtonState = 'copied'; - - // Add success state class and replace with checkmark buttonElement.classList.add('content-action--success'); buttonElement.innerHTML = ''; buttonElement.setAttribute('aria-label', 'Copied'); buttonElement.setAttribute('title', 'Copied'); } + private setCopyError(buttonElement: Element) { + this.copyButtonState = 'error'; + buttonElement.classList.add('content-action--error'); + buttonElement.innerHTML = ''; + buttonElement.setAttribute('aria-label', 'Copy failed'); + buttonElement.setAttribute('title', 'Copy failed'); + } + onCopyButtonMouseLeave(event: Event) { if (this.copyButtonState === 'idle') { return; diff --git a/tools/transforms/content-package/index.ts b/tools/transforms/content-package/index.ts index 7a870335bd..13d4c85207 100644 --- a/tools/transforms/content-package/index.ts +++ b/tools/transforms/content-package/index.ts @@ -4,6 +4,7 @@ import { computeOutputPathProcessor, computeWhoUsesProcessor, extractContentTitleProcessor, + outputCleanMarkdownProcessor, } from './processors'; import { ContentFileReader, contentFileReader } from './readers'; import { cleanMarkdownNunjucksFilter } from './rendering/filters/base64'; @@ -20,6 +21,7 @@ export default new Package('content', []) .processor(extractContentTitleProcessor) .processor(computeOutputPathProcessor) .processor(computeWhoUsesProcessor) + .processor(outputCleanMarkdownProcessor) .config((readFilesProcessor: any, contentFileReader: ContentFileReader) => { readFilesProcessor.fileReaders.push(contentFileReader); }) diff --git a/tools/transforms/content-package/processors/index.ts b/tools/transforms/content-package/processors/index.ts index 4553648936..3684cdebb6 100644 --- a/tools/transforms/content-package/processors/index.ts +++ b/tools/transforms/content-package/processors/index.ts @@ -1,3 +1,4 @@ -export * from './extractContentTitle'; export * from './computeOutputPath'; export * from './computeWhoUses'; +export * from './extractContentTitle'; +export * from './outputCleanMarkdown'; diff --git a/tools/transforms/content-package/processors/outputCleanMarkdown.ts b/tools/transforms/content-package/processors/outputCleanMarkdown.ts new file mode 100644 index 0000000000..19e3fbb517 --- /dev/null +++ b/tools/transforms/content-package/processors/outputCleanMarkdown.ts @@ -0,0 +1,61 @@ +import { writeFileSync, mkdirSync } from 'fs'; +import { dirname, join } from 'path'; +import { Processor } from 'dgeni'; +import { Document } from '../../shared'; + +export class OutputCleanMarkdownProcessor implements Processor { + $runAfter = ['readFilesProcessor']; + $runBefore = ['renderDocsProcessor']; + + $process(docs: Document[]) { + docs.forEach((doc) => { + if (doc.docType === 'content' && doc.content) { + // Clean the raw markdown content + const cleanedMarkdown = this.cleanMarkdown(doc.content); + + // Output to src/assets/content/{id}.md so Angular can serve as assets + const outputPath = join('src/assets/content', `${doc.id}.md`); + + // Ensure directory exists + mkdirSync(dirname(outputPath), { recursive: true }); + + // Write cleaned markdown file + writeFileSync(outputPath, cleanedMarkdown, 'utf8'); + } + }); + } + + private cleanMarkdown(str: string): string { + return ( + str + // Fix image sources: /assets/... → https://docs.nestjs.com/assets/... + .replace(/src="(\/[^"]+)"/g, 'src="https://docs.nestjs.com$1"') + // Fix documentation links: [text](/some/path) → [text](https://docs.nestjs.com/some/path) + .replace(/\]\((\/[^)]+)\)/g, '](https://docs.nestjs.com$1)') + // Remove custom Angular components for cleaner portable markdown + .replace(/]*><\/app-[^>]*>/g, '') + // Convert @@filename to comments with .ts extension + .replace(/@@filename\(([^)]+)\)/g, (match, filename) => { + // Add .ts extension if filename doesn't already have a proper file extension + return filename.match( + /\.(ts|js|json|html|css|scss|yaml|yml|xml|dockerfile|md)$/i, + ) + ? `// ${filename}` + : `// ${filename}.ts`; + }) + // Remove empty filename placeholders + .replace(/@@filename\(\)/g, '') + // Split TypeScript/JavaScript versions into separate code blocks + .replace( + /@@switch\n?/g, + '```\n\nJavaScript version:\n\n```javascript\n', + ) + // Remove any empty lines left by component removal + .replace(/\n\s*\n\s*\n/g, '\n\n') + ); + } +} + +export function outputCleanMarkdownProcessor() { + return new OutputCleanMarkdownProcessor(); +} diff --git a/tools/transforms/templates/content.template.html b/tools/transforms/templates/content.template.html index eb5e2d957b..6f3535dd87 100644 --- a/tools/transforms/templates/content.template.html +++ b/tools/transforms/templates/content.template.html @@ -1,8 +1,5 @@ {% block content %}
-