@ashraf_mizo/htmlcanvas 1.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/bin/cli.js +28 -0
- package/editor/alignment.js +211 -0
- package/editor/assets.js +724 -0
- package/editor/clipboard.js +177 -0
- package/editor/coords.js +121 -0
- package/editor/crop.js +325 -0
- package/editor/cssVars.js +134 -0
- package/editor/domModel.js +161 -0
- package/editor/editor.css +1996 -0
- package/editor/editor.js +833 -0
- package/editor/guides.js +513 -0
- package/editor/history.js +135 -0
- package/editor/index.html +540 -0
- package/editor/layers.js +389 -0
- package/editor/logo-final.svg +21 -0
- package/editor/logo-toolbar.svg +21 -0
- package/editor/manipulation.js +864 -0
- package/editor/multiSelect.js +436 -0
- package/editor/properties.js +1583 -0
- package/editor/selection.js +432 -0
- package/editor/serializer.js +160 -0
- package/editor/shortcuts.js +143 -0
- package/editor/slidePanel.js +361 -0
- package/editor/slides.js +101 -0
- package/editor/snap.js +98 -0
- package/editor/textEdit.js +538 -0
- package/editor/zoom.js +96 -0
- package/package.json +28 -0
- package/server.js +588 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cssVars.js — CSS variable safety layer
|
|
3
|
+
*
|
|
4
|
+
* A pure utility module with zero dependencies. Provides functions to safely
|
|
5
|
+
* detect and preserve CSS custom property (var()) references in raw style
|
|
6
|
+
* strings, without going through the CSSOM (which would resolve them away).
|
|
7
|
+
*
|
|
8
|
+
* Rule: no phase beyond Phase 1 may write a CSS value without calling
|
|
9
|
+
* hasVarReference first.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns true if the given string contains any var() reference.
|
|
14
|
+
*
|
|
15
|
+
* Uses a permissive pattern — if a value looks like it might contain var(),
|
|
16
|
+
* it is treated as protected. This correctly handles:
|
|
17
|
+
* - Simple: var(--accent)
|
|
18
|
+
* - With fallback: var(--pad, 16px)
|
|
19
|
+
* - Nested: var(--a, var(--b))
|
|
20
|
+
* - Colon in fallback: var(--a, rgb(0,0,0))
|
|
21
|
+
* - URL fallback: var(--a, url('http://example.com'))
|
|
22
|
+
*
|
|
23
|
+
* @param {*} value - The value to test. Non-strings and empty strings return false.
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* hasVarReference('var(--accent)') // true
|
|
28
|
+
* hasVarReference('var(--a, rgb(0,0,0))') // true
|
|
29
|
+
* hasVarReference('#E87420') // false
|
|
30
|
+
* hasVarReference(null) // false
|
|
31
|
+
*/
|
|
32
|
+
export function hasVarReference(value) {
|
|
33
|
+
if (typeof value !== 'string' || value === '') return false;
|
|
34
|
+
return /var\s*\(/.test(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parses a raw inline style attribute string into a plain object.
|
|
39
|
+
*
|
|
40
|
+
* Splits on `;` to get declarations, then splits each declaration on the
|
|
41
|
+
* FIRST `:` only — everything after the first colon is treated as the value.
|
|
42
|
+
* This correctly preserves colon-containing values such as:
|
|
43
|
+
* - var(--a, rgb(0,0,0))
|
|
44
|
+
* - var(--a, url('http://example.com'))
|
|
45
|
+
* - calc(100% - var(--pad))
|
|
46
|
+
*
|
|
47
|
+
* Trims both property and value. Skips declarations with no property name.
|
|
48
|
+
*
|
|
49
|
+
* @param {string|null|undefined} styleAttr - Raw value of a style="" attribute.
|
|
50
|
+
* @returns {Object.<string, string>} Plain object mapping property names to raw values.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* parseInlineStyle('color: var(--accent); margin: 0')
|
|
54
|
+
* // => { color: 'var(--accent)', margin: '0' }
|
|
55
|
+
*
|
|
56
|
+
* parseInlineStyle('background-color: var(--a, rgb(0,0,0))')
|
|
57
|
+
* // => { 'background-color': 'var(--a, rgb(0,0,0))' }
|
|
58
|
+
*/
|
|
59
|
+
export function parseInlineStyle(styleAttr) {
|
|
60
|
+
if (!styleAttr) return {};
|
|
61
|
+
|
|
62
|
+
const result = {};
|
|
63
|
+
const declarations = styleAttr.split(';');
|
|
64
|
+
|
|
65
|
+
for (const declaration of declarations) {
|
|
66
|
+
const colonIndex = declaration.indexOf(':');
|
|
67
|
+
if (colonIndex === -1) continue;
|
|
68
|
+
|
|
69
|
+
const prop = declaration.slice(0, colonIndex).trim();
|
|
70
|
+
const value = declaration.slice(colonIndex + 1).trim();
|
|
71
|
+
|
|
72
|
+
if (prop === '') continue;
|
|
73
|
+
|
|
74
|
+
result[prop] = value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Collects all CSS custom properties (--name) declared in a document's
|
|
82
|
+
* stylesheets, along with their resolved computed values.
|
|
83
|
+
*
|
|
84
|
+
* NOTE: This function must be called from within an iframe context in phases 2+
|
|
85
|
+
* where the target document is available. In Phase 1, it is exported but not
|
|
86
|
+
* called against live documents.
|
|
87
|
+
*
|
|
88
|
+
* Cross-origin stylesheets are silently skipped (they throw SecurityError on
|
|
89
|
+
* .cssRules access).
|
|
90
|
+
*
|
|
91
|
+
* @param {Document} doc - The document to inspect (e.g., iframe.contentDocument).
|
|
92
|
+
* @returns {Object.<string, {declared: string, resolved: string}>}
|
|
93
|
+
* Object keyed by --variable-name, each value has:
|
|
94
|
+
* - declared: the raw value from the stylesheet rule
|
|
95
|
+
* - resolved: the computed value from getComputedStyle on :root
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // In an iframe context:
|
|
99
|
+
* const vars = collectCSSVariables(iframe.contentDocument);
|
|
100
|
+
* // => { '--accent': { declared: '#E87420', resolved: '#E87420' }, ... }
|
|
101
|
+
*/
|
|
102
|
+
export function collectCSSVariables(doc) {
|
|
103
|
+
const vars = {};
|
|
104
|
+
|
|
105
|
+
for (const sheet of doc.styleSheets) {
|
|
106
|
+
try {
|
|
107
|
+
const rules = sheet.cssRules;
|
|
108
|
+
for (const rule of rules) {
|
|
109
|
+
if (!rule.style) continue;
|
|
110
|
+
for (let i = 0; i < rule.style.length; i++) {
|
|
111
|
+
const prop = rule.style[i];
|
|
112
|
+
if (prop.startsWith('--')) {
|
|
113
|
+
vars[prop] = {
|
|
114
|
+
declared: rule.style.getPropertyValue(prop).trim(),
|
|
115
|
+
resolved: '',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Cross-origin sheet — skip silently
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Resolve current computed values via :root
|
|
126
|
+
if (Object.keys(vars).length > 0) {
|
|
127
|
+
const computed = getComputedStyle(doc.documentElement);
|
|
128
|
+
for (const prop of Object.keys(vars)) {
|
|
129
|
+
vars[prop].resolved = computed.getPropertyValue(prop).trim();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return vars;
|
|
134
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// editor/domModel.js — DOM model with stable data-hc-id attributes
|
|
2
|
+
//
|
|
3
|
+
// Parses raw HTML and injects data-hc-id="hc-N" on every element tag using
|
|
4
|
+
// regex-based tag walking. Does NOT use DOMParser (which normalises HTML).
|
|
5
|
+
// Exclusion zones (script content, style content, comments, doctypes) are
|
|
6
|
+
// skipped so IDs are never injected into non-element contexts.
|
|
7
|
+
|
|
8
|
+
// ── Module-level state ──────────────────────────────────────────────────────
|
|
9
|
+
let _currentElementMap = null;
|
|
10
|
+
let _currentRawHTML = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns the current DOM model state set by the last injectIds call.
|
|
14
|
+
* @returns {{ elementMap: Map<string, ElementRecord> | null, rawHTML: string | null }}
|
|
15
|
+
*/
|
|
16
|
+
export function getModel() {
|
|
17
|
+
return { elementMap: _currentElementMap, rawHTML: _currentRawHTML };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ── Exclusion zones ─────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Finds ranges in the HTML string where tags should NOT be annotated:
|
|
24
|
+
* - <!-- comments -->
|
|
25
|
+
* - <script>...</script> content (between the tags, not the tags themselves)
|
|
26
|
+
* - <style>...</style> content (between the tags, not the tags themselves)
|
|
27
|
+
* - <!DOCTYPE ...>
|
|
28
|
+
*
|
|
29
|
+
* @param {string} html
|
|
30
|
+
* @returns {Array<[number, number]>} Array of [startIndex, endIndex) pairs
|
|
31
|
+
*/
|
|
32
|
+
function findExclusionZones(html) {
|
|
33
|
+
const zones = [];
|
|
34
|
+
let m;
|
|
35
|
+
|
|
36
|
+
// DOCTYPE declarations
|
|
37
|
+
const doctypeRegex = /<!DOCTYPE[^>]*>/gi;
|
|
38
|
+
while ((m = doctypeRegex.exec(html)) !== null) {
|
|
39
|
+
zones.push([m.index, m.index + m[0].length]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// HTML comments (<!-- ... -->)
|
|
43
|
+
const commentRegex = /<!--[\s\S]*?-->/g;
|
|
44
|
+
while ((m = commentRegex.exec(html)) !== null) {
|
|
45
|
+
zones.push([m.index, m.index + m[0].length]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Script content (between <script...> and </script>)
|
|
49
|
+
const scriptRegex = /<script([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
50
|
+
while ((m = scriptRegex.exec(html)) !== null) {
|
|
51
|
+
// Exclude only the content between the opening and closing tags
|
|
52
|
+
const openTagEnd = m.index + m[0].indexOf('>') + 1;
|
|
53
|
+
const closeTagStart = m.index + m[0].lastIndexOf('</');
|
|
54
|
+
if (openTagEnd < closeTagStart) {
|
|
55
|
+
zones.push([openTagEnd, closeTagStart]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Style content (between <style...> and </style>)
|
|
60
|
+
const styleRegex = /<style([^>]*)>([\s\S]*?)<\/style>/gi;
|
|
61
|
+
while ((m = styleRegex.exec(html)) !== null) {
|
|
62
|
+
const openTagEnd = m.index + m[0].indexOf('>') + 1;
|
|
63
|
+
const closeTagStart = m.index + m[0].lastIndexOf('</');
|
|
64
|
+
if (openTagEnd < closeTagStart) {
|
|
65
|
+
zones.push([openTagEnd, closeTagStart]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return zones;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Checks whether a given index falls inside any exclusion zone.
|
|
74
|
+
* @param {number} index
|
|
75
|
+
* @param {Array<[number, number]>} zones
|
|
76
|
+
* @returns {boolean}
|
|
77
|
+
*/
|
|
78
|
+
function isInExclusionZone(index, zones) {
|
|
79
|
+
return zones.some(([start, end]) => index >= start && index < end);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Main export ─────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parses an HTML string and injects data-hc-id attributes on every element.
|
|
86
|
+
* Uses regex-based tag walking on the raw HTML to preserve the original
|
|
87
|
+
* formatting exactly (no attribute reordering, no whitespace changes).
|
|
88
|
+
*
|
|
89
|
+
* ID format: "hc-{sequential-number}" starting from 0.
|
|
90
|
+
* IDs are assigned in document order.
|
|
91
|
+
*
|
|
92
|
+
* Exclusion zones: tags inside <script>, <style>, <!-- comments -->, and
|
|
93
|
+
* <!DOCTYPE> are skipped.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} rawHTML - The original HTML file content as a string
|
|
96
|
+
* @returns {{ annotatedHTML: string, elementMap: Map<string, ElementRecord> }}
|
|
97
|
+
* - annotatedHTML: HTML string with data-hc-id on every element tag
|
|
98
|
+
* - elementMap: Map from hc-id string to ElementRecord
|
|
99
|
+
*
|
|
100
|
+
* ElementRecord shape:
|
|
101
|
+
* {
|
|
102
|
+
* id: string, // e.g. "hc-42"
|
|
103
|
+
* tagName: string, // e.g. "div", "span", "img"
|
|
104
|
+
* sourceIndex: number, // character index in rawHTML where the opening tag starts
|
|
105
|
+
* sourceLength: number // length of the original opening tag in rawHTML
|
|
106
|
+
* }
|
|
107
|
+
*/
|
|
108
|
+
export function injectIds(rawHTML) {
|
|
109
|
+
_currentRawHTML = rawHTML;
|
|
110
|
+
|
|
111
|
+
const elementMap = new Map();
|
|
112
|
+
const zones = findExclusionZones(rawHTML);
|
|
113
|
+
let counter = 0;
|
|
114
|
+
let annotatedHTML = '';
|
|
115
|
+
let lastIndex = 0;
|
|
116
|
+
|
|
117
|
+
// Match opening tags: <tagName ...attributes... /> or <tagName ...attributes...>
|
|
118
|
+
// Does NOT match closing tags (</...>), comments (<!--), or doctypes (<!)
|
|
119
|
+
const openingTagRegex = /<([a-zA-Z][a-zA-Z0-9]*)((?:\s+(?:[^>"'=]+=(?:"[^"]*"|'[^']*'|[^\s>]+)|[^>"'=\s]+))*\s*)(\/?)>/g;
|
|
120
|
+
let match;
|
|
121
|
+
|
|
122
|
+
while ((match = openingTagRegex.exec(rawHTML)) !== null) {
|
|
123
|
+
const fullMatch = match[0];
|
|
124
|
+
const tagName = match[1];
|
|
125
|
+
const attributes = match[2];
|
|
126
|
+
const selfClosing = match[3];
|
|
127
|
+
const sourceIndex = match.index;
|
|
128
|
+
|
|
129
|
+
// Skip tags inside exclusion zones
|
|
130
|
+
if (isInExclusionZone(sourceIndex, zones)) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const id = `hc-${counter++}`;
|
|
135
|
+
|
|
136
|
+
// Copy everything from lastIndex up to this tag
|
|
137
|
+
annotatedHTML += rawHTML.slice(lastIndex, sourceIndex);
|
|
138
|
+
|
|
139
|
+
// Reconstruct the tag with data-hc-id injected before the closing >
|
|
140
|
+
if (selfClosing) {
|
|
141
|
+
annotatedHTML += `<${tagName}${attributes} data-hc-id="${id}" />`;
|
|
142
|
+
} else {
|
|
143
|
+
annotatedHTML += `<${tagName}${attributes} data-hc-id="${id}">`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
lastIndex = sourceIndex + fullMatch.length;
|
|
147
|
+
|
|
148
|
+
elementMap.set(id, {
|
|
149
|
+
id,
|
|
150
|
+
tagName: tagName.toLowerCase(),
|
|
151
|
+
sourceIndex,
|
|
152
|
+
sourceLength: fullMatch.length,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Append any remaining content after the last tag
|
|
157
|
+
annotatedHTML += rawHTML.slice(lastIndex);
|
|
158
|
+
|
|
159
|
+
_currentElementMap = elementMap;
|
|
160
|
+
return { annotatedHTML, elementMap };
|
|
161
|
+
}
|