From ca9be698fa4f7b17071e6fb4e5fbf5c435b38a78 Mon Sep 17 00:00:00 2001 From: harttle Date: Thu, 16 Jul 2020 16:36:52 +0800 Subject: [PATCH] fix: trimWhitespace and delimiters, see https://baidu.github.io/san/doc/api/#trimWhitespace --- bin/debug | 19 +++++++++++- src/fixtures/case.ts | 25 ++++++++++++--- src/parsers/component-class-parser.ts | 4 ++- src/parsers/parse-template.ts | 6 ++-- src/parsers/typescript-san-parser.ts | 10 ++++-- src/target-js/compilers/expr-compiler.ts | 3 +- src/utils/ast-util.ts | 25 ++++++++++++--- src/utils/lang.ts | 4 ++- test/cases/delimiters/component.js | 7 +++++ test/cases/delimiters/component.ts | 6 ++++ test/cases/delimiters/data.json | 3 ++ test/cases/delimiters/expected.html | 1 + test/cases/trim-whitespaces/component.js | 11 +++++++ test/cases/trim-whitespaces/component.ts | 9 ++++++ test/cases/trim-whitespaces/data.json | 1 + test/cases/trim-whitespaces/expected.html | 1 + test/integration.spec.ts | 20 +++++++++--- test/unit/utils/ast-util.spec.ts | 38 ++++++++++++++++++++--- 18 files changed, 165 insertions(+), 28 deletions(-) create mode 100644 test/cases/delimiters/component.js create mode 100644 test/cases/delimiters/component.ts create mode 100644 test/cases/delimiters/data.json create mode 100644 test/cases/delimiters/expected.html create mode 100644 test/cases/trim-whitespaces/component.js create mode 100644 test/cases/trim-whitespaces/component.ts create mode 100644 test/cases/trim-whitespaces/data.json create mode 100644 test/cases/trim-whitespaces/expected.html diff --git a/bin/debug b/bin/debug index 562d38d4..00dbaba7 100755 --- a/bin/debug +++ b/bin/debug @@ -7,7 +7,7 @@ const chalk = require('chalk') const { readFileSync, readdirSync, writeFileSync } = require('fs') const { resolve } = require('path') const root = resolve(__dirname, '../test/cases') -const { compile } = require('../dist/fixtures/case') +const { compile, compileTS } = require('../dist/fixtures/case') const caseName = process.argv[2] if (!caseName) { @@ -24,6 +24,7 @@ const expected = readFileSync(htmlPath, 'utf8') console.log(chalk.cyan(`[EXPECT] ${caseName}`), expected) if (!caseName.match(/-nsrc$/)) debugCompileToSource() +if (!caseName.match(/-nsrc$/)) debugCompileToTSSource() debugCompileToRenderer() function debugCompileToSource () { @@ -42,6 +43,22 @@ function debugCompileToSource () { } } +function debugCompileToTSSource () { + let got + try { + const targetCode = compileTS(caseName) + const fileName = `${__dirname}/../test/cases/${caseName}/ssr.js` + writeFileSync(fileName, targetCode) + + got = execFileSync(resolve(__dirname, `./render-by-source.js`), [caseName], { encoding: 'utf8' }).toString() + assertSanHTMLEqual(got, expected) + console.log(chalk.green(`[SRC TS] ${caseName}`), got) + } catch (err) { + console.log(chalk.red(`[SRC TS] ${caseName}`), got) + console.error(err) + } +} + function debugCompileToRenderer () { let got try { diff --git a/src/fixtures/case.ts b/src/fixtures/case.ts index e43729a7..69dfe569 100644 --- a/src/fixtures/case.ts +++ b/src/fixtures/case.ts @@ -6,10 +6,14 @@ import debugFactory from 'debug' import { compileToRenderer } from '../index' const debug = debugFactory('case') -const caseRoot = resolve(__dirname, '../../test/cases') +export const caseRoot = resolve(__dirname, '../../test/cases') const tsConfigFilePath = resolve(__dirname, '../../test/tsconfig.json') const sanProject = new SanProject(tsConfigFilePath) +export function tsExists (caseName: string) { + return existsSync(join(caseRoot, caseName, 'component.ts')) +} + export function ls () { return readdirSync(caseRoot) .filter(caseName => lstatSync(resolve(caseRoot, caseName)).isDirectory()) @@ -21,13 +25,26 @@ export function readExpected (caseName: string) { } export function compile (caseName: string, bareFunctionBody: boolean) { - debug('compile', caseName) + debug('compile js', caseName) const caseDir = join(caseRoot, caseName) - const tsFile = join(caseDir, 'component.ts') + // const tsFile = join(caseDir, 'component.ts') const jsFile = resolve(caseDir, 'component.js') const ssrOnly = /-so/.test(caseName) const targetCode = sanProject.compile( - existsSync(tsFile) ? tsFile : jsFile, + jsFile, + ToJSCompiler, + { ssrOnly, bareFunctionBody } + ) + return targetCode +} + +export function compileTS (caseName: string, bareFunctionBody: boolean) { + debug('compile ts', caseName) + const caseDir = join(caseRoot, caseName) + const tsFile = join(caseDir, 'component.ts') + const ssrOnly = /-so/.test(caseName) + const targetCode = sanProject.compile( + tsFile, ToJSCompiler, { ssrOnly, bareFunctionBody } ) diff --git a/src/parsers/component-class-parser.ts b/src/parsers/component-class-parser.ts index fbe2ba57..8e1b998b 100644 --- a/src/parsers/component-class-parser.ts +++ b/src/parsers/component-class-parser.ts @@ -49,7 +49,9 @@ export class ComponentClassParser { if (!componentClass) componentClass = defineComponent({ template: '' }) const template = getMember(componentClass, 'template', '') - const rootANode = parseAndNormalizeTemplate(template) + const trimWhitespace = getMember<'none' | 'blank' | 'all'>(componentClass, 'trimWhitespace') + const delimiters = getMember<[string, string]>(componentClass, 'delimiters') + const rootANode = parseAndNormalizeTemplate(template, { trimWhitespace, delimiters }) const childComponents = this.getChildComponentClasses(componentClass, rootANode) return new DynamicComponentInfo(id, template, rootANode, childComponents, componentClass) diff --git a/src/parsers/parse-template.ts b/src/parsers/parse-template.ts index 67ae08bb..34536620 100644 --- a/src/parsers/parse-template.ts +++ b/src/parsers/parse-template.ts @@ -1,9 +1,9 @@ -import { ANodeProperty, ExprInterpNode, parseTemplate, ANode } from 'san' +import { ParseTemplateOption, ANodeProperty, ExprInterpNode, parseTemplate, ANode } from 'san' import * as TypeGuards from '../utils/type-guards' import { parseANodeProps, visitANodeRecursively } from '../utils/anode-util' -export function parseAndNormalizeTemplate (template: string) { - const rootANode = parseTemplate(template).children![0] +export function parseAndNormalizeTemplate (template: string, options: ParseTemplateOption) { + const rootANode = parseTemplate(template, options).children![0] rootANode && normalizeRootANode(rootANode) return rootANode } diff --git a/src/parsers/typescript-san-parser.ts b/src/parsers/typescript-san-parser.ts index a743e396..9850ea59 100644 --- a/src/parsers/typescript-san-parser.ts +++ b/src/parsers/typescript-san-parser.ts @@ -1,6 +1,6 @@ import { SourceFile, ClassDeclaration } from 'ts-morph' import debugFactory from 'debug' -import { getChildComponents, getComponentDeclarations, getPropertyStringValue } from '../utils/ast-util' +import { getChildComponents, getPropertyStringArrayValue, getComponentDeclarations, getPropertyStringValue } from '../utils/ast-util' import { normalizeComponentClass } from './normalize-component' import { TypedSanSourceFile } from '../models/san-source-file' import { parseAndNormalizeTemplate } from './parse-template' @@ -25,12 +25,16 @@ export class TypeScriptSanParser { } private parseComponentClassDeclaration (classDeclaration: ClassDeclaration, defaultClassDeclaration?: ClassDeclaration): TypedComponentInfo { - const template = getPropertyStringValue(classDeclaration, 'template') + const template = getPropertyStringValue(classDeclaration, 'template', '') + const trimWhitespace = getPropertyStringValue<'none' | 'blank' | 'all'>(classDeclaration, 'trimWhitespace') + const delimiters = getPropertyStringArrayValue<[string, string]>(classDeclaration, 'delimiters') const childComponents = getChildComponents(classDeclaration, defaultClassDeclaration) return new TypedComponentInfo( classDeclaration.isDefaultExport() ? getDefaultExportedComponentID() : getExportedComponentID(classDeclaration.getName()!), template, - parseAndNormalizeTemplate(template), + parseAndNormalizeTemplate(template, { + trimWhitespace, delimiters + }), childComponents, classDeclaration ) diff --git a/src/target-js/compilers/expr-compiler.ts b/src/target-js/compilers/expr-compiler.ts index 04ccfb38..2dae8bca 100644 --- a/src/target-js/compilers/expr-compiler.ts +++ b/src/target-js/compilers/expr-compiler.ts @@ -5,6 +5,7 @@ import { ExprStringNode, ExprNode, ExprTertiaryNode, ExprBinaryNode, ExprUnaryNo import { isValidIdentifier } from '../../utils/lang' import * as TypeGuards from '../../utils/type-guards' import { _ } from '../../runtime/underscore' +import { stringifier } from './stringifier' // 二元表达式操作符映射表 const binaryOp = { @@ -100,7 +101,7 @@ function interp (interpExpr: ExprInterpNode): string { } function str (e: ExprStringNode): string { - return '"' + _.escapeHTML(e.value) + '"' + return stringifier.str(_.escapeHTML(e.value)) } // 生成文本片段代码 diff --git a/src/utils/ast-util.ts b/src/utils/ast-util.ts index f967327b..266d960c 100644 --- a/src/utils/ast-util.ts +++ b/src/utils/ast-util.ts @@ -42,16 +42,18 @@ export function getComponentDeclarations (sourceFile: SourceFile) { return sourceFile.getClasses().filter(clazz => isChildClassOf(clazz, componentClassIdentifier)) } -export function getPropertyStringValue (clazz: ClassDeclaration, memberName: string) { +export function getPropertyStringValue (clazz: ClassDeclaration, memberName: string, defaultValue: T): T; +export function getPropertyStringValue (clazz: ClassDeclaration, memberName: string): T | undefined; +export function getPropertyStringValue (clazz: ClassDeclaration, memberName: string, defaultValue?: T): T | undefined { const member = clazz.getProperty(memberName) - if (!member) return '' + if (!member) return defaultValue const init = member.getInitializer() - if (!init) return '' + if (!init) return defaultValue // 字符串常量,取其字面值 const value = getLiteralText(init) - if (value !== undefined) return value + if (value !== undefined) return value as T // 变量,找到定义处,取其字面值(非字面量跑错) if (TypeGuards.isIdentifier(init)) { @@ -64,11 +66,24 @@ export function getPropertyStringValue (clazz: ClassDeclaration, memberName: str if (str === undefined) { throw new Error(`${JSON.stringify(value.getText())} not supported, specify a string literal for "${memberName}"`) } - return str + return str as T } throw new Error(`invalid "${memberName}" property`) } +export function getPropertyStringArrayValue (clazz: ClassDeclaration, memberName: string): T | undefined { + const member = clazz.getProperty(memberName) + if (!member) return undefined + + const init = member.getInitializer() + if (!init) return undefined + + if (!TypeGuards.isArrayLiteralExpression(init)) { + throw new Error(`invalid "${memberName}": "${init.getText()}", array literal expected`) + } + return init.getElements().map(element => getLiteralText(element)) as T +} + function getLiteralText (expr: Node) { if (TypeGuards.isStringLiteral(expr) || TypeGuards.isNoSubstitutionTemplateLiteral(expr)) { return expr.getLiteralValue() diff --git a/src/utils/lang.ts b/src/utils/lang.ts index b6790744..ae5f5ef8 100644 --- a/src/utils/lang.ts +++ b/src/utils/lang.ts @@ -2,7 +2,9 @@ export function isValidIdentifier (str: string) { return !!/^[a-zA-Z_$][\w$]*$/.exec(str) } -export function getMember (clazz: Function, property: string, defaultValue: T): T { +export function getMember (clazz: Function, property: string, defaultValue: T): T +export function getMember (clazz: Function, property: string): T | undefined +export function getMember (clazz: Function, property: string, defaultValue?: T): T | undefined { if (clazz[property]) return clazz[property] if (clazz.prototype && clazz.prototype[property]) { return clazz.prototype[property] diff --git a/test/cases/delimiters/component.js b/test/cases/delimiters/component.js new file mode 100644 index 00000000..b5081d27 --- /dev/null +++ b/test/cases/delimiters/component.js @@ -0,0 +1,7 @@ +const { Component } = require('san') + +class MyComponent extends Component {} +MyComponent.delimiters = ['[[', ']]'] +MyComponent.template = `
[[ title ]]
` + +module.exports = exports = MyComponent diff --git a/test/cases/delimiters/component.ts b/test/cases/delimiters/component.ts new file mode 100644 index 00000000..7db2a750 --- /dev/null +++ b/test/cases/delimiters/component.ts @@ -0,0 +1,6 @@ +import { Component } from 'san' + +export default class MyComponent extends Component { + static template = `
[[ title ]]
` + delimiters = ['[[', ']]'] +} diff --git a/test/cases/delimiters/data.json b/test/cases/delimiters/data.json new file mode 100644 index 00000000..e6fa479d --- /dev/null +++ b/test/cases/delimiters/data.json @@ -0,0 +1,3 @@ +{ + "title": "San" +} \ No newline at end of file diff --git a/test/cases/delimiters/expected.html b/test/cases/delimiters/expected.html new file mode 100644 index 00000000..8300056a --- /dev/null +++ b/test/cases/delimiters/expected.html @@ -0,0 +1 @@ +
San
\ No newline at end of file diff --git a/test/cases/trim-whitespaces/component.js b/test/cases/trim-whitespaces/component.js new file mode 100644 index 00000000..3c78b348 --- /dev/null +++ b/test/cases/trim-whitespaces/component.js @@ -0,0 +1,11 @@ +const { Component } = require('san') + +class MyComponent extends Component {} +MyComponent.trimWhitespace = 'blank' +MyComponent.template = ` +
+ Foo bar +
+` + +module.exports = exports = MyComponent diff --git a/test/cases/trim-whitespaces/component.ts b/test/cases/trim-whitespaces/component.ts new file mode 100644 index 00000000..4e94873f --- /dev/null +++ b/test/cases/trim-whitespaces/component.ts @@ -0,0 +1,9 @@ +import { Component } from 'san' + +export default class MyComponent extends Component { + static template = ` +
+ Foo bar +
` + trimWhitespace = 'blank' +} diff --git a/test/cases/trim-whitespaces/data.json b/test/cases/trim-whitespaces/data.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/test/cases/trim-whitespaces/data.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/cases/trim-whitespaces/expected.html b/test/cases/trim-whitespaces/expected.html new file mode 100644 index 00000000..5a2aa71e --- /dev/null +++ b/test/cases/trim-whitespaces/expected.html @@ -0,0 +1 @@ +
Foobar
\ No newline at end of file diff --git a/test/integration.spec.ts b/test/integration.spec.ts index 506a0bfe..5a4af5f6 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -1,12 +1,24 @@ -import { ls, compile, getRenderArguments, readExpected, renderOnthefly } from '../src/fixtures/case' +import { ls, compile, tsExists, compileTS, getRenderArguments, readExpected, renderOnthefly } from '../src/fixtures/case' import { parseSanHTML } from '../src/index' for (const caseName of ls()) { const [expectedData, expectedHtml] = parseSanHTML(readExpected(caseName)) - if (!caseName.match(/-nsrc$/)) { - it('render to source: ' + caseName, async function () { - const code = compile(caseName, true) + it('render to source: ' + caseName, async function () { + const code = compile(caseName, true) + // eslint-disable-next-line + const render = new Function('data', 'noDataOutput', 'require', code) + // 测试在 strict mode,因此需要手动传入 require + const got = render(...getRenderArguments(caseName), require) + const [data, html] = parseSanHTML(got) + + expect(data).toEqual(expectedData) + expect(html).toEqual(expectedHtml) + }) + + if (tsExists(caseName)) { + it('render to source (TypeScript): ' + caseName, async function () { + const code = compileTS(caseName, true) // eslint-disable-next-line const render = new Function('data', 'noDataOutput', 'require', code) // 测试在 strict mode,因此需要手动传入 require diff --git a/test/unit/utils/ast-util.spec.ts b/test/unit/utils/ast-util.spec.ts index a6214f22..2d139142 100644 --- a/test/unit/utils/ast-util.spec.ts +++ b/test/unit/utils/ast-util.spec.ts @@ -1,4 +1,4 @@ -import { getComponentDeclarations, getObjectLiteralPropertyKeys, getChildComponents, getPropertyStringValue, getComponentClassIdentifier, isChildClassOf } from '../../../src/utils/ast-util' +import { getPropertyStringArrayValue, getComponentDeclarations, getObjectLiteralPropertyKeys, getChildComponents, getPropertyStringValue, getComponentClassIdentifier, isChildClassOf } from '../../../src/utils/ast-util' import { Project } from 'ts-morph' describe('utils/ast-util', function () { @@ -6,6 +6,30 @@ describe('utils/ast-util', function () { beforeEach(() => { proj = new Project({ addFilesFromTsConfig: false }) }) + describe('.getPropertyStringArrayValue()', function () { + it('should get array of strings', () => { + const file = proj.createSourceFile('foo.ts', ` + class Foo { + delimiters = ["{{", "}}"] + }`) + expect(getPropertyStringArrayValue(file.getClass('Foo'), 'delimiters')).toEqual(['{{', '}}']) + }) + it('should return undefined if property not assigned', () => { + const file = proj.createSourceFile('foo.ts', ` + class Foo { + delimiters + }`) + expect(getPropertyStringArrayValue(file.getClass('Foo'), 'delimiters')).toBeUndefined() + }) + it('should throw if not array literal', () => { + const file = proj.createSourceFile('foo.ts', ` + let foo = []; + class Foo { + delimiters = foo + }`) + expect(() => getPropertyStringArrayValue(file.getClass('Foo'), 'delimiters')).toThrow(/invalid "delimiters": "foo", array literal expected/) + }) + }) describe('.getObjectLiteralPropertyKeys()', function () { it('should support method declaration, property assignment, and shorthanded', () => { const file = proj.createSourceFile('foo.ts', ` @@ -59,13 +83,17 @@ describe('utils/ast-util', function () { }) describe('.getPropertyStringValue()', function () { - it('should return "" if member not exist', () => { + it('should return undefined if member not exist', () => { const file = proj.createSourceFile('foo.ts', `class Foo { }`) - expect(getPropertyStringValue(file.getClass('Foo'), 'template')).toEqual('') + expect(getPropertyStringValue(file.getClass('Foo'), 'template')).toEqual(undefined) }) - it('should return "" if member not initialized', () => { + it('should return undefined if member not initialized', () => { const file = proj.createSourceFile('foo.ts', `class Foo { template: string }`) - expect(getPropertyStringValue(file.getClass('Foo'), 'template')).toEqual('') + expect(getPropertyStringValue(file.getClass('Foo'), 'template')).toEqual(undefined) + }) + it('should return defaultValue if member not exist', () => { + const file = proj.createSourceFile('foo.ts', `class Foo { }`) + expect(getPropertyStringValue(file.getClass('Foo'), 'template', '')).toEqual('') }) it('should work for member with a stirng literal initializer', () => { const file = proj.createSourceFile('foo.ts', `class Foo { template = 'foo' }`)