mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	As of today, we have tooling in place that makes sure that our main README.md **roughly** represents the state of the last release: - browser versions and browser badges are those that we released last - links to our API point to the last-released API version This tooling, however, relies on the fact that every release is a sequence of two consecutive commits: - one commit that sets `package.json` version to a released version - the following that "bumps" version to `-post`. This release process is very unfortunate, because: - it made releasing from branch impossible - it required "freezing" commits to the master branch This patch removes all the tooling and transitions `README.md` to always represent tip-of-tree state. We will fully rely on `https://playwright.dev` to show versioned docs.
		
			
				
	
	
		
			181 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			181 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/**
 | 
						||
 * 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.
 | 
						||
 */
 | 
						||
 | 
						||
const Message = require('../Message');
 | 
						||
 | 
						||
function runCommands(sources, {libversion, chromiumVersion, firefoxVersion}) {
 | 
						||
  // Release version is everything that doesn't include "-".
 | 
						||
  const isReleaseVersion = !libversion.includes('-');
 | 
						||
 | 
						||
  const messages = [];
 | 
						||
  const commands = [];
 | 
						||
  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;
 | 
						||
 | 
						||
    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;
 | 
						||
      }
 | 
						||
      const name = start[1];
 | 
						||
      const from = commandStartRegex.lastIndex;
 | 
						||
      const to = end.index;
 | 
						||
      const originalText = text.substring(from, to);
 | 
						||
      commands.push({name, from, to, originalText, source});
 | 
						||
      commandStartRegex.lastIndex = commandEndRegex.lastIndex;
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  const changedSources = new Set();
 | 
						||
  // Iterate commands in reverse order so that edits don't conflict.
 | 
						||
  commands.sort((a, b) => b.from - a.from);
 | 
						||
  for (const command of commands) {
 | 
						||
    let newText = null;
 | 
						||
    if (command.name === 'version')
 | 
						||
      newText = isReleaseVersion ? 'v' + libversion : 'Tip-Of-Tree';
 | 
						||
    else if (command.name === 'chromium-version')
 | 
						||
      newText = chromiumVersion;
 | 
						||
    else if (command.name === 'firefox-version')
 | 
						||
      newText = firefoxVersion;
 | 
						||
    else if (command.name === 'chromium-version-badge')
 | 
						||
      newText = `[](https://www.chromium.org/Home)`;
 | 
						||
    else if (command.name === 'firefox-version-badge')
 | 
						||
      newText = `[](https://www.mozilla.org/en-US/firefox/new/)`;
 | 
						||
    else if (command.name === 'toc')
 | 
						||
      newText = generateTableOfContents(command.source.text(), command.to, false /* topLevelOnly */);
 | 
						||
    else if (command.name === 'toc-top-level')
 | 
						||
      newText = generateTableOfContents(command.source.text(), command.to, true /* topLevelOnly */);
 | 
						||
    else if (command.name.startsWith('toc-extends-'))
 | 
						||
      newText = generateTableOfContentsForSuperclass(command.source.text(), 'class: ' + command.name.substring('toc-extends-'.length));
 | 
						||
    if (newText === null)
 | 
						||
      messages.push(Message.error(`Unknown command 'gen:${command.name}'`));
 | 
						||
    else if (applyCommand(command, newText))
 | 
						||
      changedSources.add(command.source);
 | 
						||
  }
 | 
						||
  for (const source of changedSources)
 | 
						||
    messages.push(Message.warning(`GEN: updated ${source.projectPath()}`));
 | 
						||
  return messages;
 | 
						||
};
 | 
						||
 | 
						||
/**
 | 
						||
 * @param {{name: string, from: number, to: number, source: !Source}} command
 | 
						||
 * @param {string} editText
 | 
						||
 * @return {boolean}
 | 
						||
 */
 | 
						||
function applyCommand(command, editText) {
 | 
						||
  const text = command.source.text();
 | 
						||
  const newText = text.substring(0, command.from) + editText + text.substring(command.to);
 | 
						||
  return command.source.setText(newText);
 | 
						||
}
 | 
						||
 | 
						||
function getTOCEntriesForText(text) {
 | 
						||
  const ids = new Set();
 | 
						||
  const titles = [];
 | 
						||
  let insideCodeBlock = false;
 | 
						||
  let offset = 0;
 | 
						||
  text.split('\n').forEach((aLine, lineNumber) => {
 | 
						||
    const line = aLine.trim();
 | 
						||
    if (line.startsWith('```'))
 | 
						||
      insideCodeBlock = !insideCodeBlock;
 | 
						||
    else if (!insideCodeBlock && line.startsWith('#'))
 | 
						||
      titles.push({line, offset: offset + lineNumber});
 | 
						||
    offset += aLine.length;
 | 
						||
  });
 | 
						||
  let tocEntries = [];
 | 
						||
  for (const {line, offset} of titles) {
 | 
						||
    const [, nesting, name] = line.match(/^(#+)\s+(.*)$/);
 | 
						||
    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,
 | 
						||
      id: dedupId,
 | 
						||
      offset,
 | 
						||
    });
 | 
						||
  }
 | 
						||
  return tocEntries;
 | 
						||
}
 | 
						||
 | 
						||
/**
 | 
						||
 * @param {string} text
 | 
						||
 */
 | 
						||
function ensureInternalLinksAreValid(sources) {
 | 
						||
  const messages = [];
 | 
						||
  for (const source of sources) {
 | 
						||
    const text = source.text();
 | 
						||
    const availableLinks = new Set(getTOCEntriesForText(text).map(entry => entry.id));
 | 
						||
    const internalLinkRegex = /\]\(#([#\w\-]*)\)/g;
 | 
						||
    let match;
 | 
						||
    while ((match = internalLinkRegex.exec(text)) !== null) {
 | 
						||
      const link = match[1];
 | 
						||
      if (!availableLinks.has(link))
 | 
						||
        messages.push(Message.error(`Found invalid link: #${match[1]}`));
 | 
						||
    }
 | 
						||
  }
 | 
						||
  return messages;
 | 
						||
}
 | 
						||
 | 
						||
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);
 | 
						||
  }
 | 
						||
 | 
						||
  const minLevel = Math.min(...tocEntries.map(entry => entry.level));
 | 
						||
  tocEntries.forEach(entry => entry.level -= minLevel);
 | 
						||
  if (topLevelOnly)
 | 
						||
    tocEntries = tocEntries.filter(entry => !entry.level);
 | 
						||
  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';
 | 
						||
}
 | 
						||
 | 
						||
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;
 | 
						||
}
 | 
						||
 | 
						||
module.exports = {ensureInternalLinksAreValid, runCommands};
 |