/** * libjass * * https://github.com/Arnavion/libjass * * Copyright 2013 Arnav Singh * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { FileTransform } from "async-build"; import * as AST from "./typescript/ast"; import { Compiler } from "./typescript/compiler"; import { walk } from "./typescript/walker"; function flatten(arr: T[][]): T[] { let result: T[] = []; for (const a of arr) { result = result.concat(a); } return result; } const sorter = (() => { function visibilitySorter(value1: { isPrivate?: boolean; isProtected?: boolean; }, value2: { isPrivate?: boolean; isProtected?: boolean; }) { if (value1.isPrivate === value2.isPrivate && value1.isProtected === value2.isProtected) { return 0; } if (value1.isPrivate) { return 1; } if (value2.isPrivate) { return -1; } if (value1.isProtected) { return 1; } if (value2.isProtected) { return -1; } return 0; } const types = [AST.Property, AST.Function, AST.Interface, AST.Class, AST.Enum]; function typeSorter(value1: AST.ModuleMember | AST.NamespaceMember, value2: AST.ModuleMember | AST.NamespaceMember) { let type1Index = -1; let type2Index = -1; types.every((type, index) => { if (value1 instanceof type) { type1Index = index; } if (value2 instanceof type) { type2Index = index; } return (type1Index === -1) || (type2Index === -1); }); return type1Index - type2Index; } function nameSorter(value1: { name: string }, value2: { name: string }) { return value1.name.localeCompare(value2.name); } const sorters: ((value1: AST.ModuleMember, value2: AST.ModuleMember) => number)[] = [visibilitySorter, typeSorter, nameSorter]; return (value1: AST.ModuleMember, value2: AST.ModuleMember) => { for (const sorter of sorters) { const result = sorter(value1, value2); if (result !== 0) { return result; } } return 0; }; })(); function indenter(indent: number) { return (line: string) => ((line === "") ? line : (Array(indent + 1).join("\t") + line)); } function sanitize(str: string) { return str.replace(/&/g, "&").replace(//g, ">"); } function toVariableName(item: { name: string }) { // TODO: Handle non-letters (are both their toLowerCase() and toUpperCase()) const name = item.name; let result = ""; for (let i = 0; i < name.length; i++) { if (name[i] === name[i].toLowerCase()) { // This is lower case. Write it as lower case. result += name[i]; } else { // This is upper case. if (i === 0) { // This is the first character. Write it as lower case. result += name[i].toLowerCase(); } else if (name[i - 1] === name[i - 1].toUpperCase()) { // The previous character was upper case. if (i === name.length - 1) { // This is the last character. Write it as lower case. result += name[i].toLowerCase(); } else if (name[i + 1] === name[i + 1].toLowerCase()) { // The next character is lower case so this is the start of a new word. Write this one as upper case. result += name[i]; } else { // The next character is upper case. Write this one as lower case. result += name[i].toLowerCase(); } } else { // Previous character was lower case so this is the start of a new word. Write this one as upper case. result += name[i]; } } } return result; } function toUsageName(item: AST.Class | AST.Interface | AST.Function | AST.Property | AST.Enum): string { if (item.parent instanceof AST.Module) { return item.name; } if (item instanceof AST.Class || item instanceof AST.Interface || item instanceof AST.Enum) { if (item.isPrivate) { return item.name; } return item.fullName; } if (item.parent instanceof AST.Namespace) { if ((item as AST.CanBePrivate).isPrivate) { return item.name; } return item.fullName; } if ((item as AST.CanBeStatic).isStatic) { return toUsageName(item.parent as AST.Class | AST.Interface) + '.' + item.name; } return toVariableName(item.parent) + '.' + item.name; } function toId(item: { fullName?: string; name: string; }): string { return sanitize((item.fullName === undefined) ? item.name : item.fullName); } function toLink(item: AST.ModuleMember | AST.EnumMember | AST.TypeReference): string { let result = `${ sanitize(item.name) }`; if (AST.hasGenerics(item) && item.generics.length > 0) { const generics = item.generics as (string | AST.TypeReference | AST.IntrinsicTypeReference)[]; result += sanitize(`.<${ generics.map(generic => (generic instanceof AST.TypeReference || generic instanceof AST.IntrinsicTypeReference) ? generic.name : generic ).join(', ') }>`); } result += ''; return result; } function writeDescription(text: string): string { let result = sanitize(text).replace(/\{@link ([^} ]+)\}/g, (substring, linkTarget) => `${ linkTarget }`); let inCodeBlock = false; result = result.split("\n").map(line => { if (line.substr(0, " ".length) === " ") { line = line.substr(" ".length); if (!inCodeBlock) { inCodeBlock = true; line = `
${ line }`;
			}
		}
		else if (line.length > 0 && inCodeBlock) {
			inCodeBlock = false;
			line = `
${ line }`; } else if (line.length === 0 && !inCodeBlock) { line = "

"; } return line; }).join("\n"); if (inCodeBlock) { result += ''; } result = `

${ result }

`.replace(/

\s*<\/p>/g, "").replace(/

\s+/g, "

").replace(/\s+<\/p>/g, "

"); if (result.substr("

".length).indexOf("

") === -1) { result = result.substring("

".length, result.length - "

".length); } return result; } function writeParameters(parameters: AST.Parameter[]): string[] { if (parameters.length === 0) { return []; } return [ '
', '
' ].concat(flatten(parameters.map(parameter => { return [ `
${ sanitize(parameter.name) }
`, `
${ sanitize(parameter.type) }
`, `
${ writeDescription(parameter.description) }
` ].concat(writeParameters(parameter.subParameters).map(indenter(2))); }))).concat([ '
', '
' ]); } function functionToHtml(func: AST.Function): string[] { return [ `
`, `
${ toLink(func) }
`, '
', ` ${ writeDescription(func.description) }`, '
', `
${
				sanitize(
					`${ (func.returnType !== null) ? 'var result = ' : '' }${ toUsageName(func) }(${
					func.parameters.map(parameter => parameter.name).join(', ') });`
				) }
` ].concat(writeParameters(func.parameters).map(indenter(1))).concat( (func.returnType === null) ? [] : [ '
Returns
', `
${ sanitize(func.returnType.type) }
`, `
${ writeDescription(func.returnType.description) }
` ] ).concat([ '
', '' ]); } function interfaceToHtml(interfase: AST.Interface): string[] { const members: AST.InterfaceMember[] = []; Object.keys(interfase.members).forEach(memberName => members.push(interfase.members[memberName])); members.sort(sorter); return [ `
`, `
interface ${ toLink(interfase) }${ (interfase.baseTypes.length > 0) ? ` extends ${ interfase.baseTypes.map(baseType => baseType instanceof AST.TypeReference ? toLink(baseType) : baseType.name).join(', ') }` : '' }
`, '
', ` ${ writeDescription(interfase.description) }`, '
', '
' ].concat(flatten(members.map(member => { if (member instanceof AST.Property) { return propertyToHtml(member).map(indenter(2)); } else if (member instanceof AST.Function) { return functionToHtml(member).map(indenter(2)); } else { throw new Error(`Unrecognized member type: ${ (member as any).constructor.name }`); } }))).concat([ '
', '
', '' ]); } function classToHtml(clazz: AST.Class): string[] { const members: AST.InterfaceMember[] = []; Object.keys(clazz.members).forEach(memberName => members.push(clazz.members[memberName])); members.sort(sorter); return [ `
`, `
class ${ toLink(clazz) }${ (clazz.baseType !== null) ? ` extends ${ (clazz.baseType instanceof AST.TypeReference) ? toLink(clazz.baseType) : clazz.baseType.name }` : '' }${ (clazz.interfaces.length > 0) ? ` implements ${ clazz.interfaces.map(interfase => interfase instanceof AST.TypeReference ? toLink(interfase) : interfase.name).join(', ') }` : ''}
`, '
', ` ${ writeDescription(clazz.description) }`, '
', `
${
				sanitize(
					`var ${ toVariableName(clazz) } = new ${ toUsageName(clazz) }(${
					clazz.parameters.map(parameter => parameter.name).join(', ') });`
				) }
` ].concat(writeParameters(clazz.parameters).map(indenter(1))).concat([ '
' ]).concat(flatten(members.map(member => { if (member instanceof AST.Property) { return propertyToHtml(member).map(indenter(2)); } else if (member instanceof AST.Function) { return functionToHtml(member).map(indenter(2)); } else { throw new Error(`Unrecognized member type: ${ (member as any).constructor.name }`); } }))).concat([ '
', '
', '' ]); } function enumToHtml(enumType: AST.Enum): string[] { return [ `
`, `
enum ${ sanitize(enumType.name) }
`, '
', ` ${ writeDescription(enumType.description) }`, '
' ].concat([ '
' ]).concat(flatten(enumType.members.map(member => [ `
`, `
${ toLink(member) } = ${ member.value }
`, '
', ` ${ writeDescription(member.description) }`, '
', '
' ]))).concat([ '
', '
', '' ]); } function propertyToHtml(property: AST.Property): string[] { return [ `
`, `
${ toLink(property) }
` ].concat((property.getter === null) ? [] : [ `
Getter
`, '
', ` ${ writeDescription(property.getter.description) }`, '
', `
${ sanitize(`var result = ${ toUsageName(property) };`) }
`, `
${ sanitize(property.getter.type) }
` ]).concat((property.setter === null) ? [] : [ `
Setter
`, '
', ` ${ writeDescription(property.setter.description) }`, '
', `
${ sanitize(`${ toUsageName(property) } = value;`) }
` ].concat(writeParameters([new AST.Parameter("value", "", property.setter.type)]).map(indenter(1)))).concat([ '
', '' ]); } export function build(outputFilePath: string, root: string, rootNamespaceName: string): FileTransform { const compiler = new Compiler(); return new FileTransform(function (file): void { // Compile compiler.compile(file); // Walk const walkResult = walk(compiler, root, rootNamespaceName); const namespaces = walkResult.namespaces; const modules = walkResult.modules; // Make HTML const namespaceNames = Object.keys(namespaces) .filter(namespaceName => namespaceName.substr(0, rootNamespaceName.length) === rootNamespaceName) .sort((ns1, ns2) => ns1.localeCompare(ns2)); const moduleNames = Object.keys(modules).sort((ns1, ns2) => ns1.localeCompare(ns2)).filter(moduleName => Object.keys(modules[moduleName].members).length > 0); this.push({ path: outputFilePath, contents: Buffer.concat([new Buffer( ` ${ rootNamespaceName } API Documentation
` )]).concat(flatten(namespaceNames.map(namespaceName => { const namespace = namespaces[namespaceName]; const namespaceMembers: AST.NamespaceMember[] = []; for (const memberName of Object.keys(namespace.members)) { namespaceMembers.push(namespace.members[memberName]); } namespaceMembers.sort(sorter); const properties = namespaceMembers.filter(member => member instanceof AST.Property) as AST.Property[]; const functions = namespaceMembers.filter(member => member instanceof AST.Function) as AST.Function[]; const interfaces = namespaceMembers.filter(member => member instanceof AST.Interface) as AST.Interface[]; const classes = namespaceMembers.filter(member => member instanceof AST.Class) as AST.Class[]; const enums = namespaceMembers.filter(member => member instanceof AST.Enum) as AST.Enum[]; const result = [new Buffer( `

Namespace ${ sanitize(namespaceName) }

` )]; if (properties.length > 0) { result.push(new Buffer( `

Properties

` )); for (const property of properties) { result.push(new Buffer(propertyToHtml(property).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } if (functions.length > 0) { result.push(new Buffer( `

Free functions

` )); for (const func of functions) { result.push(new Buffer(functionToHtml(func).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } if (interfaces.length > 0) { result.push(new Buffer( `

Interfaces

` )); for (const interfase of interfaces) { result.push(new Buffer(interfaceToHtml(interfase).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } if (classes.length > 0) { result.push(new Buffer( `

Classes

` )); for (const clazz of classes) { result.push(new Buffer(classToHtml(clazz).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } if (enums.length > 0) { result.push(new Buffer( `

Enums

` )); for (const enumType of enums) { result.push(new Buffer(enumToHtml(enumType).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } result.push(new Buffer( `
` )); return result; }))).concat(flatten(moduleNames.map(moduleName => { const module = modules[moduleName]; const moduleMembers: AST.ModuleMember[] = []; for (const memberName of Object.keys(module.members)) { const member = module.members[memberName]; if ((member as AST.HasParent).parent === module) { moduleMembers.push(member); } } if (moduleMembers.length === 0) { return []; } moduleMembers.sort(sorter); const properties = moduleMembers.filter(member => member instanceof AST.Property) as AST.Property[]; const functions = moduleMembers.filter(member => member instanceof AST.Function) as AST.Function[]; const interfaces = moduleMembers.filter(member => member instanceof AST.Interface) as AST.Interface[]; const classes = moduleMembers.filter(member => member instanceof AST.Class) as AST.Class[]; const enums = moduleMembers.filter(member => member instanceof AST.Enum) as AST.Enum[]; const result = [new Buffer( `

Module ${ sanitize(moduleName) }

` )]; if (properties.length > 0) { result.push(new Buffer( `

Properties

` )); for (const property of properties) { result.push(new Buffer(propertyToHtml(property).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } if (functions.length > 0) { result.push(new Buffer( `

Free functions

` )); for (const func of functions) { result.push(new Buffer(functionToHtml(func).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } if (interfaces.length > 0) { result.push(new Buffer( `

Interfaces

` )); for (const interfase of interfaces) { result.push(new Buffer(interfaceToHtml(interfase).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } if (classes.length > 0) { result.push(new Buffer( `

Classes

` )); for (const clazz of classes) { result.push(new Buffer(classToHtml(clazz).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } if (enums.length > 0) { result.push(new Buffer( `

Enums

` )); for (const enumType of enums) { result.push(new Buffer(enumToHtml(enumType).map(indenter(5)).join("\n"))); } result.push(new Buffer( `
` )); } result.push(new Buffer( `
` )); return result; }))).concat([new Buffer( `
` )])) }); }); }