mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
* feat: remove memoryLimit option (#910) Co-authored-by: Cursor <[email protected]> * feat: add templateLimit, outputLengthLimit, and maxDepth DoS limits Enforce v11 resource guards in render and tags, fix for offset/else behavior, and update tutorials for Tag-class registration. Co-authored-by: Cursor <[email protected]> * docs: revert unnecessary tutorial churn from memoryLimit PR Restore the two-example register-filters-tags structure (Value + Hash) and undo unrelated constructor/emitter doc edits not required for DoS limits. Co-authored-by: Cursor <[email protected]> * docs: trim security-model prose and update render-tag-content Remove diary-style engine comparisons from security-model.md. Update render-tag-content tutorial to Tag class examples with tpls class field. Co-authored-by: Cursor <[email protected]> * docs: note maxDepth stack overflow applies to renderSync only Explain why async render does not need maxDepth for stack protection based on generator/toPromise driving. Co-authored-by: Cursor <[email protected]> * refactor: track maxDepth via depthLimit Limiter on Context Replace increaseDepth/decreaseDepth with a shared Limiter that supports paired use/release, matching templateLimit and outputLengthLimit patterns. Co-authored-by: Cursor <[email protected]> * fix: remove spurious diff noise in filter files Restore misc.ts from origin/next with LF line endings and re-apply only memoryLimit removal, avoiding CRLF and blank-line churn in the export block. Co-authored-by: Cursor <[email protected]> * refactor: minimize PR diff noise Co-authored-by: Cursor <[email protected]> * feat: cap strftime pad width at 1M docs: restructure security model with production guidance Co-authored-by: Cursor <[email protected]> * refactor: simplify depthLimit in partial tags and tighten security docs Drop try/finally around depthLimit in include, layout, and render; release at generator end. Consolidate production guidance in security-model.md. Fix padded-blocks lint in dos.spec.ts. Co-authored-by: Cursor <[email protected]> --------- Co-authored-by: Cursor <[email protected]>
161 lines
5.0 KiB
TypeScript
161 lines
5.0 KiB
TypeScript
import { changeCase, padStart, padEnd } from './underscore'
|
|
import { LiquidDate } from './liquid-date'
|
|
import { assert } from './assert'
|
|
|
|
/** Per-conversion numeric width cap for strftime (%N, %15d, …). */
|
|
export const MAX_STRFTIME_PAD = 1024 * 1024
|
|
|
|
const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/
|
|
interface FormatOptions {
|
|
flags: Record<string, boolean>;
|
|
width?: string;
|
|
modifier?: string;
|
|
}
|
|
|
|
// prototype extensions
|
|
function daysInMonth (d: LiquidDate) {
|
|
const feb = isLeapYear(d) ? 29 : 28
|
|
return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
}
|
|
function getDayOfYear (d: LiquidDate) {
|
|
let num = 0
|
|
for (let i = 0; i < d.getMonth(); ++i) {
|
|
num += daysInMonth(d)[i]
|
|
}
|
|
return num + d.getDate()
|
|
}
|
|
function getWeekOfYear (d: LiquidDate, startDay: number) {
|
|
// Skip to startDay of this week
|
|
const now = getDayOfYear(d) + (startDay - d.getDay())
|
|
// Find the first startDay of the year
|
|
const jan1 = new Date(d.getFullYear(), 0, 1)
|
|
const then = (7 - jan1.getDay() + startDay)
|
|
return String(Math.floor((now - then) / 7) + 1)
|
|
}
|
|
function isLeapYear (d: LiquidDate) {
|
|
const year = d.getFullYear()
|
|
return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year)))
|
|
}
|
|
function ordinal (d: LiquidDate) {
|
|
const date = d.getDate()
|
|
if ([11, 12, 13].includes(date)) return 'th'
|
|
|
|
switch (date % 10) {
|
|
case 1: return 'st'
|
|
case 2: return 'nd'
|
|
case 3: return 'rd'
|
|
default: return 'th'
|
|
}
|
|
}
|
|
function century (d: LiquidDate) {
|
|
return parseInt(d.getFullYear().toString().substring(0, 2), 10)
|
|
}
|
|
|
|
// default to 0
|
|
const padWidths: Record<string, number> = {
|
|
d: 2,
|
|
e: 2,
|
|
H: 2,
|
|
I: 2,
|
|
j: 3,
|
|
k: 2,
|
|
l: 2,
|
|
L: 3,
|
|
m: 2,
|
|
M: 2,
|
|
S: 2,
|
|
U: 2,
|
|
W: 2
|
|
}
|
|
|
|
const padSpaceChars = new Set('aAbBceklpP')
|
|
|
|
function getTimezoneOffset (d: LiquidDate, opts: FormatOptions) {
|
|
const nOffset = Math.abs(d.getTimezoneOffset())
|
|
const h = Math.floor(nOffset / 60)
|
|
const m = nOffset % 60
|
|
return (d.getTimezoneOffset() > 0 ? '-' : '+') +
|
|
padStart(h, 2, '0') +
|
|
(opts.flags[':'] ? ':' : '') +
|
|
padStart(m, 2, '0')
|
|
}
|
|
type FormatCodeHandler = (d: LiquidDate, opts: FormatOptions) => unknown
|
|
|
|
const formatCodes: Record<string, FormatCodeHandler> = {
|
|
a: (d: LiquidDate) => d.getShortWeekdayName(),
|
|
A: (d: LiquidDate) => d.getLongWeekdayName(),
|
|
b: (d: LiquidDate) => d.getShortMonthName(),
|
|
B: (d: LiquidDate) => d.getLongMonthName(),
|
|
c: (d: LiquidDate) => d.toLocaleString(),
|
|
C: (d: LiquidDate) => century(d),
|
|
d: (d: LiquidDate) => d.getDate(),
|
|
e: (d: LiquidDate) => d.getDate(),
|
|
H: (d: LiquidDate) => d.getHours(),
|
|
I: (d: LiquidDate) => String(d.getHours() % 12 || 12),
|
|
j: (d: LiquidDate) => getDayOfYear(d),
|
|
k: (d: LiquidDate) => d.getHours(),
|
|
l: (d: LiquidDate) => String(d.getHours() % 12 || 12),
|
|
L: (d: LiquidDate) => d.getMilliseconds(),
|
|
m: (d: LiquidDate) => d.getMonth() + 1,
|
|
M: (d: LiquidDate) => d.getMinutes(),
|
|
N: (d: LiquidDate, opts: FormatOptions) => {
|
|
const width = Number(opts.width) || 9
|
|
assertPadWidth(width)
|
|
const str = String(d.getMilliseconds()).slice(0, width)
|
|
return padEnd(str, width, '0')
|
|
},
|
|
p: (d: LiquidDate) => (d.getHours() < 12 ? 'AM' : 'PM'),
|
|
P: (d: LiquidDate) => (d.getHours() < 12 ? 'am' : 'pm'),
|
|
q: (d: LiquidDate) => ordinal(d),
|
|
s: (d: LiquidDate) => Math.round(d.getTime() / 1000),
|
|
S: (d: LiquidDate) => d.getSeconds(),
|
|
u: (d: LiquidDate) => d.getDay() || 7,
|
|
U: (d: LiquidDate) => getWeekOfYear(d, 0),
|
|
w: (d: LiquidDate) => d.getDay(),
|
|
W: (d: LiquidDate) => getWeekOfYear(d, 1),
|
|
x: (d: LiquidDate) => d.toLocaleDateString(),
|
|
X: (d: LiquidDate) => d.toLocaleTimeString(),
|
|
y: (d: LiquidDate) => d.getFullYear().toString().slice(2, 4),
|
|
Y: (d: LiquidDate) => d.getFullYear(),
|
|
z: getTimezoneOffset,
|
|
Z: (d: LiquidDate, opts: FormatOptions) => d.getTimeZoneName() || getTimezoneOffset(d, opts),
|
|
't': () => '\t',
|
|
'n': () => '\n',
|
|
'%': () => '%'
|
|
}
|
|
formatCodes.h = formatCodes.b
|
|
|
|
export function strftime (d: LiquidDate, formatStr: string) {
|
|
let output = ''
|
|
let remaining = formatStr
|
|
let match
|
|
while ((match = rFormat.exec(remaining))) {
|
|
output += remaining.slice(0, match.index)
|
|
remaining = remaining.slice(match.index + match[0].length)
|
|
output += format(d, match)
|
|
}
|
|
return output + remaining
|
|
}
|
|
|
|
function assertPadWidth (width: number) {
|
|
assert(width <= MAX_STRFTIME_PAD, 'strftime pad width limit exceeded')
|
|
}
|
|
|
|
function format (d: LiquidDate, match: RegExpExecArray) {
|
|
const [input, flagStr = '', width, modifier, conversion] = match
|
|
const convert = formatCodes[conversion]
|
|
if (!convert) return input
|
|
const flags: Record<string, boolean> = {}
|
|
for (const flag of flagStr) flags[flag] = true
|
|
let ret = String(convert(d, { flags, width, modifier }))
|
|
let padChar = padSpaceChars.has(conversion) ? ' ' : '0'
|
|
let padWidth = Number(width) || padWidths[conversion] || 0
|
|
if (flags['^']) ret = ret.toUpperCase()
|
|
else if (flags['#']) ret = changeCase(ret)
|
|
if (flags['_']) padChar = ' '
|
|
else if (flags['0']) padChar = '0'
|
|
if (flags['-']) padWidth = 0
|
|
else assertPadWidth(padWidth)
|
|
return padStart(ret, padWidth, padChar)
|
|
}
|