Skip to content

Commit 8da96e6

Browse files
committed
Merge branch 'main' into chore/karma-tests-migration
2 parents 24c362a + 744b783 commit 8da96e6

File tree

5 files changed

+204
-10
lines changed

5 files changed

+204
-10
lines changed

src/compiler/types/tests/generate-prop-types.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,31 @@ describe('generate-prop-types', () => {
245245

246246
expect(actualTypeInfo).toEqual(expectedTypeInfo);
247247
});
248+
249+
it('escapes "*/" in default values within jsdoc', () => {
250+
const stubImportTypes = stubTypesImportData();
251+
const componentMeta = stubComponentCompilerMeta({
252+
properties: [
253+
stubComponentCompilerProperty({
254+
defaultValue: "'*/*'",
255+
}),
256+
],
257+
});
258+
259+
const expectedTypeInfo: d.TypeInfo = [
260+
{
261+
jsdoc: "@default '*\\/*'", // Editor will evaluate this as @default '*\/*'
262+
internal: false,
263+
name: 'propName',
264+
optional: false,
265+
required: false,
266+
type: 'UserCustomPropType',
267+
},
268+
];
269+
270+
const actualTypeInfo = generatePropTypes(componentMeta, stubImportTypes);
271+
272+
expect(actualTypeInfo).toEqual(expectedTypeInfo);
273+
});
248274
});
249275
});

src/utils/helpers.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,39 @@ export const toCamelCase = (str: string) => {
4747
*/
4848
export const toTitleCase = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1);
4949

50+
/**
51+
* Escapes all occurrences of a specified pattern in a string.
52+
* @description This function replaces all matches of a given pattern in the input text with a specified replacement string.
53+
* It can handle both string and regular expression patterns and allows toggling between global and single-match replacements.
54+
* @param {string} text - The input string to process.
55+
* @param {RegExp | string} pattern - The pattern to search for in the input string. Can be a regular expression or a string.
56+
* @param {string} replacement - The string to replace each match with.
57+
* @param {boolean} [replaceAll=true] - Whether to replace all occurrences (`true`) or just the first occurrence (`false`). Defaults to `true`.
58+
* @returns {string} The processed string with the replacements applied.
59+
* @example
60+
* @see src\utils\util.ts
61+
*/
62+
export const escapeWithPattern = (
63+
text: string,
64+
pattern: RegExp | string,
65+
replacement: string,
66+
replaceAll: boolean = true,
67+
): string => {
68+
let regex: RegExp;
69+
70+
if (typeof pattern === 'string') {
71+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
72+
regex = new RegExp(escaped, replaceAll ? 'g' : '');
73+
} else {
74+
const flags = pattern.flags;
75+
const hasG = flags.includes('g');
76+
const newFlags = replaceAll ? (hasG ? flags : flags + 'g') : hasG ? flags.replace(/g/g, '') : flags;
77+
regex = new RegExp(pattern.source, newFlags);
78+
}
79+
80+
return text.replace(regex, replacement);
81+
};
82+
5083
/**
5184
* This is just a no-op, don't expect it to do anything.
5285
*/

src/utils/test/helpers.spec.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { dashToPascalCase, isDef, mergeIntoWith, toCamelCase, toDashCase } from '../helpers';
1+
import { dashToPascalCase, escapeWithPattern, isDef, mergeIntoWith, toCamelCase, toDashCase } from '../helpers';
22

