2019-11-19 05:18:28 +03: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-24 05:52:06 +03:00
const path = require ( 'path' ) ;
2019-11-19 05:18:28 +03:00
const Message = require ( '../Message' ) ;
2020-07-22 21:03:35 +03:00
function runCommands ( sources , { libversion , chromiumVersion , firefoxVersion , onlyBrowserVersions } ) {
2019-11-19 05:18:28 +03:00
// Release version is everything that doesn't include "-".
2020-02-14 05:26:38 +03:00
const isReleaseVersion = ! libversion . includes ( '-' ) ;
2019-11-19 05:18:28 +03:00
const messages = [ ] ;
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-24 05:52:06 +03:00
const sourceEdits = new SourceEdits ( source ) ;
// Extract all commands from source
2019-11-19 05:18:28 +03:00
while ( start = commandStartRegex . exec ( text ) ) { // eslint-disable-line no-cond-assign
commandEndRegex . lastIndex = commandStartRegex . lastIndex ;
const end = commandEndRegex . exec ( text ) ;
if ( ! end ) {
messages . push ( Message . error ( ` Failed to find 'gen:stop' for command ${ start [ 0 ] } ` ) ) ;
return messages ;
}
2020-04-24 05:52:06 +03:00
const commandName = start [ 1 ] ;
2019-11-19 05:18:28 +03:00
const from = commandStartRegex . lastIndex ;
const to = end . index ;
commandStartRegex . lastIndex = commandEndRegex . lastIndex ;
2020-04-24 05:52:06 +03:00
let newText = null ;
2020-07-22 21:03:35 +03:00
if ( commandName === 'chromium-version' )
2020-04-24 05:52:06 +03:00
newText = chromiumVersion ;
else if ( commandName === 'firefox-version' )
newText = firefoxVersion ;
else if ( commandName === 'chromium-version-badge' )
newText = ` [![Chromium version](https://img.shields.io/badge/chromium- ${ chromiumVersion } -blue.svg?logo=google-chrome)](https://www.chromium.org/Home) ` ;
else if ( commandName === 'firefox-version-badge' )
newText = ` [![Firefox version](https://img.shields.io/badge/firefox- ${ firefoxVersion } -blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) ` ;
2020-07-22 21:03:35 +03:00
else if ( onlyBrowserVersions )
continue ;
else if ( commandName === 'version' )
newText = isReleaseVersion ? 'v' + libversion : 'Tip-Of-Tree' ;
2020-04-24 05:52:06 +03: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 )
messages . push ( Message . error ( ` Unknown command 'gen: ${ commandName } ' ` ) ) ;
else
sourceEdits . edit ( from , to , newText ) ;
}
sourceEdits . commit ( messages ) ;
2019-11-19 05:18:28 +03:00
}
return messages ;
} ;
2020-01-22 23:21:45 +03:00
function getTOCEntriesForText ( text ) {
2019-11-19 05:18:28 +03:00
const ids = new Set ( ) ;
const titles = [ ] ;
2020-05-05 08:28:09 +03:00
const titleRegex = /^(#+)\s+(.*)$/ ;
2019-11-19 05:18:28 +03:00
let insideCodeBlock = false ;
2020-01-22 23:21:45 +03:00
let offset = 0 ;
text . split ( '\n' ) . forEach ( ( aLine , lineNumber ) => {
2019-11-19 05:18:28 +03:00
const line = aLine . trim ( ) ;
2020-01-22 23:21:45 +03:00
if ( line . startsWith ( '```' ) )
2019-11-19 05:18:28 +03:00
insideCodeBlock = ! insideCodeBlock ;
2020-05-05 08:28:09 +03:00
else if ( ! insideCodeBlock && line . match ( titleRegex ) )
2020-01-22 23:21:45 +03:00
titles . push ( { line , offset : offset + lineNumber } ) ;
offset += aLine . length ;
} ) ;
let tocEntries = [ ] ;
for ( const { line , offset } of titles ) {
2020-05-05 08:28:09 +03:00
const [ , nesting , name ] = line . match ( titleRegex ) ;
2019-11-19 05:18:28 +03:00
const delinkifiedName = name . replace ( /\[([^\]]+)\]\([^)]+\)/g , '$1' ) ;
const id = delinkifiedName . trim ( ) . toLowerCase ( ) . replace ( /\s/g , '-' ) . replace ( /[^-0-9a-zа -яё]/ig , '' ) ;
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 23:21:45 +03:00
id : dedupId ,
offset ,
2019-11-19 05:18:28 +03:00
} ) ;
}
2020-01-22 23:21:45 +03:00
return tocEntries ;
}
2020-03-19 01:34:53 +03:00
/ * *
* @ param { string } text
* /
2020-04-24 05:52:06 +03:00
function autocorrectInvalidLinks ( projectRoot , sources , allowedFilePaths ) {
const pathToHashLinks = new Map ( ) ;
2020-03-19 01:34:53 +03:00
for ( const source of sources ) {
const text = source . text ( ) ;
2020-04-24 05:52:06 +03:00
const hashLinks = new Set ( getTOCEntriesForText ( text ) . map ( entry => entry . id ) ) ;
pathToHashLinks . set ( source . filePath ( ) , hashLinks ) ;
}
const messages = [ ] ;
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-19 01:34:53 +03:00
}
2020-04-24 05:52:06 +03:00
const sourceEdits = new SourceEdits ( source ) ;
let offset = 0 ;
const edits = [ ] ;
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 ) {
messages . push ( Message . error ( ` Bad link in ${ source . projectPath ( ) } : ${ lineNumber + 1 } : file ${ relativePath } does not exist ` ) ) ;
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 {
messages . push ( Message . error ( ` Bad link in ${ source . projectPath ( ) } : ${ lineNumber + 1 } : hash "# ${ hash } " does not exist in " ${ path . relative ( projectRoot , resolvedPath ) } " ` ) ) ;
}
}
offset += line . length ;
} ) ;
sourceEdits . commit ( messages ) ;
2020-03-19 01:34:53 +03:00
}
return messages ;
2020-04-24 05:52:06 +03: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 } ) ;
}
commit ( messages = [ ] ) {
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 ) {
messages . push ( Message . error ( 'INTERNAL ERROR: incorrect edit!' ) ) ;
return ;
}
}
for ( let i = 0 ; i < this . _edits . length - 1 ; ++ i ) {
if ( this . _edits [ i ] . to > this . _edits [ i + 1 ] . from ) {
messages . push ( Message . error ( 'INTERNAL ERROR: edits are overlapping!' ) ) ;
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-19 01:34:53 +03:00
}
2020-01-22 23:21:45 +03: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-19 05:18:28 +03:00
const minLevel = Math . min ( ... tocEntries . map ( entry => entry . level ) ) ;
tocEntries . forEach ( entry => entry . level -= minLevel ) ;
2020-01-22 23:21:45 +03:00
if ( topLevelOnly )
tocEntries = tocEntries . filter ( entry => ! entry . level ) ;
2019-11-19 05:18:28 +03: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 21:05:04 +03: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-19 01:34:53 +03:00
2020-04-24 05:52:06 +03:00
module . exports = { autocorrectInvalidLinks , runCommands } ;