@inshapardaz/likhari-react 0.1.5 → 0.1.6-dev.43
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/EditorRoot.d.ts +15 -0
- package/dist/EditorRoot.d.ts.map +1 -1
- package/dist/EditorRoot.js +18 -3
- package/dist/components/Toolbar.d.ts +6 -1
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +83 -8
- package/dist/fonts/index.d.ts +22 -0
- package/dist/fonts/index.d.ts.map +1 -0
- package/dist/fonts/index.js +20 -0
- package/dist/fonts/urduWebFonts.d.ts +14 -0
- package/dist/fonts/urduWebFonts.d.ts.map +1 -0
- package/dist/fonts/urduWebFonts.js +66 -0
- package/dist/image/ImageComponent.d.ts +22 -0
- package/dist/image/ImageComponent.d.ts.map +1 -0
- package/dist/image/ImageComponent.js +132 -0
- package/dist/image/ImageCropper.d.ts +14 -0
- package/dist/image/ImageCropper.d.ts.map +1 -0
- package/dist/image/ImageCropper.js +87 -0
- package/dist/image/ImageDialog.d.ts +30 -0
- package/dist/image/ImageDialog.d.ts.map +1 -0
- package/dist/image/ImageDialog.js +194 -0
- package/dist/image/ImageNode.d.ts +64 -0
- package/dist/image/ImageNode.d.ts.map +1 -0
- package/dist/image/ImageNode.js +133 -0
- package/dist/image/ImageOptionsContext.d.ts +17 -0
- package/dist/image/ImageOptionsContext.d.ts.map +1 -0
- package/dist/image/ImageOptionsContext.js +10 -0
- package/dist/image/imageEdit.d.ts +43 -0
- package/dist/image/imageEdit.d.ts.map +1 -0
- package/dist/image/imageEdit.js +103 -0
- package/dist/image/imageUrl.d.ts +14 -0
- package/dist/image/imageUrl.d.ts.map +1 -0
- package/dist/image/imageUrl.js +27 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +2 -0
- package/dist/theme/editor.css +80 -0
- package/dist/theme/editorTheme.d.ts.map +1 -1
- package/dist/theme/editorTheme.js +1 -0
- package/dist/utils/style.d.ts +4 -0
- package/dist/utils/style.d.ts.map +1 -0
- package/dist/utils/style.js +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Converts a fractional crop rectangle to whole pixels inside a width x height
|
|
2
|
+
* image, clamped so it never leaves the image and is at least 1px. */
|
|
3
|
+
export function toPixelRect(rect, width, height) {
|
|
4
|
+
const sx = Math.min(Math.max(Math.round(rect.x * width), 0), width - 1);
|
|
5
|
+
const sy = Math.min(Math.max(Math.round(rect.y * height), 0), height - 1);
|
|
6
|
+
const sw = Math.min(Math.max(Math.round(rect.w * width), 1), width - sx);
|
|
7
|
+
const sh = Math.min(Math.max(Math.round(rect.h * height), 1), height - sy);
|
|
8
|
+
return { sx, sy, sw, sh };
|
|
9
|
+
}
|
|
10
|
+
/** When one dimension changes with the aspect ratio locked, returns both. */
|
|
11
|
+
export function fitDimension(changed, value, naturalWidth, naturalHeight) {
|
|
12
|
+
const v = Math.max(1, Math.round(value));
|
|
13
|
+
return changed === 'width'
|
|
14
|
+
? { width: v, height: Math.max(1, Math.round((v * naturalHeight) / naturalWidth)) }
|
|
15
|
+
: { width: Math.max(1, Math.round((v * naturalWidth) / naturalHeight)), height: v };
|
|
16
|
+
}
|
|
17
|
+
/** MIME type to re-encode an edited image as: its own if it is a lossy/lossless
|
|
18
|
+
* format canvas can write (JPEG, WebP, PNG), otherwise PNG (GIF, SVG, unknown). */
|
|
19
|
+
export function mimeFromSrc(src) {
|
|
20
|
+
const data = /^data:(image\/[a-z0-9.+-]+)[;,]/i.exec(src);
|
|
21
|
+
const type = data ? data[1].toLowerCase() : extensionMime(src);
|
|
22
|
+
return type === 'image/jpeg' || type === 'image/webp' ? type : 'image/png';
|
|
23
|
+
}
|
|
24
|
+
function extensionMime(src) {
|
|
25
|
+
const match = /\.(jpe?g|webp|png|gif|svg)(?:[?#].*)?$/i.exec(src);
|
|
26
|
+
if (!match)
|
|
27
|
+
return 'image/png';
|
|
28
|
+
const ext = match[1].toLowerCase();
|
|
29
|
+
return ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : ext === 'webp' ? 'image/webp' : 'image/png';
|
|
30
|
+
}
|
|
31
|
+
/** Approximate decoded size, in bytes, of a base64 data URI. */
|
|
32
|
+
export function dataUrlBytes(dataUrl) {
|
|
33
|
+
const comma = dataUrl.indexOf(',');
|
|
34
|
+
const base64 = comma === -1 ? '' : dataUrl.slice(comma + 1);
|
|
35
|
+
const padding = base64.endsWith('==') ? 2 : base64.endsWith('=') ? 1 : 0;
|
|
36
|
+
return Math.floor((base64.length * 3) / 4) - padding;
|
|
37
|
+
}
|
|
38
|
+
export function dataUrlToFile(dataUrl, name) {
|
|
39
|
+
const comma = dataUrl.indexOf(',');
|
|
40
|
+
const mime = /^data:([^;,]+)/.exec(dataUrl)?.[1] ?? 'image/png';
|
|
41
|
+
const binary = atob(dataUrl.slice(comma + 1));
|
|
42
|
+
const bytes = new Uint8Array(binary.length);
|
|
43
|
+
for (let i = 0; i < binary.length; i++)
|
|
44
|
+
bytes[i] = binary.charCodeAt(i);
|
|
45
|
+
return new File([bytes], name, { type: mime });
|
|
46
|
+
}
|
|
47
|
+
/** Loads an image for canvas use. External URLs are requested with CORS so the
|
|
48
|
+
* canvas isn't tainted; a server that doesn't allow that makes this reject. */
|
|
49
|
+
export function loadImage(src) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const img = new Image();
|
|
52
|
+
if (/^https?:/i.test(src))
|
|
53
|
+
img.crossOrigin = 'anonymous';
|
|
54
|
+
img.onload = () => resolve(img);
|
|
55
|
+
img.onerror = () => reject(new Error('IMAGE_LOAD_FAILED'));
|
|
56
|
+
img.src = src;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export const CANNOT_EDIT_PIXELS = "This image can't be edited here (its server doesn't allow it). Upload the file instead to crop or rotate it.";
|
|
60
|
+
/** Applies a rotate / flip / crop to an image and returns the result as a data URI. */
|
|
61
|
+
export async function applyPixelOp(src, op) {
|
|
62
|
+
let img;
|
|
63
|
+
try {
|
|
64
|
+
img = await loadImage(src);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error(CANNOT_EDIT_PIXELS);
|
|
68
|
+
}
|
|
69
|
+
const width = img.naturalWidth;
|
|
70
|
+
const height = img.naturalHeight;
|
|
71
|
+
if (!width || !height)
|
|
72
|
+
throw new Error('This image has no pixel size to edit (some SVGs). Upload a PNG or JPEG instead.');
|
|
73
|
+
const canvas = document.createElement('canvas');
|
|
74
|
+
const ctx = canvas.getContext('2d');
|
|
75
|
+
if (!ctx)
|
|
76
|
+
throw new Error('Image editing is not available in this browser');
|
|
77
|
+
if (op.type === 'rotate') {
|
|
78
|
+
canvas.width = height;
|
|
79
|
+
canvas.height = width;
|
|
80
|
+
ctx.translate(height / 2, width / 2);
|
|
81
|
+
ctx.rotate(op.direction === 'cw' ? Math.PI / 2 : -Math.PI / 2);
|
|
82
|
+
ctx.drawImage(img, -width / 2, -height / 2);
|
|
83
|
+
}
|
|
84
|
+
else if (op.type === 'flip') {
|
|
85
|
+
canvas.width = width;
|
|
86
|
+
canvas.height = height;
|
|
87
|
+
ctx.translate(op.axis === 'horizontal' ? width : 0, op.axis === 'vertical' ? height : 0);
|
|
88
|
+
ctx.scale(op.axis === 'horizontal' ? -1 : 1, op.axis === 'vertical' ? -1 : 1);
|
|
89
|
+
ctx.drawImage(img, 0, 0);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const { sx, sy, sw, sh } = toPixelRect(op.rect, width, height);
|
|
93
|
+
canvas.width = sw;
|
|
94
|
+
canvas.height = sh;
|
|
95
|
+
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return canvas.toDataURL(mimeFromSrc(src), 0.92);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
throw new Error(CANNOT_EDIT_PIXELS);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates the source of a linked image. Accepts http(s) URLs, relative
|
|
3
|
+
* paths, and base64 raster/SVG data URIs (what an embedded image stores);
|
|
4
|
+
* anything else (javascript:, other data: types, file:, ...) is rejected.
|
|
5
|
+
* Returns the trimmed URL, or null when it should be refused.
|
|
6
|
+
*/
|
|
7
|
+
export declare function normalizeImageUrl(input: string): string | null;
|
|
8
|
+
export declare const ACCEPTED_IMAGE_TYPES: string[];
|
|
9
|
+
/** Returns an error message if the file can't be inserted, otherwise null. */
|
|
10
|
+
export declare function validateImageFile(file: {
|
|
11
|
+
type: string;
|
|
12
|
+
size: number;
|
|
13
|
+
}, maxSizeMB: number): string | null;
|
|
14
|
+
//# sourceMappingURL=imageUrl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imageUrl.d.ts","sourceRoot":"","sources":["../../src/image/imageUrl.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO9D;AAED,eAAO,MAAM,oBAAoB,UAA0E,CAAC;AAE5G,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIxG"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates the source of a linked image. Accepts http(s) URLs, relative
|
|
3
|
+
* paths, and base64 raster/SVG data URIs (what an embedded image stores);
|
|
4
|
+
* anything else (javascript:, other data: types, file:, ...) is rejected.
|
|
5
|
+
* Returns the trimmed URL, or null when it should be refused.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeImageUrl(input) {
|
|
8
|
+
const url = input.trim();
|
|
9
|
+
if (!url)
|
|
10
|
+
return null;
|
|
11
|
+
if (/^https?:\/\/\S+$/i.test(url))
|
|
12
|
+
return url;
|
|
13
|
+
if (/^(\/|\.\.?\/)\S*$/.test(url))
|
|
14
|
+
return url;
|
|
15
|
+
if (/^data:image\/(png|jpe?g|gif|webp|svg\+xml);base64,[a-z0-9+/=]+$/i.test(url))
|
|
16
|
+
return url;
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'];
|
|
20
|
+
/** Returns an error message if the file can't be inserted, otherwise null. */
|
|
21
|
+
export function validateImageFile(file, maxSizeMB) {
|
|
22
|
+
if (!ACCEPTED_IMAGE_TYPES.includes(file.type))
|
|
23
|
+
return 'Choose a PNG, JPEG, GIF, WebP or SVG image';
|
|
24
|
+
if (file.size > maxSizeMB * 1024 * 1024)
|
|
25
|
+
return `Image is larger than ${maxSizeMB} MB`;
|
|
26
|
+
return null;
|
|
27
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export { EditorRoot } from './EditorRoot.js';
|
|
2
2
|
export type { EditorRootProps, EditorRef, EditorInitialContent } from './EditorRoot.js';
|
|
3
3
|
export { defaultMantineTheme } from './theme/mantineTheme.js';
|
|
4
|
+
export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
|
|
5
|
+
export type { ImagePayload, ImageLinkType, SerializedImageNode } from './image/ImageNode.js';
|
|
6
|
+
export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
|
|
7
|
+
export type { FontOption } from './fonts/index.js';
|
|
4
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { EditorRoot } from './EditorRoot.js';
|
|
2
2
|
export { defaultMantineTheme } from './theme/mantineTheme.js';
|
|
3
|
+
export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
|
|
4
|
+
export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
|
package/dist/nodes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"nodes.d.ts","sourceRoot":"","sources":["../src/nodes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAOlD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,WAAW,CAAC,EAQ5C,CAAC"}
|
package/dist/nodes.js
CHANGED
|
@@ -2,6 +2,7 @@ import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
|
2
2
|
import { ListItemNode, ListNode } from '@lexical/list';
|
|
3
3
|
import { LinkNode } from '@lexical/link';
|
|
4
4
|
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
|
5
|
+
import { ImageNode } from './image/ImageNode.js';
|
|
5
6
|
/**
|
|
6
7
|
* Node types registered on the editor at construction time. Per
|
|
7
8
|
* editor-architecture-design.md §2, node registration cannot be conditional
|
|
@@ -18,4 +19,5 @@ export const EDITOR_NODES = [
|
|
|
18
19
|
ListItemNode,
|
|
19
20
|
LinkNode,
|
|
20
21
|
HorizontalRuleNode,
|
|
22
|
+
ImageNode,
|
|
21
23
|
];
|
package/dist/theme/editor.css
CHANGED
|
@@ -235,6 +235,86 @@
|
|
|
235
235
|
margin: 1.5em 0;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/* Images — block-level figure; the caption is static text (edited in the
|
|
239
|
+
image's dialog), and a selected image gets a corner resize handle. */
|
|
240
|
+
.likhari-image-block {
|
|
241
|
+
margin: 0 0 1em 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.likhari-image-figure {
|
|
245
|
+
display: block;
|
|
246
|
+
margin: 0;
|
|
247
|
+
text-align: center;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.likhari-image-wrap {
|
|
251
|
+
position: relative;
|
|
252
|
+
display: inline-block;
|
|
253
|
+
max-width: 100%;
|
|
254
|
+
line-height: 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.likhari-image {
|
|
258
|
+
display: block;
|
|
259
|
+
max-width: 100%;
|
|
260
|
+
height: auto;
|
|
261
|
+
border-radius: 4px;
|
|
262
|
+
cursor: default;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.likhari-image-figure[data-selected='true'] .likhari-image {
|
|
266
|
+
outline: 2px solid var(--editor-accent);
|
|
267
|
+
outline-offset: 2px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.likhari-image-resize-handle {
|
|
271
|
+
position: absolute;
|
|
272
|
+
right: -6px;
|
|
273
|
+
bottom: -6px;
|
|
274
|
+
width: 12px;
|
|
275
|
+
height: 12px;
|
|
276
|
+
border: 2px solid var(--editor-paper);
|
|
277
|
+
border-radius: 3px;
|
|
278
|
+
background: var(--editor-accent);
|
|
279
|
+
cursor: nwse-resize;
|
|
280
|
+
touch-action: none;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.likhari-image-caption {
|
|
284
|
+
margin-top: 6px;
|
|
285
|
+
color: var(--editor-ink-soft);
|
|
286
|
+
font-size: 0.85em;
|
|
287
|
+
line-height: 1.4;
|
|
288
|
+
text-align: center;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Image dialog — crop stage. The selection dims everything outside it. */
|
|
292
|
+
.likhari-cropper-stage {
|
|
293
|
+
position: relative;
|
|
294
|
+
display: inline-block;
|
|
295
|
+
align-self: center;
|
|
296
|
+
max-width: 100%;
|
|
297
|
+
overflow: hidden;
|
|
298
|
+
line-height: 0;
|
|
299
|
+
cursor: crosshair;
|
|
300
|
+
touch-action: none;
|
|
301
|
+
user-select: none;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.likhari-cropper-image {
|
|
305
|
+
display: block;
|
|
306
|
+
max-width: 100%;
|
|
307
|
+
max-height: 320px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.likhari-cropper-selection {
|
|
311
|
+
position: absolute;
|
|
312
|
+
border: 2px solid #fff;
|
|
313
|
+
outline: 1px solid rgba(0, 0, 0, 0.5);
|
|
314
|
+
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
|
|
315
|
+
pointer-events: none;
|
|
316
|
+
}
|
|
317
|
+
|
|
238
318
|
/* Link menu: the URL item shows one truncated line. */
|
|
239
319
|
.likhari-link-menu-url {
|
|
240
320
|
max-width: 100%;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"editorTheme.d.ts","sourceRoot":"","sources":["../../src/theme/editorTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;GAIG;AACH,eAAO,MAAM,WAAW,EAAE,kBA8BzB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/utils/style.ts"],"names":[],"mappings":"AAAA;yDACyD;AACzD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAYrF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Sets (or replaces) one property in an inline CSS string like Lexical's
|
|
2
|
+
* TextNode style, keeping every other property as-is. */
|
|
3
|
+
export function setStyleProperty(css, property, value) {
|
|
4
|
+
const entries = css
|
|
5
|
+
.split(';')
|
|
6
|
+
.map((part) => part.trim())
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((part) => {
|
|
9
|
+
const i = part.indexOf(':');
|
|
10
|
+
return [part.slice(0, i).trim(), part.slice(i + 1).trim()];
|
|
11
|
+
})
|
|
12
|
+
.filter(([name]) => name !== property);
|
|
13
|
+
entries.push([property, value]);
|
|
14
|
+
return entries.map(([name, val]) => `${name}: ${val};`).join(' ');
|
|
15
|
+
}
|