33
describe('util helpers', () => {
44
describe('dashToPascalCase', () => {
@@ -107,4 +107,63 @@ describe('util helpers', () => {
107107
]);
108108
});
109109
});
110+
111+
describe('escapeWithPattern', () => {
112+
it('replaces all occurrences of a string pattern by default', () => {
113+
const text = 'foo/bar foo/bar foo/bar';
114+
const pattern = '/';
115+
const replacement = '\\/';
116+
expect(escapeWithPattern(text, pattern, replacement)).toBe('foo\\/bar foo\\/bar foo\\/bar');
117+
});
118+
119+
it('replaces only first occurrence if replaceAll is false', () => {
120+
const text = 'foo/bar foo/bar foo/bar';
121+
const pattern = '/';
122+
const replacement = '\\/';
123+
expect(escapeWithPattern(text, pattern, replacement, false)).toBe('foo\\/bar foo/bar foo/bar');
124+
});
125+
126+
it('replaces all occurrences using a RegExp pattern with no g flag by default', () => {
127+
const text = 'a+b+c+a+b+c';
128+
const pattern = /\+/; // no 'g' flag
129+
const replacement = '-';
130+
expect(escapeWithPattern(text, pattern, replacement)).toBe('a-b-c-a-b-c');
131+
});
132+
133+
it('replaces only first occurrence if replaceAll is false with RegExp', () => {
134+
const text = 'a+b+c+a+b+c';
135+
const pattern = /\+/;
136+
const replacement = '-';
137+
expect(escapeWithPattern(text, pattern, replacement, false)).toBe('a-b+c+a+b+c');
138+
});
139+
140+
it('respects the g flag if RegExp already has it and replaceAll true', () => {
141+
const text = 'x*y*z*x*y*z';
142+
const pattern = /\*/g;
143+
const replacement = '-';
144+
expect(escapeWithPattern(text, pattern, replacement, true)).toBe('x-y-z-x-y-z');
145+
});
146+
147+
it('removes the g flag if replaceAll is false', () => {
148+
const text = 'x*y*z*x*y*z';
149+
const pattern = /\*/g;
150+
const replacement = '-';
151+
expect(escapeWithPattern(text, pattern, replacement, false)).toBe('x-y*z*x*y*z');
152+
});
153+
154+
it('escapes special RegExp chars in string pattern', () => {
155+
const text = 'foo.*+?^${}()|[]\\bar';
156+
const pattern = '.*+?^${}()|[]\\';
157+
const replacement = '-ESCAPED-';
158+
expect(escapeWithPattern(text, pattern, replacement)).toBe('foo-ESCAPED-bar');
159+
});
160+
161+
it('works with empty string input', () => {
162+
expect(escapeWithPattern('', 'a', 'b')).toBe('');
163+
});
164+
165+
it('works with empty replacement', () => {
166+
expect(escapeWithPattern('abcabc', 'a', '')).toBe('bcbc');
167+
});
168+
});
110169
});

src/utils/test/util.spec.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { mockBuildCtx, mockValidatedConfig } from '@stencil/core/testing';
22
import * as util from '@utils';
33

4+
import { getTextDocs } from '@utils';
45
import type * as d from '../../declarations';
56
import { stubDiagnostic } from '../../dev-server/test/Diagnostic.stub';
67

