Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ba75574
first commit on sass analyzer
cscheid Jul 29, 2024
9247165
tag dimensions
cscheid Jul 30, 2024
a06d546
break up into small files, add import guard, test on revealjs and boo…
cscheid Jul 31, 2024
8a622e9
partial work, new interactive ui
cscheid Aug 23, 2024
cbd9034
add semicolons to appease a brittle parser we need to use
cscheid Aug 23, 2024
0a3577f
add scss-parser dependency
cscheid Aug 23, 2024
2df8ee1
sass-ui.qmd in passable shape
cscheid Aug 24, 2024
c750a05
merge
cscheid Aug 27, 2024
9da93d4
dependencies
cscheid Aug 27, 2024
03d78d8
add missing files
cscheid Aug 27, 2024
26febea
Merge branch 'main' into feature/sass-analyzer
cscheid Aug 28, 2024
ade2585
Merge branch 'main' into feature/sass-analyzer
cscheid Sep 5, 2024
3c76bf6
types from schema - support additionalProperties
cscheid Sep 5, 2024
61ba11a
use additionalProperties
cscheid Sep 5, 2024
9ca1d8e
build-js artifcats
cscheid Sep 5, 2024
1081ed6
_brand.yml - add support for google font forge in typography
cscheid Sep 5, 2024
97b5660
add string names to font weight schema
cscheid Sep 6, 2024
2059d03
Update src/resources/schema/definitions.yml
cscheid Sep 6, 2024
1607bf9
build artifacts
cscheid Sep 6, 2024
e4d1f79
headings
cscheid Sep 6, 2024
7bf9592
Merge branch 'main' into feature/sass-analyzer
cscheid Sep 6, 2024
423f878
store CSS vars always under quarto-scss-export prefix
cscheid Sep 9, 2024
d5047e5
Merge branch 'main' into feature/sass-analyzer
cscheid Sep 9, 2024
c7e5a4a
Merge branch 'main' into feature/brand-yml-html-fonts
cscheid Sep 9, 2024
18a3e69
brand typography - monospace in html
cscheid Sep 9, 2024
e752dd8
brand,typography - support for fonts.bunny.net foundry
cscheid Sep 9, 2024
d22ecfc
brand, typography - line-height and size for base,headings,monospace
cscheid Sep 9, 2024
0f693f8
scss - support line height for html+reveal
cscheid Sep 10, 2024
050c8c1
Merge branch 'feature/brand-yml-html-fonts' into feature/sass-analyzer
cscheid Sep 10, 2024
b66f421
Merge branch 'main' into feature/sass-analyzer
cscheid Sep 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/command/render/pandoc-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,29 @@ async function resolveQuartoSyntaxHighlighting(
if (themeDescriptor) {
// Other variables that need to be injected (if any)
const extraVariables = extras.html?.[kQuartoCssVariables] || [];
for (let i = 0; i < extraVariables.length; ++i) {
// For the same reason as outlined in https://github.com/rstudio/bslib/issues/1104,
// we need to patch the text to include a semicolon inside the declaration
// if it doesn't have one.
// This happens because scss-parser is brittle, and will fail to parse a declaration
// if it doesn't end with a semicolon.
//
// In addition, we know that some our variables come from the output
// of sassCompile which
// - misses the last semicolon
// - emits a :root declaration
// - triggers the scss-parser bug
// So we'll attempt to target the last declaration in the :root
// block specifically and add a semicolon if it doesn't have one.
let variable = extraVariables[i].trim();
if (
variable.endsWith("}") && variable.startsWith(":root") &&
!variable.match(/.*;\s}$/)
) {
variable = variable.slice(0, -1) + ";}";
extraVariables[i] = variable;
}
}

// The text highlighting CSS variables
const highlightCss = generateThemeCssVars(themeDescriptor.json);
Expand All @@ -308,7 +331,7 @@ async function resolveQuartoSyntaxHighlighting(
// Add this string literal to the rule set, which prevents pandoc
// from inlining this style sheet
// See https://github.com/jgm/pandoc/commit/7c0a80c323f81e6262848bfcfc922301e3f406e0
rules.push(".prevent-inlining { content: '</' }");
rules.push(".prevent-inlining { content: '</'; }");

// Compile the scss
const highlightCssPath = await compileSass(
Expand Down
6 changes: 5 additions & 1 deletion src/core/brand/brand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,12 @@ export class Brand {
}
defs.push(value2);
} else {
console.log(
"warning: google font forge not supported for non-html formats",
);
console.assert("google" in value);
console.log("warning: google font forge not supported yet");
const ret = mergeConfigs({ family: name }, ...defs.reverse());
return ret;
// download to (temporary?) directory and populate .files
}
} else if (defaultFontNames.includes(name)) {
Expand Down
57 changes: 47 additions & 10 deletions src/core/sass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { dartCompile } from "./dart-sass.ts";
import * as ld from "./lodash.ts";
import { lines } from "./text.ts";
import { sassCache } from "./sass/cache.ts";
import { cssVarsBlock } from "./sass/add-css-vars.ts";
import { md5HashBytes } from "./hash.ts";
import { kSourceMappingRegexes } from "../config/constants.ts";
import { writeTextFileSyncPreserveMode } from "./write.ts";
Expand Down Expand Up @@ -44,6 +45,7 @@ export function outputVariable(
return `$${variable.name}: ${variable.value}${isDefault ? " !default" : ""};`;
}

let counter: number = 1;
export async function compileSass(
bundles: SassBundleLayers[],
temp: TempContext,
Expand Down Expand Up @@ -104,39 +106,74 @@ export async function compileSass(
// * Mixins are available to rules as well
// * Rules may use functions, variables, and mixins
// (theme follows framework so it can override the framework rules)
const scssInput = [
let scssInput = [
'// quarto-scss-analysis-annotation { "origin": null }',
...frameWorkUses,
'// quarto-scss-analysis-annotation { "origin": null }',
...quartoUses,
'// quarto-scss-analysis-annotation { "origin": null }',
...userUses,
'// quarto-scss-analysis-annotation { "origin": null }',
...frameworkFunctions,
'// quarto-scss-analysis-annotation { "origin": null }',
...quartoFunctions,
'// quarto-scss-analysis-annotation { "origin": null }',
...userFunctions,
'// quarto-scss-analysis-annotation { "origin": null }',
...userDefaults.reverse(),
'// quarto-scss-analysis-annotation { "origin": null }',
...quartoDefaults.reverse(),
'// quarto-scss-analysis-annotation { "origin": null }',
...frameworkDefaults.reverse(),
'// quarto-scss-analysis-annotation { "origin": null }',
...frameworkMixins,
'// quarto-scss-analysis-annotation { "origin": null }',
...quartoMixins,
'// quarto-scss-analysis-annotation { "origin": null }',
...userMixins,
'// quarto-scss-analysis-annotation { "origin": null }',
...frameworkRules,
'// quarto-scss-analysis-annotation { "origin": null }',
...quartoRules,
'// quarto-scss-analysis-annotation { "origin": null }',
...userRules,
'// quarto-scss-analysis-annotation { "origin": null }',
].join("\n\n");

const hash = md5HashBytes(new TextEncoder().encode(scssInput));
scssInput += "\n" + cssVarsBlock(scssInput);

// Compile the scss
// Note that you can set this to undefined to bypass the cache entirely
const cacheKey = hash;
// bundles.map((bundle) => bundle.key).join("|") + "-" +
// (minified ? "min" : "nomin");

return await compileWithCache(
const result = await compileWithCache(
scssInput,
loadPaths,
temp,
minified,
cacheKey,
md5HashBytes(new TextEncoder().encode(scssInput)),
);
if (!Deno.env.get("QUARTO_SAVE_SCSS")) {
return result;
}
const partialOutput = Deno.readTextFileSync(result);
// now we attempt to find the SCSS variables in the output
// and inject them back in the SCSS file so that our debug tooling can use them.
const scssToWrite = [scssInput];
const internalVars = Array.from(
partialOutput.matchAll(/(--quarto-scss-export-[^;}]+;?)/g),
).map((m) => m[0]);
const annotation = {
"css-vars": internalVars,
};
scssToWrite.push(
`// quarto-scss-analysis-annotation ${JSON.stringify(annotation)}`,
);
scssInput = scssToWrite.join("\n");
const prefix = Deno.env.get("QUARTO_SAVE_SCSS");
const counterValue = counter++;
Deno.writeTextFileSync(
`${prefix}-${counterValue}.scss`,
scssInput,
);

return result;
}

/*-- scss:uses --*/
Expand Down
31 changes: 31 additions & 0 deletions src/core/sass/add-css-vars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* add-css-vars.ts
*
* Analyzes SCSS files for CSS variables and adds them as additional CSS variables
* for our tooling to use.
*
* Copyright (C) 2024 Posit Software, PBC
*/

import { parse } from "scss-parser";

import { makeParserModule } from "./analyzer/parse.ts";
import { cleanSassAst } from "./analyzer/clean-ast.ts";
import { propagateDeclarationTypes } from "./analyzer/declaration-types.ts";
import { getVariableDependencies } from "./analyzer/get-dependencies.ts";

const { getSassAst } = makeParserModule(parse);
export const cssVarsBlock = (scssSource: string) => {
const ast = propagateDeclarationTypes(cleanSassAst(getSassAst(scssSource)));
const deps = getVariableDependencies(ast);

const output: string[] = [":root {"];
for (const [dep, _] of deps) {
const decl = ast.get(dep);
if (decl.valueType === "color") {
output.push(`--quarto-scss-export-${dep}: #{$${dep}};`);
}
}
output.push("}");
return output.join("\n");
};
1 change: 1 addition & 0 deletions src/core/sass/analyzer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.js
69 changes: 69 additions & 0 deletions src/core/sass/analyzer/ast-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export const walk = (node: any, cb: (node: any) => unknown) => {
if (!node || typeof node !== "object") return;
if (!cb(node)) {
return;
};
for (const key of Object.keys(node)) {
walk(node[key], cb);
}
}

export const withType = (node: any, func: (ast: any) => any) => {
if (!node?.type) {
return node;
}
return func(node);
}

export const withTypeAndArray = (node: any, func: (ast: any) => any) => {
if (!node?.type) {
return node;
}
if (!node?.children || !Array.isArray(node.children)) {
return node;
}
return func(node);
}

export const filterDeep = (outer: any, cb: (v: any) => boolean): any =>
withType(outer, (ast: any) => {
return Object.fromEntries(Object.entries(ast).map(([k, v]) => {
if (Array.isArray(v)) {
return [k, v.filter(cb).map((v: any) => filterDeep(v, cb))];
} else if (v && typeof v === "object") {
return [k, filterDeep(v, cb)];
} else {
return [k, v];
}
}));
});

export const mapDeep = (outer: any, cb: (mapped: any) => any): any =>
withType(outer, (ast: any) => {
if (Array.isArray(ast.children)) {
ast.children = ast.children.map((v: any) => mapDeep(v, cb));
}
if (Array.isArray(ast.value)) {
ast.value = ast.value.map((v: any) => mapDeep(v, cb));
}
return cb(ast);
});

export const collect = (outer: any, cb: (v: any) => boolean): any[] => {
const results: any = [];
walk(outer, (node: any) => {
if (cb(node)) {
results.push(node);
}
return true;
});
return results;
}

export const annotateNode = (node: any, annotation: Record<string, unknown>) => {
if (!node.annotation) {
node.annotation = {};
}
Object.assign(node.annotation, annotation);
return node;
}
56 changes: 56 additions & 0 deletions src/core/sass/analyzer/clean-ast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { withTypeAndArray, filterDeep, mapDeep } from "./ast-utils.ts";
import { isRealContent, isNotPunctuation } from "./remove-nonsemantic-entries.ts";
import { simplifyLineInfo } from "./line-info.ts";
import { explicitlyTagDefaultValues } from "./default-declarations.ts";
import { fixImmediateTypes, tagNamedColors, tagColorConstructors, findDimensionValues } from "./value-types.ts";
import { groupArguments } from "./group-arguments.ts";
import { forwardAnnotations } from "./forward-annotations.ts";

const valueArrayToObjectKeys = (outer: any) =>
withTypeAndArray(outer, (node: any) => {
const keys = node.children.map((v: any) => v.type);
if (keys.length !== new Set(keys).size) {
return node;
}
return {
...node,
...Object.fromEntries(node.children
.map((v: any) => {
const key = v.type;
let children = v.children;
return [key, {...v, children}];
})),
};
});

export const cleanSassAst = (ast: any) => {
// we now attempt to turn this glorified lexer into a real AST

// before everything else, we associate declarations with the
// annotations that tell us which part of the theming system
// they belong to
ast = forwardAnnotations(ast);

// before clearing out the punctuation, group arguments
ast = filterDeep(ast, isRealContent);
ast = mapDeep(ast, groupArguments);

// clear out all the gunk from the AST
ast = filterDeep(ast, isNotPunctuation);

ast = mapDeep(ast, simplifyLineInfo);
ast = mapDeep(ast, explicitlyTagDefaultValues);
ast = mapDeep(ast, findDimensionValues);
ast = mapDeep(ast, fixImmediateTypes);
ast = mapDeep(ast, tagColorConstructors);
ast = mapDeep(ast, tagNamedColors);

// if the value array looks like an array of keys and values,
// insert those values into the parent object
// additionally, in these properties, if the value is an array
// with a single element, remove the array and just use the
// element.
ast = mapDeep(ast, valueArrayToObjectKeys);

return ast;
}
Loading
Loading