Skip to content

Commit

Permalink
fix: trimWhitespace and delimiters, see https://baidu.github.io/san/d…
Browse files Browse the repository at this point in the history
  • Loading branch information
harttle committed Jul 16, 2020
1 parent 26cfbc8 commit ca9be69
Show file tree
Hide file tree
Showing 18 changed files with 165 additions and 28 deletions.
19 changes: 18 additions & 1 deletion bin/debug
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 () {
Expand All @@ -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 {
Expand Down
25 changes: 21 additions & 4 deletions src/fixtures/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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 }
)
Expand Down
4 changes: 3 additions & 1 deletion src/parsers/component-class-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions src/parsers/parse-template.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
10 changes: 7 additions & 3 deletions src/parsers/typescript-san-parser.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
)
Expand Down
3 changes: 2 additions & 1 deletion src/target-js/compilers/expr-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -100,7 +101,7 @@ function interp (interpExpr: ExprInterpNode): string {
}

function str (e: ExprStringNode): string {
return '"' + _.escapeHTML(e.value) + '"'
return stringifier.str(_.escapeHTML(e.value))
}

// 生成文本片段代码
Expand Down
25 changes: 20 additions & 5 deletions src/utils/ast-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends string> (clazz: ClassDeclaration, memberName: string, defaultValue: T): T;
export function getPropertyStringValue<T extends string> (clazz: ClassDeclaration, memberName: string): T | undefined;
export function getPropertyStringValue<T extends string> (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)) {
Expand All @@ -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<T extends string[]> (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()
Expand Down
4 changes: 3 additions & 1 deletion src/utils/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ export function isValidIdentifier (str: string) {
return !!/^[a-zA-Z_$][\w$]*$/.exec(str)
}

export function getMember<T> (clazz: Function, property: string, defaultValue: T): T {
export function getMember<T> (clazz: Function, property: string, defaultValue: T): T
export function getMember<T> (clazz: Function, property: string): T | undefined
export function getMember<T> (clazz: Function, property: string, defaultValue?: T): T | undefined {
if (clazz[property]) return clazz[property]
if (clazz.prototype && clazz.prototype[property]) {
return clazz.prototype[property]
Expand Down
7 changes: 7 additions & 0 deletions test/cases/delimiters/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { Component } = require('san')

class MyComponent extends Component {}
MyComponent.delimiters = ['[[', ']]']
MyComponent.template = `<div>[[ title ]]</div>`

module.exports = exports = MyComponent
6 changes: 6 additions & 0 deletions test/cases/delimiters/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Component } from 'san'

export default class MyComponent extends Component {
static template = `<div>[[ title ]]</div>`
delimiters = ['[[', ']]']
}
3 changes: 3 additions & 0 deletions test/cases/delimiters/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"title": "San"
}
1 change: 1 addition & 0 deletions test/cases/delimiters/expected.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><!--s-data:{"title":"San"}-->San</div>
11 changes: 11 additions & 0 deletions test/cases/trim-whitespaces/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { Component } = require('san')

class MyComponent extends Component {}
MyComponent.trimWhitespace = 'blank'
MyComponent.template = `
<div>
<a>Foo</a> <span>bar</span>
</div>
`

module.exports = exports = MyComponent
9 changes: 9 additions & 0 deletions test/cases/trim-whitespaces/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Component } from 'san'

export default class MyComponent extends Component {
static template = `
<div>
<a>Foo</a> <span>bar</span>
</div>`
trimWhitespace = 'blank'
}
1 change: 1 addition & 0 deletions test/cases/trim-whitespaces/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions test/cases/trim-whitespaces/expected.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div><!--s-data:{}--><a>Foo</a><span>bar</span></div>
20 changes: 16 additions & 4 deletions test/integration.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
38 changes: 33 additions & 5 deletions test/unit/utils/ast-util.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
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 () {
let proj
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', `
Expand Down Expand Up @@ -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' }`)
Expand Down

0 comments on commit ca9be69

Please sign in to comment.