diff --git a/examples/rollup/src/App.svelte b/examples/rollup/src/App.svelte index 745f8bcb..14345983 100644 --- a/examples/rollup/src/App.svelte +++ b/examples/rollup/src/App.svelte @@ -3,6 +3,7 @@ import typescript from "svelte-highlight/languages/typescript"; import atomOneDark from "svelte-highlight/styles/atom-one-dark"; import DynamicImport from "./DynamicImport.svelte"; + import ScopedStyles from "./ScopedStyles.svelte"; const code = "const add = (a: number, b: number) => a + b;"; @@ -14,3 +15,5 @@ + + diff --git a/examples/rollup/src/ScopedStyles.svelte b/examples/rollup/src/ScopedStyles.svelte new file mode 100644 index 00000000..7abd7a9c --- /dev/null +++ b/examples/rollup/src/ScopedStyles.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/examples/routify/src/components/ScopedStyles.svelte b/examples/routify/src/components/ScopedStyles.svelte new file mode 100644 index 00000000..7abd7a9c --- /dev/null +++ b/examples/routify/src/components/ScopedStyles.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/examples/routify/src/routes/index.svelte b/examples/routify/src/routes/index.svelte index 48dd90ed..a91d7470 100644 --- a/examples/routify/src/routes/index.svelte +++ b/examples/routify/src/routes/index.svelte @@ -3,6 +3,7 @@ import typescript from "svelte-highlight/languages/typescript"; import atomOneDark from "svelte-highlight/styles/atom-one-dark"; import DynamicImport from "../components/DynamicImport.svelte"; + import ScopedStyles from "../components/ScopedStyles.svelte"; const code = "const add = (a: number, b: number) => a + b;"; @@ -14,3 +15,5 @@ + + diff --git a/examples/sveltekit/src/lib/ScopedStyles.svelte b/examples/sveltekit/src/lib/ScopedStyles.svelte new file mode 100644 index 00000000..7abd7a9c --- /dev/null +++ b/examples/sveltekit/src/lib/ScopedStyles.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/examples/sveltekit/src/routes/+page.svelte b/examples/sveltekit/src/routes/+page.svelte index 70390d08..6e3e4740 100644 --- a/examples/sveltekit/src/routes/+page.svelte +++ b/examples/sveltekit/src/routes/+page.svelte @@ -3,6 +3,7 @@ import typescript from "svelte-highlight/languages/typescript"; import atomOneDark from "svelte-highlight/styles/atom-one-dark"; import DynamicImport from "$lib/DynamicImport.svelte"; + import ScopedStyles from "$lib/ScopedStyles.svelte"; const code = "const add = (a: number, b: number) => a + b;"; @@ -14,3 +15,5 @@ + + diff --git a/examples/vite/src/App.svelte b/examples/vite/src/App.svelte index 745f8bcb..14345983 100644 --- a/examples/vite/src/App.svelte +++ b/examples/vite/src/App.svelte @@ -3,6 +3,7 @@ import typescript from "svelte-highlight/languages/typescript"; import atomOneDark from "svelte-highlight/styles/atom-one-dark"; import DynamicImport from "./DynamicImport.svelte"; + import ScopedStyles from "./ScopedStyles.svelte"; const code = "const add = (a: number, b: number) => a + b;"; @@ -14,3 +15,5 @@ + + diff --git a/examples/vite/src/ScopedStyles.svelte b/examples/vite/src/ScopedStyles.svelte new file mode 100644 index 00000000..7abd7a9c --- /dev/null +++ b/examples/vite/src/ScopedStyles.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/examples/vite@svelte-5/src/App.svelte b/examples/vite@svelte-5/src/App.svelte index 745f8bcb..14345983 100644 --- a/examples/vite@svelte-5/src/App.svelte +++ b/examples/vite@svelte-5/src/App.svelte @@ -3,6 +3,7 @@ import typescript from "svelte-highlight/languages/typescript"; import atomOneDark from "svelte-highlight/styles/atom-one-dark"; import DynamicImport from "./DynamicImport.svelte"; + import ScopedStyles from "./ScopedStyles.svelte"; const code = "const add = (a: number, b: number) => a + b;"; @@ -14,3 +15,5 @@ + + diff --git a/examples/vite@svelte-5/src/ScopedStyles.svelte b/examples/vite@svelte-5/src/ScopedStyles.svelte new file mode 100644 index 00000000..7abd7a9c --- /dev/null +++ b/examples/vite@svelte-5/src/ScopedStyles.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/examples/webpack/src/App.svelte b/examples/webpack/src/App.svelte index 745f8bcb..14345983 100644 --- a/examples/webpack/src/App.svelte +++ b/examples/webpack/src/App.svelte @@ -3,6 +3,7 @@ import typescript from "svelte-highlight/languages/typescript"; import atomOneDark from "svelte-highlight/styles/atom-one-dark"; import DynamicImport from "./DynamicImport.svelte"; + import ScopedStyles from "./ScopedStyles.svelte"; const code = "const add = (a: number, b: number) => a + b;"; @@ -14,3 +15,5 @@ + + diff --git a/examples/webpack/src/ScopedStyles.svelte b/examples/webpack/src/ScopedStyles.svelte new file mode 100644 index 00000000..7abd7a9c --- /dev/null +++ b/examples/webpack/src/ScopedStyles.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/scripts/build-styles.ts b/scripts/build-styles.ts index a052f0f9..1c619eb7 100644 --- a/scripts/build-styles.ts +++ b/scripts/build-styles.ts @@ -1,33 +1,59 @@ import { $, Glob } from "bun"; import path from "node:path"; +import postcss from "postcss"; import { createMarkdown } from "./utils/create-markdown"; -import { minifyCss } from "./utils/minify-css"; +import { minifyCss, minifyPreset } from "./utils/minify-css"; +import { pluginScopedStyles } from "./utils/plugin-scoped-styles"; import { toCamelCase } from "./utils/to-pascal-case"; import { writeTo } from "./utils/write-to"; -import postcss from "postcss"; + +const preserveLicenseComments = { + // Preserve license comments. + remove: (comment: string) => !/(License|Author)/i.test(comment), +} as const; const createScopedStyles = (props: { source: string; moduleName: string }) => { const { source, moduleName } = props; return postcss([ - { - postcssPlugin: "postcss-plugin:scoped-styles", - Once(root) { - root.walkRules((rule) => { - rule.selectors = rule.selectors.map((selector) => { - if (/^pre /.test(selector)) { - selector = `pre.${moduleName}${selector.replace(/^pre /, " ")}`; - } else { - selector = `.${moduleName} ${selector}`; - } - return selector; - }); - }); - }, - }, + pluginScopedStyles({ moduleName }), + ...minifyPreset(preserveLicenseComments), ]).process(source).css; }; +const createJsStyles = (props: { moduleName: string; content: string }) => { + const { moduleName, content } = props; + + const css = minifyCss( + // Escape backticks for JS template literal. + content.replace(/\`/g, "\\`"), + preserveLicenseComments, + ); + + return `const ${moduleName} = \`\`;\n + export default ${moduleName};\n`; +}; + +const createScopedJsStyles = (props: { + moduleName: string; + content: string; +}) => { + const { moduleName, content } = props; + + const css = minifyCss( + // Escape backticks for JS template literal. + content.replace(/\`/g, "\\`"), + preserveLicenseComments, + ); + + return ` +const ${moduleName} = Object.freeze({ + moduleName: "${moduleName}", + content: \`\` +});\n +export default ${moduleName};\n`; +}; + export type ModuleNames = Array<{ name: string; moduleName: string }>; export async function buildStyles() { @@ -61,22 +87,10 @@ export async function buildStyles() { const content = await Bun.file(absPath).text(); const css_minified = minifyCss(content); - // Escape backticks for JS template literal. - const content_css_for_js = minifyCss(content.replace(/\`/g, "\\`"), { - remove: (comment) => { - if (/(License|Author)/i.test(comment)) { - // Preserve license comments. - return false; - } - - return true; - }, - }); - - const exportee = `const ${moduleName} = \`\`;\n - export default ${moduleName};\n`; - - await writeTo(`src/styles/${name}.js`, exportee); + await writeTo( + `src/styles/${name}.js`, + createJsStyles({ moduleName, content }), + ); await writeTo( `src/styles/${name}.d.ts`, `export { ${moduleName} as default } from "./";\n`, @@ -85,6 +99,19 @@ export async function buildStyles() { const scoped_style = createScopedStyles({ source: content, moduleName }); + await writeTo( + `src/styles/${name}.scoped.js`, + createScopedJsStyles({ moduleName, content: scoped_style }), + ); + await writeTo( + `src/styles/${name}.scoped.d.ts`, + `declare const ${moduleName}: Readonly<{ + moduleName: "${moduleName}"; + content: \`\`; + }>;\n + export default ${moduleName};\n`, + ); + scoped_styles += scoped_style; } else { // Copy over other file types, like images. @@ -128,6 +155,15 @@ export async function buildStyles() { }) .join(""); + const typesScopedStyles = styles + .map((style) => `| "${style.moduleName}"\n`) + .join(""); + + await writeTo( + "src/scoped-style.d.ts", + `export type ScopedModuleName = ${typesScopedStyles}`, + ); + const types = styles .map((style) => `export declare const ${style.moduleName}: string;\n`) .join(""); @@ -144,6 +180,8 @@ export async function buildStyles() { // Don't format metadata used in docs. await Bun.write("www/data/styles.json", JSON.stringify(styles)); + + // For performance, a dedictated CSS file is used for scoped styles in docs. await Bun.write( "www/data/scoped-styles.css", minifyCss(scoped_styles, { removeAll: true }), diff --git a/scripts/utils/minify-css.ts b/scripts/utils/minify-css.ts index 4b771c06..57456e69 100644 --- a/scripts/utils/minify-css.ts +++ b/scripts/utils/minify-css.ts @@ -2,11 +2,13 @@ import cssnano from "cssnano"; import litePreset, { type LiteOptions } from "cssnano-preset-lite"; import postcss from "postcss"; +export const minifyPreset = ( + discardComments?: LiteOptions["discardComments"], +) => [cssnano({ preset: litePreset({ discardComments }) })]; + export const minifyCss = ( css: string, discardComments?: LiteOptions["discardComments"], ) => { - return postcss([ - cssnano({ preset: litePreset({ discardComments }) }), - ]).process(css).css; + return postcss(minifyPreset(discardComments)).process(css).css; }; diff --git a/scripts/utils/plugin-scoped-styles.ts b/scripts/utils/plugin-scoped-styles.ts new file mode 100644 index 00000000..e4c8e787 --- /dev/null +++ b/scripts/utils/plugin-scoped-styles.ts @@ -0,0 +1,20 @@ +import type { Plugin } from "postcss"; + +export function pluginScopedStyles({ + moduleName, +}: { + moduleName: string; +}): Plugin { + return { + postcssPlugin: "postcss-plugin:scoped-styles", + Once(root) { + root.walkRules((rule) => { + rule.selectors = rule.selectors.map((selector) => { + selector = `.${moduleName} ${selector}`; + + return selector; + }); + }); + }, + }; +} diff --git a/src/ScopedStyle.svelte b/src/ScopedStyle.svelte new file mode 100644 index 00000000..716f7918 --- /dev/null +++ b/src/ScopedStyle.svelte @@ -0,0 +1,16 @@ + + + + {@html content} + + +
+ +
diff --git a/src/ScopedStyle.svelte.d.ts b/src/ScopedStyle.svelte.d.ts new file mode 100644 index 00000000..908d0c50 --- /dev/null +++ b/src/ScopedStyle.svelte.d.ts @@ -0,0 +1,10 @@ +import type { SvelteComponentTyped } from "svelte"; + +export default class ScopedStyle extends SvelteComponentTyped< + { + moduleName: import("./scoped-style.d.ts").ScopedModuleName; + content: ``; + }, + {}, + {} +> {} diff --git a/src/index.d.ts b/src/index.d.ts index 7991367e..610ae71e 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -2,3 +2,4 @@ export { default as Highlight, default as default } from "./Highlight.svelte"; export { default as HighlightAuto } from "./HighlightAuto.svelte"; export { default as HighlightSvelte } from "./HighlightSvelte.svelte"; export { default as LineNumbers } from "./LineNumbers.svelte"; +export { default as ScopedStyle } from "./ScopedStyle.svelte"; diff --git a/src/index.js b/src/index.js index 9b4830f3..a2a586ac 100644 --- a/src/index.js +++ b/src/index.js @@ -2,3 +2,4 @@ export { default as default, default as Highlight } from "./Highlight.svelte"; export { default as HighlightAuto } from "./HighlightAuto.svelte"; export { default as HighlightSvelte } from "./HighlightSvelte.svelte"; export { default as LineNumbers } from "./LineNumbers.svelte"; +export { default as ScopedStyle } from "./ScopedStyle.svelte"; diff --git a/src/scoped-style.d.ts b/src/scoped-style.d.ts new file mode 100644 index 00000000..d65e368d --- /dev/null +++ b/src/scoped-style.d.ts @@ -0,0 +1,250 @@ +export type ScopedModuleName = + | "_1cLight" + | "_3024" + | "a11yDark" + | "a11yLight" + | "agate" + | "anOldHope" + | "androidstudio" + | "apathy" + | "apprentice" + | "arduinoLight" + | "arta" + | "ascetic" + | "ashes" + | "atelierCave" + | "atelierCaveLight" + | "atelierDune" + | "atelierDuneLight" + | "atelierEstuary" + | "atelierEstuaryLight" + | "atelierForest" + | "atelierForestLight" + | "atelierHeath" + | "atelierHeathLight" + | "atelierLakeside" + | "atelierLakesideLight" + | "atelierPlateau" + | "atelierPlateauLight" + | "atelierSavanna" + | "atelierSavannaLight" + | "atelierSeaside" + | "atelierSeasideLight" + | "atelierSulphurpool" + | "atelierSulphurpoolLight" + | "atlas" + | "atomOneDark" + | "atomOneDarkReasonable" + | "atomOneLight" + | "base16Github" + | "base16IrBlack" + | "base16Monokai" + | "base16Nord" + | "bespin" + | "blackMetal" + | "blackMetalBathory" + | "blackMetalBurzum" + | "blackMetalDarkFuneral" + | "blackMetalGorgoroth" + | "blackMetalImmortal" + | "blackMetalKhold" + | "blackMetalMarduk" + | "blackMetalMayhem" + | "blackMetalNile" + | "blackMetalVenom" + | "brewer" + | "bright" + | "brogrammer" + | "brownPaper" + | "brushTrees" + | "brushTreesDark" + | "chalk" + | "circus" + | "classicDark" + | "classicLight" + | "codepenEmbed" + | "codeschool" + | "colorBrewer" + | "colors" + | "cupcake" + | "cupertino" + | "danqing" + | "darcula" + | "dark" + | "darkViolet" + | "darkmoss" + | "darktooth" + | "decaf" + | "_default" + | "defaultDark" + | "defaultLight" + | "devibeans" + | "dirtysea" + | "docco" + | "dracula" + | "edgeDark" + | "edgeLight" + | "eighties" + | "embers" + | "equilibriumDark" + | "equilibriumGrayDark" + | "equilibriumGrayLight" + | "equilibriumLight" + | "espresso" + | "eva" + | "evaDim" + | "far" + | "felipec" + | "flat" + | "foundation" + | "framer" + | "fruitSoda" + | "gigavolt" + | "github" + | "githubDark" + | "githubDarkDimmed" + | "gml" + | "googleDark" + | "googleLight" + | "googlecode" + | "gradientDark" + | "gradientLight" + | "grayscale" + | "grayscaleDark" + | "grayscaleLight" + | "greenScreen" + | "gruvboxDarkHard" + | "gruvboxDarkMedium" + | "gruvboxDarkPale" + | "gruvboxDarkSoft" + | "gruvboxLightHard" + | "gruvboxLightMedium" + | "gruvboxLightSoft" + | "hardcore" + | "harmonic16Dark" + | "harmonic16Light" + | "heetchDark" + | "heetchLight" + | "helios" + | "hopscotch" + | "horizonDark" + | "horizonLight" + | "humanoidDark" + | "humanoidLight" + | "hybrid" + | "iaDark" + | "iaLight" + | "icyDark" + | "idea" + | "intellijLight" + | "irBlack" + | "isblEditorDark" + | "isblEditorLight" + | "isotope" + | "kimber" + | "kimbieDark" + | "kimbieLight" + | "lightfair" + | "lioshi" + | "londonTube" + | "macintosh" + | "magula" + | "marrakesh" + | "materia" + | "material" + | "materialDarker" + | "materialLighter" + | "materialPalenight" + | "materialVivid" + | "mellowPurple" + | "mexicoLight" + | "mocha" + | "monoBlue" + | "monokai" + | "monokaiSublime" + | "nebula" + | "nightOwl" + | "nnfxDark" + | "nnfxLight" + | "nord" + | "nova" + | "obsidian" + | "ocean" + | "oceanicnext" + | "oneLight" + | "onedark" + | "outrunDark" + | "pandaSyntaxDark" + | "pandaSyntaxLight" + | "papercolorDark" + | "papercolorLight" + | "paraiso" + | "paraisoDark" + | "paraisoLight" + | "pasque" + | "phd" + | "pico" + | "pojoaque" + | "pop" + | "porple" + | "purebasic" + | "qtcreatorDark" + | "qtcreatorLight" + | "qualia" + | "railscasts" + | "rainbow" + | "rebecca" + | "rosPine" + | "rosPineDawn" + | "rosPineMoon" + | "routeros" + | "sagelight" + | "sandcastle" + | "schoolBook" + | "setiUi" + | "shadesOfPurple" + | "shapeshifter" + | "silkDark" + | "silkLight" + | "snazzy" + | "solarFlare" + | "solarFlareLight" + | "solarizedDark" + | "solarizedLight" + | "spacemacs" + | "srcery" + | "stackoverflowDark" + | "stackoverflowLight" + | "summercamp" + | "summerfruitDark" + | "summerfruitLight" + | "sunburst" + | "synthMidnightTerminalDark" + | "synthMidnightTerminalLight" + | "tango" + | "tender" + | "tokyoNightDark" + | "tokyoNightLight" + | "tomorrow" + | "tomorrowNight" + | "tomorrowNightBlue" + | "tomorrowNightBright" + | "twilight" + | "unikittyDark" + | "unikittyLight" + | "vs" + | "vs2015" + | "vulcan" + | "windows10" + | "windows10Light" + | "windows95" + | "windows95Light" + | "windowsHighContrast" + | "windowsHighContrastLight" + | "windowsNt" + | "windowsNtLight" + | "woodland" + | "xcode" + | "xcodeDusk" + | "xt256" + | "zenburn"; diff --git a/tests/SvelteHighlightPackage.test.svelte b/tests/SvelteHighlightPackage.test.svelte index 6e58560f..8505f2ff 100644 --- a/tests/SvelteHighlightPackage.test.svelte +++ b/tests/SvelteHighlightPackage.test.svelte @@ -1,7 +1,8 @@