Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "purestack-repo",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.18.1",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/purestack/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "purestack",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/purestack.mjs",
"types": "./dist/purestack.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/purestack/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const version: string = '1.1.7'
export const version: string = '1.1.8'

export * from '@purestack/ts-ssg'
2 changes: 1 addition & 1 deletion packages/ts-common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-common",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-common.mjs",
"types": "./dist/ts-common.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-components",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-components.mjs",
"types": "./dist/ts-components.d.mts",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest'
import { sanitizeEmailStyleValue } from './emailHtmlPolicy'

describe('sanitizeEmailStyleValue', () => {
it.each([
[' red ', 'red'],
['red!important', 'red'],
[' red \t!IMPORTANT \n', 'red'],
['red !important extra', 'red !important extra'],
['red ! important', 'red ! important'],
['!important', undefined],
])('normalizes %j to %j', (value, expected) => {
expect(sanitizeEmailStyleValue('color', value)).toBe(expected)
})

it('rejects oversized values containing long internal whitespace', () => {
const value = `red${' '.repeat(200_000)}x`
expect(sanitizeEmailStyleValue('color', value)).toBeUndefined()
})

it('applies the length limit after removing the important suffix', () => {
const value = 'a'.repeat(512)
expect(sanitizeEmailStyleValue('font-family', `${value} !important`)).toBe(
value,
)
expect(
sanitizeEmailStyleValue('font-family', `${value}a !important`),
).toBeUndefined()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,10 @@ export function normalizeEmailContentId(value: string) {
}

function stripImportant(value: string) {
return value.replace(/\s*!important\s*$/i, '').trim()
const trimmed = value.trim()
return trimmed.slice(-10).toLowerCase() === '!important'
? trimmed.slice(0, -10).trimEnd()
: trimmed
}

function isSafeCssValue(value: string) {
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-css/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-css",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-css.mjs",
"types": "./dist/ts-css.d.mts",
Expand Down
8 changes: 8 additions & 0 deletions packages/ts-css/src/hyphenize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ describe('hyphenizeCss', () => {
expect(hyphenizeCss('ID')).toBe('id') // all uppercase, no hyphen
})

it('handles long uppercase runs with and without an acronym boundary', () => {
const acronym = 'A'.repeat(200_000)
expect(hyphenizeCss(acronym)).toBe(acronym.toLowerCase())
expect(hyphenizeCss(`${acronym}Value`)).toBe(
`${acronym.toLowerCase()}-value`,
)
})

it('preserves existing kebab-case / hyphens', () => {
expect(hyphenizeCss('font-size')).toBe('font-size')
expect(hyphenizeCss('-moz-appearance')).toBe('-moz-appearance')
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-css/src/hyphenize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function hyphenizeCss(prop: string): string {
// 1. Split acronym boundaries like "XMLHttp" → "XML-Http"
// 2. Then split camelCase boundaries like "fooBar" → "foo-Bar"
const withSeparators = withOpera
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')

return withSeparators.toLowerCase()
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-html/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-html",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-html.mjs",
"types": "./dist/ts-html.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-minidom/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-minidom",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-minidom.mjs",
"types": "./dist/ts-minidom.d.mts",
Expand Down
20 changes: 20 additions & 0 deletions packages/ts-minidom/src/minidom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@ import { describe, expect, it } from 'vitest'
import { parseFragment } from './minidom'

describe('parseFragment', () => {
it.each([
['<', '<'],
['<', '<'],
['<', '<'],
['<', '<'],
['"', '"'],
[''', '''],
['&lt;&gt;&amp;&quot;&apos;', '<>&"\''],
['&#60;&#x3e;&#x1F600;', '<>😀'],
['&#60 &#x3e', '< >'],
['&#1114112;', '\uFFFD'],
['&unknown;', '&unknown;'],
])('decodes entities once in text and attributes: %j', (input, expected) => {
const fragment = parseFragment(`<p title="${input}">${input}</p>`)
const paragraph = fragment.firstElementChild
expect(paragraph?.textContent).toBe(expected)
expect(paragraph?.getAttribute('title')).toBe(expected)
expect(paragraph?.children.length).toBe(0)
})

it('throws when a duplicate attribute would hide meaningful information', () => {
expect(() =>
parseFragment('<div class="first" class="second"></div>'),
Expand Down
32 changes: 20 additions & 12 deletions packages/ts-minidom/src/minidom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1722,18 +1722,26 @@ function decodeEntities(value: string) {
if (code < 0 || code > 0x10ffff) return '\uFFFD'
return String.fromCodePoint(code)
}
return value
.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex: string) =>
safeCodePoint(Number.parseInt(hex, 16)),
)
.replace(/&#([0-9]+);?/g, (_, dec: string) =>
safeCodePoint(Number.parseInt(dec, 10)),
)
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
const namedEntities: Record<string, string> = {
lt: '<',
gt: '>',
amp: '&',
quot: '"',
apos: "'",
}
return value.replace(
/&#(?:x([0-9a-fA-F]+)|([0-9]+));?|&(lt|gt|amp|quot|apos);/g,
(
match: string,
hex: string | undefined,
dec: string | undefined,
named: string | undefined,
) => {
if (hex !== undefined) return safeCodePoint(Number.parseInt(hex, 16))
if (dec !== undefined) return safeCodePoint(Number.parseInt(dec, 10))
return namedEntities[named ?? ''] ?? match
},
)
}

function escapeText(value: string) {
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-page-scripts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-page-scripts",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-page-scripts.mjs",
"types": "./dist/ts-page-scripts.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-render/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-render",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-render.mjs",
"types": "./dist/ts-render.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-ssg-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "purestack-component-navigation",
"displayName": "PureStack Component Tools",
"description": "Component navigation, prop IntelliSense, lit-html syntax highlighting, and template formatting for Markdown, Regor MDX, and TypeScript templates.",
"version": "1.1.7",
"version": "1.1.8",
"private": true,
"publisher": "purestack",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-ssg/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-ssg",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-ssg.mjs",
"types": "./dist/ts-ssg.d.mts",
Expand Down
22 changes: 22 additions & 0 deletions packages/ts-ssg/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,28 @@ describe('resolveSiteConfig sitemap', () => {
expect(config.sitemap.robots.allow).toEqual(['/'])
})

it.each([
[' https://docs.example.com/// ', 'https://docs.example.com'],
['', ''],
[' ', ''],
['///', ''],
['https://docs.example.com/path', 'https://docs.example.com/path'],
[
`https://docs.example.com/${'/'.repeat(200_000)}x`,
`https://docs.example.com/${'/'.repeat(200_000)}x`,
],
[
`https://docs.example.com${'/'.repeat(200_000)}`,
'https://docs.example.com',
],
])('normalizes sitemap base URL case %#', (baseUrl, expected) => {
const config = resolveSiteConfig({
rootDir: process.cwd(),
sitemap: { baseUrl },
})
expect(config.sitemap.baseUrl).toBe(expected)
})

it('resolves robots directives from sitemap config', () => {
const config = resolveSiteConfig({
rootDir: process.cwd(),
Expand Down
5 changes: 3 additions & 2 deletions packages/ts-ssg/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,8 +825,9 @@ function resolveCrossOrigin(

function normalizeBaseUrl(value: string): string {
const normalized = value.trim()
if (normalized.length === 0) return ''
return normalized.replace(/\/+$/, '')
let end = normalized.length
while (end > 0 && normalized[end - 1] === '/') --end
return normalized.slice(0, end)
}

function resolveRobotsConfig(
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-ssg/src/config/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function getHead(config?: BasicHeadConfig) {
const head = createHead(getHeadConfig(merge(DEFAULTS, config))).push(
h('meta').attr({
name: 'generator',
content: 'PureStack v1.1.7',
content: 'PureStack v1.1.8',
}),
h('meta').attr({
name: 'color-scheme',
Expand Down
41 changes: 41 additions & 0 deletions packages/ts-ssg/src/navigation/nav-file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import fs from 'node:fs/promises'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { resolveNavigationConfig } from './config'
import { loadCustomNavigation } from './nav-file'

describe('navigation file roots', () => {
afterEach(() => vi.restoreAllMocks())

it.each([
[' ///guide/// ', 'guide'],
['reference/', 'docs/reference'],
['reference\\nested\\', 'docs/reference/nested'],
['///', '.'],
['.', 'docs'],
['', undefined],
[`guide${'/'.repeat(200_000)}x`, 'docs/guide/x'],
[`${'/'.repeat(200_000)}guide${'/'.repeat(200_000)}`, 'guide'],
])('normalizes root case %#', async (root, expected) => {
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify({ root }))
const navigation = await loadCustomNavigation(
'content',
new Set(['docs']),
resolveNavigationConfig(),
)
expect(navigation.docs.root).toBe(expected)
})

it.each(['../../outside', '/../../outside', '..\\..\\outside'])(
'rejects traversal root %j',
async (root) => {
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify({ root }))
await expect(
loadCustomNavigation(
'content',
new Set(['docs']),
resolveNavigationConfig(),
),
).rejects.toThrow('Navigation roots must stay inside the content root.')
},
)
})
12 changes: 10 additions & 2 deletions packages/ts-ssg/src/navigation/nav-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ async function readNavFile(
function normalizeNavRoot(value: unknown, folder: string) {
const root = resolveString(value)
if (!root) return undefined
const normalizedInput = root.replaceAll('\\', '/').replace(/^\/+|\/+$/g, '')
const normalizedInput = trimSlashes(root.replaceAll('\\', '/'))
const joined = root.trim().startsWith('/')
? normalizedInput
: path.posix.join(folder, normalizedInput)
const normalized =
joined === '.' ? '' : path.posix.normalize(joined).replace(/^\/+|\/+$/g, '')
joined === '.' ? '' : trimSlashes(path.posix.normalize(joined))
if (
normalized === '..' ||
normalized.startsWith('../') ||
Expand All @@ -90,6 +90,14 @@ function normalizeNavRoot(value: unknown, folder: string) {
return normalized
}

function trimSlashes(value: string) {
let start = 0
let end = value.length
while (start < end && value[start] === '/') ++start
while (end > start && value[end - 1] === '/') --end
return value.slice(start, end)
}

function normalizeNavItems(value: unknown, folder: string): InternalNavItem[] {
if (!Array.isArray(value)) return []
const items: InternalNavItem[] = []
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-style/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-style",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-style.mjs",
"types": "./dist/ts-style.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-svg-icons/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-svg-icons",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-svg-icons.mjs",
"types": "./dist/ts-svg-icons.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-util-node/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-util-node",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-util-node.mjs",
"types": "./dist/ts-util-node.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-util/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-util",
"version": "1.1.7",
"version": "1.1.8",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-util.mjs",
"types": "./dist/ts-util.d.mts",
Expand Down
Loading