webmat
Version:
Formats your entire project with clang-format
167 lines (138 loc) • 5.15 kB
text/typescript
/**
* @license
* Copyright (c) 2018 Google Inc. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* Code distributed by Google as part of this project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import * as dom5 from 'dom5';
import * as esprima from 'esprima';
import * as fs from 'fs';
import * as parse5 from 'parse5';
import {HtmlFileContent} from './format';
const SINGLE_TAB = ' ';
/**
* Indents and writes the content to the file.
*
* @param formattedContent Content to be written to the file.
*/
export async function writeTofile(formattedContent: (HtmlFileContent)):
Promise<void> {
const writableStream = fs.createWriteStream(formattedContent.filePath);
await updateAst(formattedContent);
const formattedDom = generateDom(formattedContent);
writableStream.write(formattedDom);
writableStream.end();
}
/**
* Tabs the given content and writes it to the AST in the content chunks
*
* @param formattedContent Content to be written to the AST
*/
function updateAst(formattedContent: HtmlFileContent): Promise<void[]> {
const updatePromises: Promise<void>[] = [];
for (const chunk of formattedContent.contents) {
const nodeLocation = chunk.node.__location as parse5.LocationInfo;
let numTabs = 1;
if (nodeLocation.col) {
numTabs = Math.floor(nodeLocation.col / SINGLE_TAB.length) + 1;
}
const tabbedString = SINGLE_TAB.repeat(numTabs);
const updatePromise = chunk.streamReader.streamCached.then((data) => {
const allTokens = esprima.tokenize(data, {loc: true});
const templateTokens = allTokens.filter((token) => {
return token.type === 'Template';
});
let stringifiedContents = '';
let splitData = data.split('\n');
const nonIndentableLines: {start: number, end: number}[] = [];
for (const templateToken of templateTokens) {
const firstNonIndentableLine = templateToken.loc!.start.line;
const lastNonIndentableLine = templateToken.loc!.end.line;
// just indent if it is all the same line
if (templateToken.loc!.start.line !== templateToken.loc!.end.line) {
nonIndentableLines.push({
start: firstNonIndentableLine,
end: lastNonIndentableLine,
});
}
}
// only tab nonempty lines and non-templates
for (let lineNumber = 1; lineNumber <= splitData.length; lineNumber++) {
const index = lineNumber - 1;
let line = splitData[index];
let isIndentable = true;
for (const nonIndentableLine of nonIndentableLines) {
// first line of a template string is indentable
if (lineNumber > nonIndentableLine.start &&
lineNumber <= nonIndentableLine.end) {
isIndentable = false;
break;
}
}
if (line.length && isIndentable) {
line = `${tabbedString}${line}`;
}
splitData[index] = line;
}
splitData = splitData.map((line) => {
return line;
});
const tabbedData = splitData.join(`\n`);
stringifiedContents += tabbedData;
const trimmedContents = stringifiedContents.trim();
// deal with tabs at the beginning and end if the script is not empty
if (stringifiedContents) {
stringifiedContents = `\n${tabbedString}${trimmedContents}` +
`\n${SINGLE_TAB.repeat(numTabs - 1)}`;
}
dom5.setTextContent(chunk.node, stringifiedContents);
});
updatePromises.push(updatePromise);
}
return Promise.all(updatePromises);
}
/**
* Transforms the content into a dom string with prpoper spacing around the
* spliced inline scripts.
*
* @param formattedContent Content to be transformed into a dom string.
*/
function generateDom(formattedContent: HtmlFileContent): string {
const sortedChunks = formattedContent.contents.sort((a, b) => {
const aLine =
(a.node.__location as parse5.ElementLocationInfo).startTag.line;
const bLine =
(b.node.__location as parse5.ElementLocationInfo).startTag.line;
if (aLine > bLine) {
return -1;
} else if (aLine < bLine) {
return 1;
}
return 0;
});
const splitDom = formattedContent.dom.split('\n');
for (const chunk of sortedChunks) {
const location = chunk.node.__location as parse5.ElementLocationInfo;
const startTag = location.startTag;
const endTag = location.endTag;
const numLines = endTag.line - startTag.line + 1;
const scriptTagWhitespace: dom5.Node = {
attrs: [],
nodeName: '#text',
value: ' '.repeat(startTag.col - 1),
__location: {} as parse5.ElementLocationInfo
};
const fakeNode: dom5.Node = {
attrs: [],
childNodes: [scriptTagWhitespace, chunk.node],
nodeName: 'div',
__location: {} as parse5.ElementLocationInfo
};
const formattedChunk = parse5.serialize(fakeNode);
splitDom.splice(startTag.line - 1, numLines, formattedChunk);
}
return splitDom.join('\n');
}