@@ -298,4 +299,72 @@ interface Foo extends Components.Foo, HTMLStencilElement {`);
298299
expect(util.isJsFile(fileName)).toEqual(true);
299300
});
300301
});
302+
303+
describe('getTextDocs', () => {
304+
let docs: d.CompilerJsDoc;
305+
306+
beforeEach(() => {
307+
docs = {
308+
tags: [],
309+
text: '',
310+
};
311+
});
312+
it('returns empty string for null or undefined', () => {
313+
expect(getTextDocs(null)).toBe('');
314+
expect(getTextDocs(undefined)).toBe('');
315+
});
316+
317+
it('returns only main text if no tags', () => {
318+
docs = {
319+
text: 'Some doc text.',
320+
tags: [],
321+
};
322+
expect(getTextDocs(docs)).toBe('Some doc text.');
323+
});
324+
325+
it('replaces line breaks in main text with spaces', () => {
326+
docs = {
327+
text: 'Line 1\nLine 2\r\nLine 3',
328+
tags: [],
329+
};
330+
expect(getTextDocs(docs)).toBe('Line 1 Line 2 Line 3');
331+
});
332+
333+
it('filters out internal tags and escapes "*/" in text and tags', () => {
334+
docs = {
335+
text: 'This text ends with */ sequence.',
336+
tags: [
337+
{ name: 'internal', text: 'should be removed' },
338+
{ name: 'default', text: "'*/*'" },
339+
{ name: 'deprecated', text: 'Use something else' },
340+
],
341+
};
342+
343+
const result = getTextDocs(docs);
344+
345+
// Should replace "*/" with "*\/" (single backslash)
346+
expect(result).toContain('This text ends with *\\/ sequence.');
347+
348+
// @internal tag filtered out
349+
expect(result).not.toContain('@internal');
350+
351+
// @default and @deprecated tags included and escaped
352+
expect(result).toContain("@default '*\\/*'");
353+
expect(result).toContain('@deprecated Use something else');
354+
355+
// Main text and tags separated by new lines
356+
const lines = result.split('\n');
357+
expect(lines[0]).toBe('This text ends with *\\/ sequence.');
358+
expect(lines).toContain("@default '*\\/*'");
359+
expect(lines).toContain('@deprecated Use something else');
360+
});
361+
362+
it('trims the result', () => {
363+
docs = {
364+
text: ' Some text with spaces ',
365+
tags: [],
366+
};
367+
expect(getTextDocs(docs)).toBe('Some text with spaces');
368+
});
369+
});
301370
});

src/utils/util.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type * as d from '../declarations';
2-
import { dashToPascalCase, isString, toDashCase } from './helpers';
2+
import { dashToPascalCase, escapeWithPattern, isString, toDashCase } from './helpers';
33
import { buildError } from './message-utils';
44

55
/**
@@ -8,6 +8,8 @@ import { buildError } from './message-utils';
88
*/
99
const SUPPRESSED_JSDOC_TAGS: ReadonlyArray<string> = ['virtualProp', 'slot', 'part', 'internal'];
1010

11+
const LINE_BREAK_REGEX: Readonly<RegExp> = /\r?\n|\r/g;
12+
1113
/**
1214
* Create a stylistically-appropriate JS variable name from a filename
1315
*
@@ -111,16 +113,21 @@ export const generatePreamble = (config: d.ValidatedConfig): string => {
111113
return preambleComment.join('\n');
112114
};
113115

114-
const lineBreakRegex = /\r?\n|\r/g;
115116
export function getTextDocs(docs: d.CompilerJsDoc | undefined | null) {
116117
if (docs == null) {
117118
return '';
118119
}
119-
return `${docs.text.replace(lineBreakRegex, ' ')}
120-
${docs.tags
121-
.filter((tag) => tag.name !== 'internal')
122-
.map((tag) => `@${tag.name} ${(tag.text || '').replace(lineBreakRegex, ' ')}`)
123-
.join('\n')}`.trim();
120+
121+
const mainText = escapeWithPattern(docs.text.replace(LINE_BREAK_REGEX, ' '), /\*\//, '*\\/', true);
122+
123+
const tags = docs.tags
124+
.filter((tag) => tag.name !== 'internal')
125+
.map((tag) => {
126+
const tagText = escapeWithPattern((tag.text || '').replace(LINE_BREAK_REGEX, ' '), /\*\//, '*\\/', true);
127+
return `@${tag.name} ${tagText}`;
128+
});
129+
130+
return [mainText, ...tags].join('\n').trim();
124131
}
125132

126133
/**
@@ -163,10 +170,10 @@ function formatDocBlock(docs: d.CompilerJsDoc, indentation: number = 0): string
163170
*/
164171
function getDocBlockLines(docs: d.CompilerJsDoc): string[] {
165172
return [
166-
...docs.text.split(lineBreakRegex),
173+
...docs.text.split(LINE_BREAK_REGEX),
167174
...docs.tags
168175
.filter((tag) => !SUPPRESSED_JSDOC_TAGS.includes(tag.name))
169-
.map((tag) => `@${tag.name} ${tag.text || ''}`.split(lineBreakRegex)),
176+
.map((tag) => `@${tag.name} ${tag.text || ''}`.split(LINE_BREAK_REGEX)),
170177
]
171178
.flat()
172179
.filter(Boolean);

0 commit comments

Comments
 (0)