Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: verify tab groups in docs during lint #18768

Merged
merged 1 commit into from Nov 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/playwright-core/types/types.d.ts
Expand Up @@ -13483,7 +13483,6 @@ export interface APIRequest {
* If you want API requests to not interfere with the browser cookies you should create a new [APIRequestContext] by
* calling [apiRequest.newContext([options])](https://playwright.dev/docs/api/class-apirequest#api-request-new-context).
* Such `APIRequestContext` object will have its own isolated cookie storage.
*
*/
export interface APIRequestContext {
/**
Expand Down Expand Up @@ -14188,7 +14187,6 @@ export interface APIRequestContext {
* [APIResponse] class represents responses returned by
* [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get)
* and similar methods.
*
*/
export interface APIResponse {
/**
Expand Down
11 changes: 8 additions & 3 deletions utils/doclint/cli.js
Expand Up @@ -89,7 +89,6 @@ async function run() {

// Patch docker version in docs
{
const regex = new RegExp("(mcr.microsoft.com/playwright[^: ]*):?([^ ]*)");
for (const filePath of getAllMarkdownFiles(path.join(PROJECT_DIR, 'docs'))) {
let content = fs.readFileSync(filePath).toString();
content = content.replace(new RegExp('(mcr.microsoft.com/playwright[^:]*):([\\w\\d-.]+)', 'ig'), (match, imageName, imageVersion) => {
Expand Down Expand Up @@ -165,6 +164,9 @@ async function run() {

// This validates member links.
documentation.setLinkRenderer(() => undefined);
// This validates code snippet groups in comments.
documentation.setCodeGroupsTransformer(lang, tabs => tabs.map(tab => tab.spec));
documentation.generateSourceCodeComments();

const relevantMarkdownFiles = new Set([...getAllMarkdownFiles(documentationRoot)
// filter out language specific files
Expand All @@ -185,9 +187,12 @@ async function run() {
if (langs.some(other => other !== lang && filePath.endsWith(`-${other}.md`)))
continue;
const data = fs.readFileSync(filePath, 'utf-8');
const rootNode = md.filterNodesForLanguage(md.parse(data), lang);
let rootNode = md.filterNodesForLanguage(md.parse(data), lang);
// Validates code snippet groups.
rootNode = md.processCodeGroups(rootNode, lang, tabs => tabs.map(tab => tab.spec));
// Renders links.
documentation.renderLinksInText(rootNode);
// Validate links
// Validate links.
{
md.visitAll(rootNode, node => {
if (!node.text)
Expand Down
20 changes: 16 additions & 4 deletions utils/doclint/documentation.js
Expand Up @@ -165,9 +165,23 @@ class Documentation {
this._patchLinks?.(null, nodes);
}

/**
* @param {string} lang
* @param {import('../markdown').CodeGroupTransformer} transformer
*/
setCodeGroupsTransformer(lang, transformer) {
this._codeGroupsTransformer = { lang, transformer };
}

generateSourceCodeComments() {
for (const clazz of this.classesArray)
clazz.visit(item => item.comment = generateSourceCodeComment(item.spec));
for (const clazz of this.classesArray) {
clazz.visit(item => {
let spec = item.spec;
if (spec && this._codeGroupsTransformer)
spec = md.processCodeGroups(spec, this._codeGroupsTransformer.lang, this._codeGroupsTransformer.transformer);
item.comment = generateSourceCodeComment(spec);
});
}
}

clone() {
Expand Down Expand Up @@ -814,8 +828,6 @@ function patchLinks(classOrMember, spec, classesMap, membersMap, linkRenderer) {
function generateSourceCodeComment(spec) {
const comments = (spec || []).filter(n => !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c));
md.visitAll(comments, node => {
if (node.codeLang && node.codeLang.includes('tab=js-js'))
node.type = 'null';
if (node.type === 'li' && node.liType === 'bullet')
node.liType = 'default';
if (node.type === 'note') {
Expand Down
2 changes: 1 addition & 1 deletion utils/generate_types/index.js
Expand Up @@ -16,7 +16,6 @@

//@ts-check
const path = require('path');
const os = require('os');
const toKebabCase = require('lodash/kebabCase')
const devices = require('../../packages/playwright-core/lib/server/deviceDescriptors');
const Documentation = require('../doclint/documentation');
Expand Down Expand Up @@ -87,6 +86,7 @@ class TypesGenerator {
return createMarkdownLink(member, `${className}${member.alias}`);
throw new Error('Unknown member kind ' + member.kind);
});
this.documentation.setCodeGroupsTransformer('js', tabs => tabs.filter(tab => tab.value === 'ts').map(tab => tab.spec));
this.documentation.generateSourceCodeComments();

const handledClasses = new Set();
Expand Down
93 changes: 84 additions & 9 deletions utils/markdown.js
Expand Up @@ -49,7 +49,7 @@

/** @typedef {MarkdownBaseNode & {
* type: 'note',
* text: string,
* text: string,
* noteType: string,
* }} MarkdownNoteNode */

Expand All @@ -62,6 +62,12 @@
* lines: string[],
* }} MarkdownPropsNode */

/** @typedef {{
* value: string, groupId: string, spec: MarkdownNode
* }} CodeGroup */

/** @typedef {function(CodeGroup[]): MarkdownNode[]} CodeGroupTransformer */

/** @typedef {MarkdownTextNode | MarkdownLiNode | MarkdownCodeNode | MarkdownNoteNode | MarkdownHeaderNode | MarkdownNullNode | MarkdownPropsNode } MarkdownNode */

function flattenWrappedLines(content) {
Expand Down Expand Up @@ -307,7 +313,7 @@ function innerRenderMdNode(indent, node, lastNode, result, maxColumns) {
if (process.env.API_JSON_MODE)
result.push(`${indent}\`\`\`${node.codeLang}`);
else
result.push(`${indent}\`\`\`${codeLangToHighlighter(node.codeLang)}`);
result.push(`${indent}\`\`\`${node.codeLang ? parseCodeLang(node.codeLang).highlighter : ''}`);
for (const line of node.lines)
result.push(indent + line);
result.push(`${indent}\`\`\``);
Expand Down Expand Up @@ -469,13 +475,82 @@ function filterNodesForLanguage(nodes, language) {

/**
* @param {string} codeLang
* @return {string}
* @return {{ highlighter: string, language: string|undefined, codeGroup: string|undefined}}
*/
function codeLangToHighlighter(codeLang) {
const [lang] = codeLang.split(' ');
if (lang === 'python')
return 'py';
return lang;
function parseCodeLang(codeLang) {
if (codeLang === 'python async')
return { highlighter: 'py', codeGroup: 'python-async', language: 'python' };
if (codeLang === 'python sync')
return { highlighter: 'py', codeGroup: 'python-sync', language: 'python' };

const [highlighter] = codeLang.split(' ');
if (!highlighter)
throw new Error(`Cannot parse code block lang: "${codeLang}"`);

const languageMatch = codeLang.match(/ lang=([\w\d]+)/);
let language = languageMatch ? languageMatch[1] : undefined;
if (!language) {
if (highlighter === 'ts')
language = 'js';
else if (['js', 'python', 'csharp', 'java'].includes(highlighter))
language = highlighter;
}

const tabMatch = codeLang.match(/ tab=([\w\d-]+)/);
return { highlighter, language, codeGroup: tabMatch ? tabMatch[1] : '' };
}

/**
* @param {MarkdownNode[]} spec
* @param {string} language
* @param {CodeGroupTransformer} transformer
* @returns {MarkdownNode[]}
*/
function processCodeGroups(spec, language, transformer) {
/** @type {MarkdownNode[]} */
const newSpec = [];
for (let i = 0; i < spec.length; ++i) {
/** @type {{value: string, groupId: string, spec: MarkdownNode}[]} */
const tabs = [];
for (;i < spec.length; i++) {
const codeLang = spec[i].codeLang;
if (!codeLang)
break;
let parsed;
try {
parsed = parseCodeLang(codeLang);
} catch (e) {
throw new Error(e.message + '\n while processing:\n' + render([spec[i]]));
}
if (!parsed.codeGroup)
break;
if (parsed.language && parsed.language !== language)
continue;
const [groupId, value] = parsed.codeGroup.split('-');
tabs.push({ groupId, value, spec: spec[i] });
}
if (tabs.length) {
if (tabs.length === 1)
throw new Error(`Lonely tab "${tabs[0].spec.codeLang}". Make sure there are at least two tabs in the group.\n` + render([tabs[0].spec]));

// Validate group consistency.
const groupId = tabs[0].groupId;
const values = new Set();
for (const tab of tabs) {
if (tab.groupId !== groupId)
throw new Error('Mixed group ids: ' + render(spec));
if (values.has(tab.value))
throw new Error(`Duplicated tab "${tab.value}"\n` + render(tabs.map(tab => tab.spec)));
values.add(tab.value);
}

// Append transformed nodes.
newSpec.push(...transformer(tabs));
}
if (i < spec.length)
newSpec.push(spec[i]);
}
return newSpec;
}

module.exports = { parse, render, clone, visitAll, visit, generateToc, filterNodesForLanguage, codeLangToHighlighter };
module.exports = { parse, render, clone, visitAll, visit, generateToc, filterNodesForLanguage, parseCodeLang, processCodeGroups };