@wdprlib/render 1.3.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -356,10 +356,76 @@ function normalizeCssValue(value) {
356
356
  result = result.replace(/[\s\u0000-\u001f\u007f-\u009f]/g, "");
357
357
  return result.toLowerCase();
358
358
  }
359
+ function isUrlAllowed(rawUrl) {
360
+ let url = rawUrl;
361
+ if (url.length >= 2) {
362
+ const first = url[0];
363
+ const last = url[url.length - 1];
364
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
365
+ url = url.slice(1, -1);
366
+ }
367
+ }
368
+ if (url === "")
369
+ return true;
370
+ if (url.startsWith("#"))
371
+ return true;
372
+ if (url.startsWith("./") || url.startsWith("../"))
373
+ return true;
374
+ if (url.startsWith("//"))
375
+ return true;
376
+ if (url.startsWith("/"))
377
+ return true;
378
+ if (url.startsWith("http://") || url.startsWith("https://"))
379
+ return true;
380
+ if (url.startsWith("data:image/")) {
381
+ const after = url.slice("data:image/".length);
382
+ const sep = Math.min(after.indexOf(";") === -1 ? after.length : after.indexOf(";"), after.indexOf(",") === -1 ? after.length : after.indexOf(","));
383
+ const mime = after.slice(0, sep);
384
+ if (mime === "png" || mime === "jpeg" || mime === "jpg" || mime === "gif" || mime === "webp") {
385
+ return true;
386
+ }
387
+ }
388
+ return false;
389
+ }
390
+ function* iterateUrls(normalized) {
391
+ let searchPos = 0;
392
+ while (searchPos < normalized.length) {
393
+ const idx = normalized.indexOf("url(", searchPos);
394
+ if (idx === -1)
395
+ return;
396
+ let depth = 1;
397
+ let quoteChar = null;
398
+ let i = idx + 4;
399
+ while (i < normalized.length && depth > 0) {
400
+ const ch = normalized[i];
401
+ if (quoteChar !== null) {
402
+ if (ch === quoteChar)
403
+ quoteChar = null;
404
+ } else if (ch === '"' || ch === "'") {
405
+ quoteChar = ch;
406
+ } else if (ch === "(") {
407
+ depth++;
408
+ } else if (ch === ")") {
409
+ depth--;
410
+ }
411
+ i++;
412
+ }
413
+ if (depth > 0) {
414
+ yield { inner: normalized.slice(idx + 4), malformed: true };
415
+ return;
416
+ }
417
+ yield { inner: normalized.slice(idx + 4, i - 1), malformed: false };
418
+ searchPos = i;
419
+ }
420
+ }
359
421
  function isDangerousCssValue(value) {
360
422
  const normalized = normalizeCssValue(value);
361
- if (normalized.includes("url("))
362
- return true;
423
+ for (const { inner, malformed } of iterateUrls(normalized)) {
424
+ if (malformed)
425
+ return true;
426
+ if (!isUrlAllowed(inner))
427
+ return true;
428
+ }
363
429
  if (normalized.includes("expression("))
364
430
  return true;
365
431
  if (normalized.includes("-moz-binding"))
@@ -370,21 +436,61 @@ function isDangerousCssValue(value) {
370
436
  return true;
371
437
  return false;
372
438
  }
439
+ function splitDeclarations(style) {
440
+ const out = [];
441
+ let buf = "";
442
+ let parenDepth = 0;
443
+ let quoteChar = null;
444
+ for (const ch of style) {
445
+ if (quoteChar !== null) {
446
+ buf += ch;
447
+ if (ch === quoteChar)
448
+ quoteChar = null;
449
+ continue;
450
+ }
451
+ if (ch === '"' || ch === "'") {
452
+ quoteChar = ch;
453
+ buf += ch;
454
+ continue;
455
+ }
456
+ if (ch === "(") {
457
+ parenDepth++;
458
+ buf += ch;
459
+ continue;
460
+ }
461
+ if (ch === ")") {
462
+ if (parenDepth > 0)
463
+ parenDepth--;
464
+ buf += ch;
465
+ continue;
466
+ }
467
+ if (ch === ";" && parenDepth === 0) {
468
+ out.push(buf);
469
+ buf = "";
470
+ continue;
471
+ }
472
+ buf += ch;
473
+ }
474
+ if (buf.length > 0)
475
+ out.push(buf);
476
+ return out;
477
+ }
373
478
  function sanitizeStyleValue(style) {
374
479
  const endsWithSemicolon = style.trimEnd().endsWith(";");
375
- const declarations = style.split(";").map((d) => d.trim()).filter(Boolean);
480
+ const declarations = splitDeclarations(style).map((d) => d.trim()).filter(Boolean);
376
481
  const safe = [];
377
482
  for (const decl of declarations) {
378
483
  const colonIdx = decl.indexOf(":");
379
484
  if (colonIdx === -1)
380
485
  continue;
381
- const property = decl.slice(0, colonIdx).trim().toLowerCase();
486
+ const property = decl.slice(0, colonIdx).trim();
382
487
  const value = decl.slice(colonIdx + 1).trim();
383
488
  if (isDangerousCssValue(value))
384
489
  continue;
385
- if (property.startsWith("-moz-binding"))
490
+ const normalisedProperty = normalizeCssValue(property);
491
+ if (normalisedProperty.startsWith("-moz-binding"))
386
492
  continue;
387
- if (property === "behavior")
493
+ if (normalisedProperty === "behavior")
388
494
  continue;
389
495
  safe.push(decl);
390
496
  }
@@ -833,7 +939,7 @@ function renderLink(ctx, data) {
833
939
  const attrs = [`href="${escapeAttr(href)}"`];
834
940
  if (data.type === "page" && typeof data.link === "object") {
835
941
  const page = data.link.page;
836
- const isSpecialPage = page.startsWith("//") || page.includes(":") || page.includes("#/");
942
+ const isSpecialPage = page.startsWith("//") || page.includes("#/");
837
943
  if (!isSpecialPage) {
838
944
  const hashIdx = page.indexOf("#");
839
945
  const pageToCheck = hashIdx !== -1 ? page.slice(0, hashIdx) : page;
@@ -4610,6 +4716,9 @@ function generateDefaultUrl(pageName, contents) {
4610
4716
  return path;
4611
4717
  }
4612
4718
  function renderHtmlBlock(ctx, data) {
4719
+ if (ctx.settings.allowHtmlBlocks === false) {
4720
+ return;
4721
+ }
4613
4722
  const index = ctx.nextHtmlBlockIndex();
4614
4723
  const pageName = ctx.page?.pageName ?? "";
4615
4724
  const callbackUrl = ctx.options.resolvers?.htmlBlockUrl?.(index);
package/dist/index.js CHANGED
@@ -304,10 +304,76 @@ function normalizeCssValue(value) {
304
304
  result = result.replace(/[\s\u0000-\u001f\u007f-\u009f]/g, "");
305
305
  return result.toLowerCase();
306
306
  }
307
+ function isUrlAllowed(rawUrl) {
308
+ let url = rawUrl;
309
+ if (url.length >= 2) {
310
+ const first = url[0];
311
+ const last = url[url.length - 1];
312
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
313
+ url = url.slice(1, -1);
314
+ }
315
+ }
316
+ if (url === "")
317
+ return true;
318
+ if (url.startsWith("#"))
319
+ return true;
320
+ if (url.startsWith("./") || url.startsWith("../"))
321
+ return true;
322
+ if (url.startsWith("//"))
323
+ return true;
324
+ if (url.startsWith("/"))
325
+ return true;
326
+ if (url.startsWith("http://") || url.startsWith("https://"))
327
+ return true;
328
+ if (url.startsWith("data:image/")) {
329
+ const after = url.slice("data:image/".length);
330
+ const sep = Math.min(after.indexOf(";") === -1 ? after.length : after.indexOf(";"), after.indexOf(",") === -1 ? after.length : after.indexOf(","));
331
+ const mime = after.slice(0, sep);
332
+ if (mime === "png" || mime === "jpeg" || mime === "jpg" || mime === "gif" || mime === "webp") {
333
+ return true;
334
+ }
335
+ }
336
+ return false;
337
+ }
338
+ function* iterateUrls(normalized) {
339
+ let searchPos = 0;
340
+ while (searchPos < normalized.length) {
341
+ const idx = normalized.indexOf("url(", searchPos);
342
+ if (idx === -1)
343
+ return;
344
+ let depth = 1;
345
+ let quoteChar = null;
346
+ let i = idx + 4;
347
+ while (i < normalized.length && depth > 0) {
348
+ const ch = normalized[i];
349
+ if (quoteChar !== null) {
350
+ if (ch === quoteChar)
351
+ quoteChar = null;
352
+ } else if (ch === '"' || ch === "'") {
353
+ quoteChar = ch;
354
+ } else if (ch === "(") {
355
+ depth++;
356
+ } else if (ch === ")") {
357
+ depth--;
358
+ }
359
+ i++;
360
+ }
361
+ if (depth > 0) {
362
+ yield { inner: normalized.slice(idx + 4), malformed: true };
363
+ return;
364
+ }
365
+ yield { inner: normalized.slice(idx + 4, i - 1), malformed: false };
366
+ searchPos = i;
367
+ }
368
+ }
307
369
  function isDangerousCssValue(value) {
308
370
  const normalized = normalizeCssValue(value);
309
- if (normalized.includes("url("))
310
- return true;
371
+ for (const { inner, malformed } of iterateUrls(normalized)) {
372
+ if (malformed)
373
+ return true;
374
+ if (!isUrlAllowed(inner))
375
+ return true;
376
+ }
311
377
  if (normalized.includes("expression("))
312
378
  return true;
313
379
  if (normalized.includes("-moz-binding"))
@@ -318,21 +384,61 @@ function isDangerousCssValue(value) {
318
384
  return true;
319
385
  return false;
320
386
  }
387
+ function splitDeclarations(style) {
388
+ const out = [];
389
+ let buf = "";
390
+ let parenDepth = 0;
391
+ let quoteChar = null;
392
+ for (const ch of style) {
393
+ if (quoteChar !== null) {
394
+ buf += ch;
395
+ if (ch === quoteChar)
396
+ quoteChar = null;
397
+ continue;
398
+ }
399
+ if (ch === '"' || ch === "'") {
400
+ quoteChar = ch;
401
+ buf += ch;
402
+ continue;
403
+ }
404
+ if (ch === "(") {
405
+ parenDepth++;
406
+ buf += ch;
407
+ continue;
408
+ }
409
+ if (ch === ")") {
410
+ if (parenDepth > 0)
411
+ parenDepth--;
412
+ buf += ch;
413
+ continue;
414
+ }
415
+ if (ch === ";" && parenDepth === 0) {
416
+ out.push(buf);
417
+ buf = "";
418
+ continue;
419
+ }
420
+ buf += ch;
421
+ }
422
+ if (buf.length > 0)
423
+ out.push(buf);
424
+ return out;
425
+ }
321
426
  function sanitizeStyleValue(style) {
322
427
  const endsWithSemicolon = style.trimEnd().endsWith(";");
323
- const declarations = style.split(";").map((d) => d.trim()).filter(Boolean);
428
+ const declarations = splitDeclarations(style).map((d) => d.trim()).filter(Boolean);
324
429
  const safe = [];
325
430
  for (const decl of declarations) {
326
431
  const colonIdx = decl.indexOf(":");
327
432
  if (colonIdx === -1)
328
433
  continue;
329
- const property = decl.slice(0, colonIdx).trim().toLowerCase();
434
+ const property = decl.slice(0, colonIdx).trim();
330
435
  const value = decl.slice(colonIdx + 1).trim();
331
436
  if (isDangerousCssValue(value))
332
437
  continue;
333
- if (property.startsWith("-moz-binding"))
438
+ const normalisedProperty = normalizeCssValue(property);
439
+ if (normalisedProperty.startsWith("-moz-binding"))
334
440
  continue;
335
- if (property === "behavior")
441
+ if (normalisedProperty === "behavior")
336
442
  continue;
337
443
  safe.push(decl);
338
444
  }
@@ -781,7 +887,7 @@ function renderLink(ctx, data) {
781
887
  const attrs = [`href="${escapeAttr(href)}"`];
782
888
  if (data.type === "page" && typeof data.link === "object") {
783
889
  const page = data.link.page;
784
- const isSpecialPage = page.startsWith("//") || page.includes(":") || page.includes("#/");
890
+ const isSpecialPage = page.startsWith("//") || page.includes("#/");
785
891
  if (!isSpecialPage) {
786
892
  const hashIdx = page.indexOf("#");
787
893
  const pageToCheck = hashIdx !== -1 ? page.slice(0, hashIdx) : page;
@@ -4558,6 +4664,9 @@ function generateDefaultUrl(pageName, contents) {
4558
4664
  return path;
4559
4665
  }
4560
4666
  function renderHtmlBlock(ctx, data) {
4667
+ if (ctx.settings.allowHtmlBlocks === false) {
4668
+ return;
4669
+ }
4561
4670
  const index = ctx.nextHtmlBlockIndex();
4562
4671
  const pageName = ctx.page?.pageName ?? "";
4563
4672
  const callbackUrl = ctx.options.resolvers?.htmlBlockUrl?.(index);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/render",
3
- "version": "1.3.3",
3
+ "version": "2.0.0",
4
4
  "description": "HTML renderer for Wikidot markup",
5
5
  "keywords": [
6
6
  "html",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@wdprlib/ast": "1.2.1",
42
+ "@wdprlib/ast": "2.0.0",
43
43
  "domhandler": "^5.0.3",
44
44
  "htmlparser2": "^10.0.0",
45
45
  "sanitize-html": "^2.14.0",