2019-11-18 18:18:28 -08:00
|
|
|
|
/**
|
|
|
|
|
* Copyright 2017 Google Inc. All rights reserved.
|
|
|
|
|
*
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
|
2020-04-23 19:52:06 -07:00
|
|
|
|
const path = require('path');
|
2019-11-18 18:18:28 -08:00
|
|
|
|
|
2020-12-21 18:09:55 -08:00
|
|
|
|
function runCommands(sources, {libversion, chromiumVersion, firefoxVersion, webkitVersion}) {
|
2019-11-18 18:18:28 -08:00
|
|
|
|
// Release version is everything that doesn't include "-".
|
2020-02-13 18:26:38 -08:00
|
|
|
|
const isReleaseVersion = !libversion.includes('-');
|
2019-11-18 18:18:28 -08:00
|
|
|
|
|
2020-12-28 16:19:28 -08:00
|
|
|
|
const errors = [];
|
2019-11-18 18:18:28 -08:00
|
|
|
|
for (const source of sources) {
|
|
|
|
|
const text = source.text();
|
|
|
|
|
const commandStartRegex = /<!--\s*gen:([a-z-]+)\s*-->/ig;
|
|
|
|
|
const commandEndRegex = /<!--\s*gen:stop\s*-->/ig;
|
|
|
|
|
let start;
|
|
|
|
|
|
2020-04-23 19:52:06 -07:00
|
|
|
|
const sourceEdits = new SourceEdits(source);
|
|
|
|
|
// Extract all commands from source
|
2019-11-18 18:18:28 -08:00
|
|
|
|
while (start = commandStartRegex.exec(text)) { // eslint-disable-line no-cond-assign
|
|
|
|
|
commandEndRegex.lastIndex = commandStartRegex.lastIndex;
|
|
|
|
|
const end = commandEndRegex.exec(text);
|
|
|
|
|
if (!end) {
|
2020-12-28 16:19:28 -08:00
|
|
|
|
errors.push(`Failed to find 'gen:stop' for command ${start[0]}`);
|
|
|
|
|
return errors;
|
2019-11-18 18:18:28 -08:00
|
|
|
|
}
|
2020-04-23 19:52:06 -07:00
|
|
|
|
const commandName = start[1];
|
2019-11-18 18:18:28 -08:00
|
|
|
|
const from = commandStartRegex.lastIndex;
|
|
|
|
|
const to = end.index;
|
|
|
|
|
commandStartRegex.lastIndex = commandEndRegex.lastIndex;
|
|
|
|
|
|
2020-04-23 19:52:06 -07:00
|
|
|
|
let newText = null;
|
2020-07-22 11:03:35 -07:00
|
|
|
|
if (commandName === 'chromium-version')
|
2020-04-23 19:52:06 -07:00
|
|
|
|
newText = chromiumVersion;
|
|
|
|
|
else if (commandName === 'firefox-version')
|
|
|
|
|
newText = firefoxVersion;
|
2020-12-21 18:09:55 -08:00
|
|
|
|
else if (commandName === 'webkit-version')
|
|
|
|
|
newText = webkitVersion;
|
2020-04-23 19:52:06 -07:00
|
|
|
|
else if (commandName === 'chromium-version-badge')
|
|
|
|
|
newText = `[](https://www.chromium.org/Home)`;
|
|
|
|
|
else if (commandName === 'firefox-version-badge')
|
|
|
|
|
newText = `[](https://www.mozilla.org/en-US/firefox/new/)`;
|
2020-07-22 11:03:35 -07:00
|
|
|
|
else if (commandName === 'version')
|
|
|
|
|
newText = isReleaseVersion ? 'v' + libversion : 'Tip-Of-Tree';
|
2020-04-23 19:52:06 -07:00
|
|
|
|
else if (commandName === 'toc')
|
|
|
|
|
newText = generateTableOfContents(source.text(), to, false /* topLevelOnly */);
|
|
|
|
|
else if (commandName === 'toc-top-level')
|
|
|
|
|
newText = generateTableOfContents(source.text(), to, true /* topLevelOnly */);
|
|
|
|
|
else if (commandName.startsWith('toc-extends-'))
|
|
|
|
|
newText = generateTableOfContentsForSuperclass(source.text(), 'class: ' + commandName.substring('toc-extends-'.length));
|
|
|
|
|
|
|
|
|
|
if (newText === null)
|
2020-12-28 16:19:28 -08:00
|
|
|
|
errors.push(`Unknown command 'gen:${commandName}'`);
|
2020-04-23 19:52:06 -07:00
|
|
|
|
else
|
|
|
|
|
sourceEdits.edit(from, to, newText);
|
|
|
|
|
}
|
2020-12-28 16:19:28 -08:00
|
|
|
|
sourceEdits.commit(errors);
|
2019-11-18 18:18:28 -08:00
|
|
|
|
}
|
2020-12-28 16:19:28 -08:00
|
|
|
|
return errors;
|
2019-11-18 18:18:28 -08:00
|
|
|
|
};
|
|
|
|
|
|
2020-01-22 12:21:45 -08:00
|
|
|
|
function getTOCEntriesForText(text) {
|
2019-11-18 18:18:28 -08:00
|
|
|
|
const ids = new Set();
|
|
|
|
|
const titles = [];
|
2020-05-04 22:28:09 -07:00
|
|
|
|
const titleRegex = /^(#+)\s+(.*)$/;
|
2019-11-18 18:18:28 -08:00
|
|
|
|
let insideCodeBlock = false;
|
2020-01-22 12:21:45 -08:00
|
|
|
|
let offset = 0;
|
|
|
|
|
text.split('\n').forEach((aLine, lineNumber) => {
|
2019-11-18 18:18:28 -08:00
|
|
|
|
const line = aLine.trim();
|
2020-01-22 12:21:45 -08:00
|
|
|
|
if (line.startsWith('```'))
|
2019-11-18 18:18:28 -08:00
|
|
|
|
insideCodeBlock = !insideCodeBlock;
|
2020-05-04 22:28:09 -07:00
|
|
|
|
else if (!insideCodeBlock && line.match(titleRegex))
|
2020-01-22 12:21:45 -08:00
|
|
|
|
titles.push({line, offset: offset + lineNumber});
|
|
|
|
|
offset += aLine.length;
|
|
|
|
|
});
|
|
|
|
|
let tocEntries = [];
|
|
|
|
|
for (const {line, offset} of titles) {
|
2020-05-04 22:28:09 -07:00
|
|
|
|
const [, nesting, name] = line.match(titleRegex);
|
2019-11-18 18:18:28 -08:00
|
|
|
|
const delinkifiedName = name.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
2020-09-10 15:14:00 -07:00
|
|
|
|
const id = delinkifiedName.trim().toLowerCase().replace(/\s/g, '-').replace(/[^-_0-9a-zа-яё]/ig, '');
|
2019-11-18 18:18:28 -08:00
|
|
|
|
let dedupId = id;
|
|
|
|
|
let counter = 0;
|
|
|
|
|
while (ids.has(dedupId))
|
|
|
|
|
dedupId = id + '-' + (++counter);
|
|
|
|
|
ids.add(dedupId);
|
|
|
|
|
tocEntries.push({
|
|
|
|
|
level: nesting.length,
|
|
|
|
|
name: delinkifiedName,
|
2020-01-22 12:21:45 -08:00
|
|
|
|
id: dedupId,
|
|
|
|
|
offset,
|
2019-11-18 18:18:28 -08:00
|
|
|
|
});
|
|
|
|
|
}
|
2020-01-22 12:21:45 -08:00
|
|
|
|
return tocEntries;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-18 15:34:53 -07:00
|
|
|
|
/**
|
|
|
|
|
* @param {string} text
|
|
|
|
|
*/
|
2020-04-23 19:52:06 -07:00
|
|
|
|
function autocorrectInvalidLinks(projectRoot, sources, allowedFilePaths) {
|
|
|
|
|
const pathToHashLinks = new Map();
|
2020-03-18 15:34:53 -07:00
|
|
|
|
for (const source of sources) {
|
|
|
|
|
const text = source.text();
|
2020-04-23 19:52:06 -07:00
|
|
|
|
const hashLinks = new Set(getTOCEntriesForText(text).map(entry => entry.id));
|
|
|
|
|
pathToHashLinks.set(source.filePath(), hashLinks);
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-28 16:19:28 -08:00
|
|
|
|
const errors = [];
|
2020-04-23 19:52:06 -07:00
|
|
|
|
for (const source of sources) {
|
|
|
|
|
const allRelativePaths = [];
|
|
|
|
|
for (const filepath of allowedFilePaths) {
|
|
|
|
|
allRelativePaths.push('/' + path.relative(projectRoot, filepath));
|
|
|
|
|
allRelativePaths.push(path.relative(path.dirname(source.filePath()), filepath));
|
2020-03-18 15:34:53 -07:00
|
|
|
|
}
|
2020-04-23 19:52:06 -07:00
|
|
|
|
const sourceEdits = new SourceEdits(source);
|
|
|
|
|
let offset = 0;
|
|
|
|
|
|
|
|
|
|
const lines = source.text().split('\n');
|
|
|
|
|
lines.forEach((line, lineNumber) => {
|
|
|
|
|
const linkRegex = /\]\(([^\)]*)\)/gm;
|
|
|
|
|
let match;
|
|
|
|
|
while (match = linkRegex.exec(line)) {
|
|
|
|
|
const hrefOffset = offset + lineNumber + match.index + 2; // +2 since we have to skip ](
|
|
|
|
|
const [, href] = match;
|
|
|
|
|
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('mailto:'))
|
|
|
|
|
continue;
|
|
|
|
|
const [relativePath, hash] = href.split('#');
|
|
|
|
|
const hashOffset = hrefOffset + relativePath.length + 1;
|
|
|
|
|
|
|
|
|
|
let resolvedPath = resolveLinkPath(source, relativePath);
|
|
|
|
|
let hashLinks = pathToHashLinks.get(resolvedPath);
|
|
|
|
|
|
|
|
|
|
if (!hashLinks) {
|
|
|
|
|
// Attempt to autocorrect
|
|
|
|
|
const newRelativePath = autocorrectText(relativePath, allRelativePaths);
|
|
|
|
|
if (!newRelativePath) {
|
2020-12-28 16:19:28 -08:00
|
|
|
|
errors.push(`Bad link in ${source.projectPath()}:${lineNumber + 1}: file ${relativePath} does not exist`);
|
2020-04-23 19:52:06 -07:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
resolvedPath = resolveLinkPath(source, newRelativePath);
|
|
|
|
|
hashLinks = pathToHashLinks.get(resolvedPath);
|
|
|
|
|
sourceEdits.edit(hrefOffset, hrefOffset + relativePath.length, newRelativePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!hash || hashLinks.has(hash))
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
const newHashLink = autocorrectText(hash, [...hashLinks]);
|
|
|
|
|
if (newHashLink) {
|
|
|
|
|
sourceEdits.edit(hashOffset, hashOffset + hash.length, newHashLink);
|
|
|
|
|
} else {
|
2020-12-28 16:19:28 -08:00
|
|
|
|
errors.push(`Bad link in ${source.projectPath()}:${lineNumber + 1}: hash "#${hash}" does not exist in "${path.relative(projectRoot, resolvedPath)}"`);
|
2020-04-23 19:52:06 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
offset += line.length;
|
|
|
|
|
});
|
|
|
|
|
|
2020-12-28 16:19:28 -08:00
|
|
|
|
sourceEdits.commit(errors);
|
2020-03-18 15:34:53 -07:00
|
|
|
|
}
|
2020-12-28 16:19:28 -08:00
|
|
|
|
return errors;
|
2020-04-23 19:52:06 -07:00
|
|
|
|
|
|
|
|
|
function resolveLinkPath(source, relativePath) {
|
|
|
|
|
if (!relativePath)
|
|
|
|
|
return source.filePath();
|
|
|
|
|
if (relativePath.startsWith('/'))
|
|
|
|
|
return path.resolve(projectRoot, '.' + relativePath);
|
|
|
|
|
return path.resolve(path.dirname(source.filePath()), relativePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class SourceEdits {
|
|
|
|
|
constructor(source) {
|
|
|
|
|
this._source = source;
|
|
|
|
|
this._edits = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
edit(from, to, newText) {
|
|
|
|
|
this._edits.push({from, to, newText});
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-28 16:19:28 -08:00
|
|
|
|
commit(errors = []) {
|
2020-04-23 19:52:06 -07:00
|
|
|
|
if (!this._edits.length)
|
|
|
|
|
return;
|
|
|
|
|
this._edits.sort((a, b) => a.from - b.from);
|
|
|
|
|
for (const edit of this._edits) {
|
|
|
|
|
if (edit.from > edit.to) {
|
2020-12-28 16:19:28 -08:00
|
|
|
|
errors.push('INTERNAL ERROR: incorrect edit!');
|
2020-04-23 19:52:06 -07:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < this._edits.length - 1; ++i) {
|
|
|
|
|
if (this._edits[i].to > this._edits[i + 1].from) {
|
2020-12-28 16:19:28 -08:00
|
|
|
|
errors.push('INTERNAL ERROR: edits are overlapping!');
|
2020-04-23 19:52:06 -07:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this._edits.reverse();
|
|
|
|
|
let text = this._source.text();
|
|
|
|
|
for (const edit of this._edits)
|
|
|
|
|
text = text.substring(0, edit.from) + edit.newText + text.substring(edit.to);
|
|
|
|
|
this._source.setText(text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function autocorrectText(text, options, maxCorrectionsRatio = 0.5) {
|
|
|
|
|
if (!options.length)
|
|
|
|
|
return null;
|
|
|
|
|
const scores = options.map(option => ({option, score: levenshteinDistance(text, option)}));
|
|
|
|
|
scores.sort((a, b) => a.score - b.score);
|
|
|
|
|
if (scores[0].score > text.length * maxCorrectionsRatio)
|
|
|
|
|
return null;
|
|
|
|
|
return scores[0].option;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function levenshteinDistance(a, b) {
|
|
|
|
|
const N = a.length, M = b.length;
|
|
|
|
|
const d = new Int32Array(N * M);
|
|
|
|
|
for (let i = 0; i < N * M; ++i)
|
|
|
|
|
d[i] = 0;
|
|
|
|
|
for (let j = 0; j < M; ++j)
|
|
|
|
|
d[j] = j;
|
|
|
|
|
for (let i = 0; i < N; ++i)
|
|
|
|
|
d[i * M] = i;
|
|
|
|
|
for (let i = 1; i < N; ++i) {
|
|
|
|
|
for (let j = 1; j < M; ++j) {
|
|
|
|
|
const cost = a[i] === b[j] ? 0 : 1;
|
|
|
|
|
d[i * M + j] = Math.min(
|
|
|
|
|
d[(i - 1) * M + j] + 1, // d[i-1][j] + 1
|
|
|
|
|
d[i * M + j - 1] + 1, // d[i][j - 1] + 1
|
|
|
|
|
d[(i - 1) * M + j - 1] + cost // d[i - 1][j - 1] + cost
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return d[N * M - 1];
|
2020-03-18 15:34:53 -07:00
|
|
|
|
}
|
|
|
|
|
|
2020-01-22 12:21:45 -08:00
|
|
|
|
function generateTableOfContents(text, offset, topLevelOnly) {
|
|
|
|
|
const allTocEntries = getTOCEntriesForText(text);
|
|
|
|
|
|
|
|
|
|
let tocEntries = [];
|
|
|
|
|
let nesting = 0;
|
|
|
|
|
for (const tocEntry of allTocEntries) {
|
|
|
|
|
if (tocEntry.offset < offset)
|
|
|
|
|
continue;
|
|
|
|
|
if (tocEntries.length) {
|
|
|
|
|
nesting += tocEntry.level - tocEntries[tocEntries.length - 1].level;
|
|
|
|
|
if (nesting < 0)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
tocEntries.push(tocEntry);
|
|
|
|
|
}
|
2019-11-18 18:18:28 -08:00
|
|
|
|
|
|
|
|
|
const minLevel = Math.min(...tocEntries.map(entry => entry.level));
|
|
|
|
|
tocEntries.forEach(entry => entry.level -= minLevel);
|
2020-01-22 12:21:45 -08:00
|
|
|
|
if (topLevelOnly)
|
|
|
|
|
tocEntries = tocEntries.filter(entry => !entry.level);
|
2019-11-18 18:18:28 -08:00
|
|
|
|
return '\n' + tocEntries.map(entry => {
|
|
|
|
|
const prefix = entry.level % 2 === 0 ? '-' : '*';
|
|
|
|
|
const padding = ' '.repeat(entry.level);
|
|
|
|
|
return `${padding}${prefix} [${entry.name}](#${entry.id})`;
|
|
|
|
|
}).join('\n') + '\n';
|
|
|
|
|
}
|
2020-01-27 10:05:04 -08:00
|
|
|
|
|
|
|
|
|
function generateTableOfContentsForSuperclass(text, name) {
|
|
|
|
|
const allTocEntries = getTOCEntriesForText(text);
|
|
|
|
|
|
|
|
|
|
for (const tocEntry of allTocEntries) {
|
|
|
|
|
if (tocEntry.name !== name)
|
|
|
|
|
continue;
|
|
|
|
|
const offset = text.indexOf('<!-- GEN:stop -->', tocEntry.offset);
|
|
|
|
|
return generateTableOfContents(text, offset, false);
|
|
|
|
|
}
|
|
|
|
|
return text;
|
|
|
|
|
}
|
2020-03-18 15:34:53 -07:00
|
|
|
|
|
2020-12-28 07:03:09 -08:00
|
|
|
|
module.exports = {autocorrectInvalidLinks, runCommands};
|