@accelflow/space-content 0.1.0-beta.4 → 0.1.0-beta.5

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.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/package.json +5 -3
  3. package/render.mjs +12 -3
package/README.md CHANGED
@@ -214,3 +214,19 @@ paragraphs and content outside this wrapper are unchanged. Customize the gap:
214
214
  ```css
215
215
  .space-content { --space-content-paragraph-gap: 0.75em; }
216
216
  ```
217
+
218
+ ### Image dimensions
219
+
220
+ `renderArticleBody` retains independently specified `width` and `height` for JSON
221
+ image nodes and raster attachment nodes. Each dimension must be an integer from
222
+ 1 through 10000 pixels. Missing dimensions retain the browser's automatic sizing.
223
+ Images keep `max-width:100%` so they fit narrower article containers.
224
+
225
+ For HTML bodies, the sanitizer retains only bounded pixel width/height, image
226
+ `width:100%`, `height:auto`, and `max-width:100%` on the image/figure elements used
227
+ by Space exports. Other inline CSS and executable attributes remain disallowed.
228
+
229
+ Run `npm run verify:local` for the standard local verification. This runs the
230
+ SDK tests, including Build API transfer-to-render image regressions for JSON and
231
+ HTML, then verifies the packed public exports and Vite/Next static builds, and
232
+ audits dependencies. The same image regressions run against the packed SDK.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@accelflow/space-content",
3
- "version": "0.1.0-beta.4",
3
+ "version": "0.1.0-beta.5",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"
@@ -29,7 +29,9 @@
29
29
  "scripts": {
30
30
  "test": "node --test test/*.test.mjs",
31
31
  "prepack": "npm test",
32
- "verify:package": "node scripts/verify-package.mjs"
32
+ "verify:package": "node scripts/verify-package.mjs",
33
+ "audit:dependencies": "npm audit --audit-level=moderate",
34
+ "verify:local": "npm run verify:package && npm run audit:dependencies"
33
35
  },
34
36
  "files": [
35
37
  "node.mjs",
@@ -61,7 +63,7 @@
61
63
  "tag": "beta"
62
64
  },
63
65
  "dependencies": {
64
- "sanitize-html": "^2.18.0"
66
+ "sanitize-html": "2.18.1"
65
67
  },
66
68
  "author": {
67
69
  "name": "Accelflow Inc.",
package/render.mjs CHANGED
@@ -4,6 +4,11 @@ export const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, c =>
4
4
  const linkTarget = attrs => attrs?.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
5
5
  const hash = /^[a-f0-9]{64}$/;
6
6
  const raster = new Set(['image/png','image/jpeg','image/gif','image/webp','image/avif']);
7
+ const imagePixels = /^(?:[1-9]\d{0,3}|10000)px$/;
8
+ function imageStyle(attrs) {
9
+ const dimensions=['width','height'].filter(key=>Number.isSafeInteger(attrs[key])&&attrs[key]>0&&attrs[key]<=10000).map(key=>`${key}:${attrs[key]}px`);
10
+ return dimensions.length?` style="max-width:100%;${dimensions.join(';')}"`:'';
11
+ }
7
12
  function assetsIndex(assets = []) {
8
13
  const ids = new Map(), paths = new Map();
9
14
  for (const a of assets) {
@@ -26,12 +31,16 @@ export function sanitizeArticleHtml(html, {assets = [], pdfMode = 'link'} = {})
26
31
  allowedTags: [...sanitizeHtml.defaults.allowedTags,'img','details','summary','nav',...(pdfMode==='embed'?['iframe']:[])],
27
32
  allowedAttributes: {
28
33
  '*':['id','class'], a:['href','title','data-level','download','target','rel'],
29
- img:['src','alt','title','width','height','loading','referrerpolicy'],
34
+ img:['src','alt','title','width','height','loading','referrerpolicy','style'], figure:['style'],
30
35
  nav:['data-table-of-contents','data-max-level','aria-label'], details:['open','data-space-toc'],
31
36
  li:['data-level','data-checked'], ol:['start'], td:['colspan','rowspan'], th:['colspan','rowspan','scope'],
32
37
  iframe:['src','title','sandbox','loading','referrerpolicy'],
33
38
  },
34
39
  allowedClasses:{'*':['*']},
40
+ allowedStyles:{
41
+ img:{width:[imagePixels,/^100%$/],height:[imagePixels,/^auto$/],'max-width':[/^100%$/]},
42
+ figure:{width:[imagePixels],'max-width':[/^100%$/]},
43
+ },
35
44
  allowedSchemes:['https','http','mailto','tel'], allowedSchemesByTag:{img:['https'],iframe:[]}, allowProtocolRelative:false,
36
45
  transformTags: {
37
46
  a: (tagName, attrs) => {const {target,rel,...rest}=attrs;return {tagName,attribs:{...rest,...(target==='_blank'&&/^https?:\/\//i.test(safeUrl(attrs.href))?{target:'_blank',rel:'noopener noreferrer'}:{})}};},
@@ -69,11 +78,11 @@ export function renderArticleBody(body, options = {}) {
69
78
  for(const mark of Array.isArray(n.marks)?n.marks:[]) {const tag={bold:'strong',italic:'em',underline:'u',strike:'s',code:'code'}[mark?.type];if(tag)html=wrap(tag,html);else if(mark?.type==='link'){const href=safeUrl(mark.attrs?.href);if(href)html=wrap('a',html,` href="${e(href)}"${linkTarget(mark.attrs)}`);}}
70
79
  } else if(n.type==='heading') html=wrap('h'+(Number.isInteger(a.level)&&a.level>=1&&a.level<=6?a.level:2),children,` id="${e(anchors.get(n))}"`);
71
80
  else if(n.type==='tableOfContents') html=renderTableOfContents(headings,{locale:options.locale??'ja',maxLevel:a.maxLevel===2?2:3});
72
- else if(n.type==='image') {const src=safeUrl(a.src,true);html=src?`<img src="${e(src)}" alt="${e(a.alt)}" loading="lazy" referrerpolicy="no-referrer">`:'';}
81
+ else if(n.type==='image') {const src=safeUrl(a.src,true);html=src?`<img src="${e(src)}" alt="${e(a.alt)}" loading="lazy" referrerpolicy="no-referrer"${imageStyle(a)}>`:'';}
73
82
  else if(n.type==='attachment') {
74
83
  const asset=ids.get(a.attachmentId);if(!asset)throw Error('attachment_not_in_manifest');
75
84
  const src='/space-assets/'+asset.hash, label=e(asset.filename);
76
- html=raster.has(asset.contentType)?`<img src="${src}" alt="${label}" loading="lazy">`:`<a href="${src}" download="${label}">${label}</a>`;
85
+ html=raster.has(asset.contentType)?`<img src="${src}" alt="${label}" loading="lazy"${imageStyle(a)}>`:`<a href="${src}" download="${label}">${label}</a>`;
77
86
  if(asset.contentType==='application/pdf'&&options.pdfMode==='embed')html=`<iframe class="space-pdf" src="${src}" title="${label}" sandbox="" loading="lazy" referrerpolicy="no-referrer"></iframe>`+html;
78
87
  } else if(n.type==='externalLinkCard') {const href=safeUrl(a.href);html=href?wrap('a',e(a.title||href),` href="${e(href)}"${linkTarget(a)}`):e(a.title);}
79
88
  else if(n.type==='articleReference') {const target=options.resolveArticle?.(a);const href=safeUrl(target?.href);const label=e(a.labelMode==='custom'?a.customLabel:target?.title??a.fallbackTitle??a.title??'');html=href?wrap('a',label,` href="${e(href)}"`):label;}