Skip to content

Commit 5459be0

Browse files
aslushnikovdgozman
andauthored
cherry-pick(#18768): chore: verify tab groups in docs during lint (#18837)
This extracts the logic from playwright.dev so that we get early warnings. Co-authored-by: Dmitry Gozman <[email protected]>
1 parent 553a211 commit 5459be0

File tree

5 files changed

+109
-19
lines changed

5 files changed

+109
-19
lines changed

packages/playwright-core/types/types.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13501,7 +13501,6 @@ export interface APIRequest {
1350113501
* If you want API requests to not interfere with the browser cookies you should create a new [APIRequestContext] by
1350213502
* calling [apiRequest.newContext([options])](https://playwright.dev/docs/api/class-apirequest#api-request-new-context).
1350313503
* Such `APIRequestContext` object will have its own isolated cookie storage.
13504-
*
1350513504
*/
1350613505
export interface APIRequestContext {
1350713506
/**
@@ -14206,7 +14205,6 @@ export interface APIRequestContext {
1420614205
* [APIResponse] class represents responses returned by
1420714206
* [apiRequestContext.get(url[, options])](https://playwright.dev/docs/api/class-apirequestcontext#api-request-context-get)
1420814207
* and similar methods.
14209-
*
1421014208
*/
1421114209
export interface APIResponse {
1421214210
/**

utils/doclint/cli.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ async function run() {
8989

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

166165
// This validates member links.
167166
documentation.setLinkRenderer(() => undefined);
167+
// This validates code snippet groups in comments.
168+
documentation.setCodeGroupsTransformer(lang, tabs => tabs.map(tab => tab.spec));
169+
documentation.generateSourceCodeComments();
168170

169171
const relevantMarkdownFiles = new Set([...getAllMarkdownFiles(documentationRoot)
170172
// filter out language specific files
@@ -185,9 +187,12 @@ async function run() {
185187
if (langs.some(other => other !== lang && filePath.endsWith(`-${other}.md`)))
186188
continue;
187189
const data = fs.readFileSync(filePath, 'utf-8');
188-
const rootNode = md.filterNodesForLanguage(md.parse(data), lang);
190+
let rootNode = md.filterNodesForLanguage(md.parse(data), lang);
191+
// Validates code snippet groups.
192+
rootNode = md.processCodeGroups(rootNode, lang, tabs => tabs.map(tab => tab.spec));
193+
// Renders links.
189194
documentation.renderLinksInText(rootNode);
190-
// Validate links
195+
// Validate links.
191196
{
192197
md.visitAll(rootNode, node => {
193198
if (!node.text)

utils/doclint/documentation.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,23 @@ class Documentation {
165165
this._patchLinks?.(null, nodes);
166166
}
167167

168+
/**
169+
* @param {string} lang
170+
* @param {import('../markdown').CodeGroupTransformer} transformer
171+
*/
172+
setCodeGroupsTransformer(lang, transformer) {
173+
this._codeGroupsTransformer = { lang, transformer };
174+
}
175+
168176
generateSourceCodeComments() {
169-
for (const clazz of this.classesArray)
170-
clazz.visit(item => item.comment = generateSourceCodeComment(item.spec));
177+
for (const clazz of this.classesArray) {
178+
clazz.visit(item => {
179+
let spec = item.spec;
180+
if (spec && this._codeGroupsTransformer)
181+
spec = md.processCodeGroups(spec, this._codeGroupsTransformer.lang, this._codeGroupsTransformer.transformer);
182+
item.comment = generateSourceCodeComment(spec);
183+
});
184+
}
171185
}
172186

173187
clone() {
@@ -814,8 +828,6 @@ function patchLinks(classOrMember, spec, classesMap, membersMap, linkRenderer) {
814828
function generateSourceCodeComment(spec) {
815829
const comments = (spec || []).filter(n => !n.type.startsWith('h') && (n.type !== 'li' || n.liType !== 'default')).map(c => md.clone(c));
816830
md.visitAll(comments, node => {
817-
if (node.codeLang && node.codeLang.includes('tab=js-js'))
818-
node.type = 'null';
819831
if (node.type === 'li' && node.liType === 'bullet')
820832
node.liType = 'default';
821833
if (node.type === 'note') {

utils/generate_types/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

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

9292
const handledClasses = new Set();

utils/markdown.js

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949

5050
/** @typedef {MarkdownBaseNode & {
5151
* type: 'note',
52-
* text: string,
52+
* text: string,
5353
* noteType: string,
5454
* }} MarkdownNoteNode */
5555

@@ -62,6 +62,12 @@
6262
* lines: string[],
6363
* }} MarkdownPropsNode */
6464

65+
/** @typedef {{
66+
* value: string, groupId: string, spec: MarkdownNode
67+
* }} CodeGroup */
68+
69+
/** @typedef {function(CodeGroup[]): MarkdownNode[]} CodeGroupTransformer */
70+
6571
/** @typedef {MarkdownTextNode | MarkdownLiNode | MarkdownCodeNode | MarkdownNoteNode | MarkdownHeaderNode | MarkdownNullNode | MarkdownPropsNode } MarkdownNode */
6672

6773
function flattenWrappedLines(content) {
@@ -307,7 +313,7 @@ function innerRenderMdNode(indent, node, lastNode, result, maxColumns) {
307313
if (process.env.API_JSON_MODE)
308314
result.push(`${indent}\`\`\`${node.codeLang}`);
309315
else
310-
result.push(`${indent}\`\`\`${codeLangToHighlighter(node.codeLang)}`);
316+
result.push(`${indent}\`\`\`${node.codeLang ? parseCodeLang(node.codeLang).highlighter : ''}`);
311317
for (const line of node.lines)
312318
result.push(indent + line);
313319
result.push(`${indent}\`\`\``);
@@ -469,13 +475,82 @@ function filterNodesForLanguage(nodes, language) {
469475

470476
/**
471477
* @param {string} codeLang
472-
* @return {string}
478+
* @return {{ highlighter: string, language: string|undefined, codeGroup: string|undefined}}
473479
*/
474-
function codeLangToHighlighter(codeLang) {
475-
const [lang] = codeLang.split(' ');
476-
if (lang === 'python')
477-
return 'py';
478-
return lang;
480+
function parseCodeLang(codeLang) {
481+
if (codeLang === 'python async')
482+
return { highlighter: 'py', codeGroup: 'python-async', language: 'python' };
483+
if (codeLang === 'python sync')
484+
return { highlighter: 'py', codeGroup: 'python-sync', language: 'python' };
485+
486+
const [highlighter] = codeLang.split(' ');
487+
if (!highlighter)
488+
throw new Error(`Cannot parse code block lang: "${codeLang}"`);
489+
490+
const languageMatch = codeLang.match(/ lang=([\w\d]+)/);
491+
let language = languageMatch ? languageMatch[1] : undefined;
492+
if (!language) {
493+
if (highlighter === 'ts')
494+
language = 'js';
495+
else if (['js', 'python', 'csharp', 'java'].includes(highlighter))
496+
language = highlighter;
497+
}
498+
499+
const tabMatch = codeLang.match(/ tab=([\w\d-]+)/);
500+
return { highlighter, language, codeGroup: tabMatch ? tabMatch[1] : '' };
501+
}
502+
503+
/**
504+
* @param {MarkdownNode[]} spec
505+
* @param {string} language
506+
* @param {CodeGroupTransformer} transformer
507+
* @returns {MarkdownNode[]}
508+
*/
509+
function processCodeGroups(spec, language, transformer) {
510+
/** @type {MarkdownNode[]} */
511+
const newSpec = [];
512+
for (let i = 0; i < spec.length; ++i) {
513+
/** @type {{value: string, groupId: string, spec: MarkdownNode}[]} */
514+
const tabs = [];
515+
for (;i < spec.length; i++) {
516+
const codeLang = spec[i].codeLang;
517+
if (!codeLang)
518+
break;
519+
let parsed;
520+
try {
521+
parsed = parseCodeLang(codeLang);
522+
} catch (e) {
523+
throw new Error(e.message + '\n while processing:\n' + render([spec[i]]));
524+
}
525+
if (!parsed.codeGroup)
526+
break;
527+
if (parsed.language && parsed.language !== language)
528+
continue;
529+
const [groupId, value] = parsed.codeGroup.split('-');
530+
tabs.push({ groupId, value, spec: spec[i] });
531+
}
532+
if (tabs.length) {
533+
if (tabs.length === 1)
534+
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]));
535+
536+
// Validate group consistency.
537+
const groupId = tabs[0].groupId;
538+
const values = new Set();
539+
for (const tab of tabs) {
540+
if (tab.groupId !== groupId)
541+
throw new Error('Mixed group ids: ' + render(spec));
542+
if (values.has(tab.value))
543+
throw new Error(`Duplicated tab "${tab.value}"\n` + render(tabs.map(tab => tab.spec)));
544+
values.add(tab.value);
545+
}
546+
547+
// Append transformed nodes.
548+
newSpec.push(...transformer(tabs));
549+
}
550+
if (i < spec.length)
551+
newSpec.push(spec[i]);
552+
}
553+
return newSpec;
479554
}
480555

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

0 commit comments

Comments
 (0)