Skip to content

Commit

Permalink
Merge pull request #6034 from telamonian/style-svg-with-css
Browse files Browse the repository at this point in the history
Expose icon SVG to theme CSS
  • Loading branch information
blink1073 committed Aug 14, 2019
2 parents 704fb0d + 48d6cec commit 083c65d
Show file tree
Hide file tree
Showing 198 changed files with 2,287 additions and 2,962 deletions.
2 changes: 2 additions & 0 deletions buildutils/package.json
Expand Up @@ -49,6 +49,7 @@
"mini-css-extract-plugin": "~0.6.0",
"package-json": "^6.3.0",
"path": "~0.12.7",
"prettier": "^1.18.2",
"semver": "^6.1.0",
"sort-package-json": "~1.22.1",
"typescript": "~3.5.1",
Expand All @@ -60,6 +61,7 @@
"@types/inquirer": "^6.0.3",
"@types/mini-css-extract-plugin": "^0.2.0",
"@types/node": "^12.0.2",
"@types/prettier": "^1.16.4",
"@types/webpack": "^4.4.32",
"rimraf": "~2.6.2"
}
Expand Down
5 changes: 1 addition & 4 deletions buildutils/src/build.ts
Expand Up @@ -183,10 +183,7 @@ export namespace Build {
},
{
test: /\.svg/,
use: [
{ loader: 'svg-url-loader', options: {} },
{ loader: 'svgo-loader', options: { plugins: [] } }
]
use: [{ loader: 'svg-url-loader', options: { encoding: 'none' } }]
},
{
test: /\.(png|jpg|gif|ttf|woff|woff2|eot)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
Expand Down
175 changes: 164 additions & 11 deletions buildutils/src/ensure-package.ts
Expand Up @@ -6,17 +6,48 @@
import * as fs from 'fs-extra';
import * as glob from 'glob';
import * as path from 'path';
import * as prettier from 'prettier';
import * as ts from 'typescript';
import { getDependency } from './get-dependency';
import * as utils from './utils';

const CSS_HEADER = `
const HEADER_TEMPLATE = `
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */
/* This file was auto-generated by {{funcName}}() in @jupyterlab/buildutils */
`;

const ICON_IMPORTS_TEMPLATE = `
import { Icon } from './interfaces';
// icon svg import statements
{{iconImportStatements}}
// defaultIcons definition
export namespace IconImports {
export const defaultIcons: ReadonlyArray<Icon.IModel> = [
{{iconModelDeclarations}}
];
}
`;

const ICON_CSS_CLASSES_TEMPLATE = `
/**
* (DEPRECATED) Support for consuming icons as CSS background images
*/
/* Icons urls */
:root {
{{iconCSSUrls}}
}
/* Icon CSS class declarations */
{{iconCSSDeclarations}}
`;

/**
Expand Down Expand Up @@ -138,17 +169,20 @@ export async function ensurePackage(

// Template the CSS index file.
if (cssImports && fs.existsSync(path.join(pkgPath, 'style/base.css'))) {
let cssIndex = CSS_HEADER.trim();
const funcName = 'ensurePackage';
let cssIndexContents = utils.fromTemplate(
HEADER_TEMPLATE,
{ funcName },
{ end: '' }
);
cssImports.forEach(cssImport => {
cssIndex += `\n@import url('~${cssImport}');`;
cssIndexContents += `\n@import url('~${cssImport}');`;
});
cssIndex += "\n\n@import url('./base.css');\n";
const cssPath = path.join(pkgPath, 'style/index.css');
const prev = fs.readFileSync(cssPath, { encoding: 'utf8' });
if (prev !== cssIndex) {
messages.push(`Updated ./${data.style}`);
fs.writeFileSync(cssPath, cssIndex);
}
cssIndexContents += "\n\n@import url('./base.css');\n";

// write out cssIndexContents, if needed
const cssIndexPath = path.join(pkgPath, 'style/index.css');
messages.push(...ensureFile(cssIndexPath, cssIndexContents, false));
}

// Look for unused packages
Expand Down Expand Up @@ -290,6 +324,78 @@ export async function ensurePackage(
return messages;
}

/**
* An extra ensure function just for the @jupyterlab/ui-components package.
* Ensures that the icon svg import statements are synced with the contents
* of ui-components/style/icons.
*
* @param pkgPath - The path to the @jupyterlab/ui-components package.
*
* @returns A list of changes that were made to ensure the package.
*/
export async function ensureUiComponents(pkgPath: string): Promise<string[]> {
const funcName = 'ensureUiComponents';
let messages: string[] = [];

const svgs = glob.sync(path.join(pkgPath, 'style/icons', '**/*.svg'));

/* support for glob import of icon svgs */
const iconSrcDir = path.join(pkgPath, 'src/icon');

// build the per-icon import code
let _iconImportStatements: string[] = [];
let _iconModelDeclarations: string[] = [];
svgs.forEach(svg => {
const name = utils.stem(svg);
const nameCamel = utils.camelCase(name) + 'Svg';
_iconImportStatements.push(
`import ${nameCamel} from '${path.relative(iconSrcDir, svg)}';`
);
_iconModelDeclarations.push(`{ name: '${name}', svg: ${nameCamel} }`);
});
const iconImportStatements = _iconImportStatements.join('\n');
const iconModelDeclarations = _iconModelDeclarations.join(',\n');

// generate the actual contents of the iconImports file
const iconImportsPath = path.join(iconSrcDir, 'iconImports.ts');
const iconImportsContents = utils.fromTemplate(
HEADER_TEMPLATE + ICON_IMPORTS_TEMPLATE,
{ funcName, iconImportStatements, iconModelDeclarations }
);
messages.push(...ensureFile(iconImportsPath, iconImportsContents));

/* support for deprecated icon CSS classes */
const iconCSSDir = path.join(pkgPath, 'style');

// build the per-icon import code
let _iconCSSUrls: string[] = [];
let _iconCSSDeclarations: string[] = [];
svgs.forEach(svg => {
const name = utils.stem(svg);
const urlName = 'jp-icon-' + name;
const className = 'jp-' + utils.camelCase(name, true) + 'Icon';

_iconCSSUrls.push(
`--${urlName}: url('${path.relative(iconCSSDir, svg)}');`
);
_iconCSSDeclarations.push(
`.${className} {background-image: var(--${urlName})}`
);
});
const iconCSSUrls = _iconCSSUrls.join('\n');
const iconCSSDeclarations = _iconCSSDeclarations.join('\n');

// generate the actual contents of the iconCSSClasses file
const iconCSSClassesPath = path.join(iconCSSDir, 'deprecated.css');
const iconCSSClassesContent = utils.fromTemplate(
HEADER_TEMPLATE + ICON_CSS_CLASSES_TEMPLATE,
{ funcName, iconCSSUrls, iconCSSDeclarations }
);
messages.push(...ensureFile(iconCSSClassesPath, iconCSSClassesContent));

return messages;
}

/**
* The options used to ensure a package.
*/
Expand Down Expand Up @@ -340,6 +446,53 @@ export interface IEnsurePackageOptions {
differentVersions?: string[];
}

/**
* Ensure that contents of a file match a supplied string. If they do match,
* do nothing and return an empty array. If they don't match, overwrite the
* file and return an array with an update message.
*
* @param path: The path to the file being checked. The file must exist,
* or else this function does nothing.
*
* @param contents: The desired file contents.
*
* @param prettify: default = true. If true, format the contents with
* `prettier` before comparing/writing. Set to false only if you already
* know your code won't be modified later by the `prettier` git commit hook.
*
* @returns a string array with 0 or 1 messages.
*/
function ensureFile(
path: string,
contents: string,
prettify: boolean = true
): string[] {
let messages: string[] = [];

if (!fs.existsSync(path)) {
// bail
messages.push(
`Tried to ensure the contents of ./${path}, but the file does not exist`
);
return messages;
}

// run the newly generated contents through prettier before comparing
if (prettify) {
contents = prettier.format(contents, { filepath: path, singleQuote: true });
}

const prev = fs.readFileSync(path, {
encoding: 'utf8'
});
if (prev !== contents) {
fs.writeFileSync(path, contents);
messages.push(`Updated ./${path}`);
}

return messages;
}

/**
* Extract the module imports from a TypeScript source file.
*
Expand Down
16 changes: 15 additions & 1 deletion buildutils/src/ensure-repo.ts
Expand Up @@ -13,7 +13,11 @@
*/
import * as path from 'path';
import * as utils from './utils';
import { ensurePackage, IEnsurePackageOptions } from './ensure-package';
import {
ensurePackage,
ensureUiComponents,
IEnsurePackageOptions
} from './ensure-package';

type Dict<T> = { [key: string]: T };

Expand Down Expand Up @@ -341,6 +345,16 @@ export async function ensureIntegrity(): Promise<boolean> {
}
}

// ensure the icon svg imports
pkgMessages = await ensureUiComponents(pkgPaths['@jupyterlab/ui-components']);
if (pkgMessages.length > 0) {
let pkgName = '@jupyterlab/ui-components';
if (!messages[pkgName]) {
messages[pkgName] = [];
}
messages[pkgName] = messages[pkgName].concat(pkgMessages);
}

// Handle the top level package.
let corePath = path.resolve('.', 'package.json');
let coreData: any = utils.readJSONFile(corePath);
Expand Down
88 changes: 88 additions & 0 deletions buildutils/src/utils.ts
Expand Up @@ -114,6 +114,54 @@ export function writeJSONFile(filePath: string, data: any): boolean {
return false;
}

/**
* Simple template substitution for template vars of the form {{name}}
*
* @param templ: the template string.
* Ex: `This header generated by {{funcName}}`
*
* @param subs: an object in which the parameter keys are the template
* variables and the parameter values are the substitutions.
*
* @param options: function options.
*
* @param options.autoindent: default = true. If true, will try to match
* indentation level of {{var}} in substituted template.
*
* @param options.end: default = '\n'. Inserted at the end of
* a template post-substitution and post-trim.
*
* @returns the input template with all {{vars}} substituted, then `.trim`-ed.
*/
export function fromTemplate(
templ: string,
subs: Dict<string>,
options: { autoindent?: boolean; end?: string } = {}
) {
// default options values
const autoindent =
options.autoindent === undefined ? true : options.autoindent;
const end = options.end === undefined ? '\n' : options.end;

Object.keys(subs).forEach(key => {
const val = subs[key];

if (autoindent) {
// try to match the indentation level of the {{var}} in the input template.
templ = templ.split(`{{${key}}}`).reduce((acc, cur) => {
// Regex: 0 or more non-newline whitespaces followed by end of string
let indentRe = acc.match(/([^\S\r\n]*).*$/);
let indent = indentRe ? indentRe[1] : '';
return acc + val.split('\n').join('\n' + indent) + cur;
});
} else {
templ = templ.split(`{{${key}}}`).join(val);
}
});

return templ.trim() + end;
}

/**
*
* Call a command, checking its status.
Expand Down Expand Up @@ -286,3 +334,43 @@ export function ensureUnixPathSep(source: string) {
}
return source.replace(backSlash, '/');
}

/**
* Get the last portion of a path, without its extension (if any).
*
* @param path - The file path.
*
* @returns the last part of the path, sans extension.
*/
export function stem(path: string): string {
return path
.split('\\')
.pop()
.split('/')
.pop()
.split('.')
.shift();
}

/**
* Given a 'snake-case', 'snake_case', or 'snake case' string,
* will return the camel case version: 'snakeCase'.
*
* @param str: the snake-case input string.
*
* @param upper: default = false. If true, the first letter of the
* returned string will be capitalized.
*
* @returns the camel case version of the input string.
*/
export function camelCase(str: string, upper: boolean = false): string {
return str.replace(/(?:^\w|[A-Z]|\b\w|\s+|-+|_+)/g, function(match, index) {
if (+match === 0 || match[0] === '-') {
return '';
} else if (index === 0 && !upper) {
return match.toLowerCase();
} else {
return match.toUpperCase();
}
});
}
1 change: 1 addition & 0 deletions dev_mode/imports.css
Expand Up @@ -31,6 +31,7 @@
@import url('~@jupyterlab/tabmanager-extension/style/index.css');
@import url('~@jupyterlab/terminal-extension/style/index.css');
@import url('~@jupyterlab/tooltip-extension/style/index.css');
@import url('~@jupyterlab/ui-components-extension/style/index.css');
@import url('~@jupyterlab/vdom-extension/style/index.css');
@import url('~@jupyterlab/vega4-extension/style/index.css');
@import url('~@jupyterlab/vega5-extension/style/index.css');

0 comments on commit 083c65d

Please sign in to comment.