editor-ts 0.0.1
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/README.md +401 -0
- package/index.ts +81 -0
- package/package.json +42 -0
- package/src/core/AssetManager.ts +127 -0
- package/src/core/ComponentManager.ts +206 -0
- package/src/core/Page.ts +138 -0
- package/src/core/StyleManager.ts +221 -0
- package/src/core/ToolbarManager.ts +170 -0
- package/src/core/init.ts +374 -0
- package/src/styles/branding.css +109 -0
- package/src/types.ts +157 -0
- package/src/utils/helpers.ts +199 -0
- package/src/utils/toolbar.ts +140 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { PageBody, Component, ComponentQuery } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manager for handling component operations
|
|
5
|
+
*/
|
|
6
|
+
export class ComponentManager {
|
|
7
|
+
private body: PageBody;
|
|
8
|
+
private parsedComponents: Component[];
|
|
9
|
+
|
|
10
|
+
constructor(body: PageBody) {
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.parsedComponents = this.parse();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse components from JSON string
|
|
17
|
+
*/
|
|
18
|
+
private parse(): Component[] {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(this.body.components) as Component[];
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Failed to parse components:', error);
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find components by query
|
|
29
|
+
*/
|
|
30
|
+
find(query: ComponentQuery): Component[] {
|
|
31
|
+
return this.findInTree(this.parsedComponents, query);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Recursively search component tree
|
|
36
|
+
*/
|
|
37
|
+
private findInTree(components: Component[], query: ComponentQuery): Component[] {
|
|
38
|
+
const results: Component[] = [];
|
|
39
|
+
|
|
40
|
+
for (const component of components) {
|
|
41
|
+
let matches = true;
|
|
42
|
+
|
|
43
|
+
// Check ID
|
|
44
|
+
if (query.id && component.attributes?.id !== query.id) {
|
|
45
|
+
matches = false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check type
|
|
49
|
+
if (query.type && component.type !== query.type) {
|
|
50
|
+
matches = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check tagName
|
|
54
|
+
if (query.tagName && component.tagName !== query.tagName) {
|
|
55
|
+
matches = false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check attributes
|
|
59
|
+
if (query.attributes && matches) {
|
|
60
|
+
for (const [key, value] of Object.entries(query.attributes)) {
|
|
61
|
+
if (component.attributes?.[key] !== value) {
|
|
62
|
+
matches = false;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (matches) {
|
|
69
|
+
results.push(component);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Search in nested components
|
|
73
|
+
if (component.components && component.components.length > 0) {
|
|
74
|
+
results.push(...this.findInTree(component.components, query));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Find a single component by ID
|
|
83
|
+
*/
|
|
84
|
+
findById(id: string): Component | null {
|
|
85
|
+
const results = this.find({ id });
|
|
86
|
+
return results.length > 0 ? results[0]! : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Find components by type
|
|
91
|
+
*/
|
|
92
|
+
findByType(type: string): Component[] {
|
|
93
|
+
return this.find({ type });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Find components by tag name
|
|
98
|
+
*/
|
|
99
|
+
findByTagName(tagName: string): Component[] {
|
|
100
|
+
return this.find({ tagName });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Add a component to the root level
|
|
105
|
+
*/
|
|
106
|
+
addComponent(component: Component): void {
|
|
107
|
+
this.parsedComponents.push(component);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Add a component as a child of another component
|
|
112
|
+
*/
|
|
113
|
+
addChildComponent(parentId: string, component: Component): boolean {
|
|
114
|
+
const parent = this.findById(parentId);
|
|
115
|
+
if (parent) {
|
|
116
|
+
if (!parent.components) {
|
|
117
|
+
parent.components = [];
|
|
118
|
+
}
|
|
119
|
+
parent.components.push(component);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Remove a component by ID
|
|
127
|
+
*/
|
|
128
|
+
removeComponent(id: string): boolean {
|
|
129
|
+
return this.removeFromTree(this.parsedComponents, id);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Recursively remove component from tree
|
|
134
|
+
*/
|
|
135
|
+
private removeFromTree(components: Component[], id: string): boolean {
|
|
136
|
+
for (let i = 0; i < components.length; i++) {
|
|
137
|
+
const component = components[i];
|
|
138
|
+
|
|
139
|
+
if (component?.attributes?.id === id) {
|
|
140
|
+
components.splice(i, 1);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (component?.components && component.components.length > 0) {
|
|
145
|
+
if (this.removeFromTree(component.components, id)) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Update a component's attributes
|
|
156
|
+
*/
|
|
157
|
+
updateComponent(id: string, updates: Partial<Component>): boolean {
|
|
158
|
+
const component = this.findById(id);
|
|
159
|
+
if (component) {
|
|
160
|
+
Object.assign(component, updates);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get all components
|
|
168
|
+
*/
|
|
169
|
+
getAll(): Component[] {
|
|
170
|
+
return this.parsedComponents;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get component count
|
|
175
|
+
*/
|
|
176
|
+
count(): number {
|
|
177
|
+
return this.countInTree(this.parsedComponents);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Recursively count components
|
|
182
|
+
*/
|
|
183
|
+
private countInTree(components: Component[]): number {
|
|
184
|
+
let count = components.length;
|
|
185
|
+
for (const component of components) {
|
|
186
|
+
if (component.components && component.components.length > 0) {
|
|
187
|
+
count += this.countInTree(component.components);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return count;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Sync changes back to page body
|
|
195
|
+
*/
|
|
196
|
+
sync(): void {
|
|
197
|
+
this.body.components = JSON.stringify(this.parsedComponents);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Replace all components
|
|
202
|
+
*/
|
|
203
|
+
replaceAll(components: Component[]): void {
|
|
204
|
+
this.parsedComponents = components;
|
|
205
|
+
}
|
|
206
|
+
}
|
package/src/core/Page.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { PageData, PageBody } from '../types';
|
|
2
|
+
import { ComponentManager } from './ComponentManager';
|
|
3
|
+
import { StyleManager } from './StyleManager';
|
|
4
|
+
import { AssetManager } from './AssetManager';
|
|
5
|
+
import { ToolbarManager } from './ToolbarManager';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Main class for managing page content
|
|
9
|
+
*/
|
|
10
|
+
export class Page {
|
|
11
|
+
private data: PageData;
|
|
12
|
+
public components: ComponentManager;
|
|
13
|
+
public styles: StyleManager;
|
|
14
|
+
public assets: AssetManager;
|
|
15
|
+
public toolbars: ToolbarManager;
|
|
16
|
+
|
|
17
|
+
constructor(pageData: PageData | string) {
|
|
18
|
+
if (typeof pageData === 'string') {
|
|
19
|
+
this.data = JSON.parse(pageData) as PageData;
|
|
20
|
+
} else {
|
|
21
|
+
this.data = pageData;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Initialize managers
|
|
25
|
+
this.components = new ComponentManager(this.data.body);
|
|
26
|
+
this.styles = new StyleManager(this.data.body);
|
|
27
|
+
this.assets = new AssetManager(this.data.body);
|
|
28
|
+
this.toolbars = new ToolbarManager();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the page title
|
|
33
|
+
*/
|
|
34
|
+
getTitle(): string {
|
|
35
|
+
return this.data.title;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Set the page title
|
|
40
|
+
*/
|
|
41
|
+
setTitle(title: string): void {
|
|
42
|
+
this.data.title = title;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the page item ID
|
|
47
|
+
*/
|
|
48
|
+
getItemId(): number {
|
|
49
|
+
return this.data.item_id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Set the page item ID
|
|
54
|
+
*/
|
|
55
|
+
setItemId(itemId: number): void {
|
|
56
|
+
this.data.item_id = itemId;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the raw HTML
|
|
61
|
+
*/
|
|
62
|
+
getHTML(): string {
|
|
63
|
+
return this.data.body.html;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Set the raw HTML
|
|
68
|
+
*/
|
|
69
|
+
setHTML(html: string): void {
|
|
70
|
+
this.data.body.html = html;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the compiled CSS
|
|
75
|
+
*/
|
|
76
|
+
getCSS(): string {
|
|
77
|
+
return this.data.body.css;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set the compiled CSS
|
|
82
|
+
*/
|
|
83
|
+
setCSS(css: string): void {
|
|
84
|
+
this.data.body.css = css;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the page body
|
|
89
|
+
*/
|
|
90
|
+
getBody(): PageBody {
|
|
91
|
+
return this.data.body;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Export the page as JSON string
|
|
96
|
+
*/
|
|
97
|
+
toJSON(): string {
|
|
98
|
+
// Sync all managers back to data
|
|
99
|
+
this.components.sync();
|
|
100
|
+
this.styles.sync();
|
|
101
|
+
this.assets.sync();
|
|
102
|
+
|
|
103
|
+
return JSON.stringify(this.data, null, 2);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Export the page as object
|
|
108
|
+
*/
|
|
109
|
+
toObject(): PageData {
|
|
110
|
+
// Sync all managers back to data
|
|
111
|
+
this.components.sync();
|
|
112
|
+
this.styles.sync();
|
|
113
|
+
this.assets.sync();
|
|
114
|
+
|
|
115
|
+
return this.data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Load page from JSON file
|
|
120
|
+
*/
|
|
121
|
+
static fromJSON(json: string): Page {
|
|
122
|
+
return new Page(json);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clone the page
|
|
127
|
+
*/
|
|
128
|
+
clone(): Page {
|
|
129
|
+
return new Page(JSON.parse(JSON.stringify(this.data)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get raw page data
|
|
134
|
+
*/
|
|
135
|
+
getRawData(): PageData {
|
|
136
|
+
return this.data;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { PageBody, Style, StyleQuery, CSSProperties } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manager for handling CSS styles
|
|
5
|
+
*/
|
|
6
|
+
export class StyleManager {
|
|
7
|
+
private body: PageBody;
|
|
8
|
+
private styles: Style[];
|
|
9
|
+
|
|
10
|
+
constructor(body: PageBody) {
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.styles = body.styles || [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find styles by query
|
|
17
|
+
*/
|
|
18
|
+
find(query: StyleQuery): Style[] {
|
|
19
|
+
return this.styles.filter((style) => {
|
|
20
|
+
let matches = true;
|
|
21
|
+
|
|
22
|
+
// Check selector
|
|
23
|
+
if (query.selector) {
|
|
24
|
+
const hasSelector = style.selectors.some((sel) => {
|
|
25
|
+
if (typeof sel === 'string') {
|
|
26
|
+
return sel === query.selector || sel.includes(query.selector!);
|
|
27
|
+
}
|
|
28
|
+
return sel.name === query.selector;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!hasSelector && style.selectorsAdd !== query.selector) {
|
|
32
|
+
matches = false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check media query
|
|
37
|
+
if (query.mediaText && style.mediaText !== query.mediaText) {
|
|
38
|
+
matches = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check state
|
|
42
|
+
if (query.state && style.state !== query.state) {
|
|
43
|
+
matches = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return matches;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Find styles for a specific selector
|
|
52
|
+
*/
|
|
53
|
+
findBySelector(selector: string): Style[] {
|
|
54
|
+
return this.find({ selector });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Find styles for a media query
|
|
59
|
+
*/
|
|
60
|
+
findByMedia(mediaText: string): Style[] {
|
|
61
|
+
return this.find({ mediaText });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Add a new style rule
|
|
66
|
+
*/
|
|
67
|
+
addStyle(style: Style): void {
|
|
68
|
+
this.styles.push(style);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Remove styles by selector
|
|
73
|
+
*/
|
|
74
|
+
removeBySelector(selector: string): number {
|
|
75
|
+
const initialLength = this.styles.length;
|
|
76
|
+
this.styles = this.styles.filter((style) => {
|
|
77
|
+
const hasSelector = style.selectors.some((sel) => {
|
|
78
|
+
if (typeof sel === 'string') {
|
|
79
|
+
return sel === selector;
|
|
80
|
+
}
|
|
81
|
+
return sel.name === selector;
|
|
82
|
+
});
|
|
83
|
+
return !hasSelector && style.selectorsAdd !== selector;
|
|
84
|
+
});
|
|
85
|
+
return initialLength - this.styles.length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update styles for a selector
|
|
90
|
+
*/
|
|
91
|
+
updateStyle(selector: string, properties: CSSProperties, options?: { mediaText?: string; state?: string }): boolean {
|
|
92
|
+
const matchingStyles = this.styles.filter((style) => {
|
|
93
|
+
const hasSelector = style.selectors.some((sel) => {
|
|
94
|
+
if (typeof sel === 'string') {
|
|
95
|
+
return sel === selector;
|
|
96
|
+
}
|
|
97
|
+
return sel.name === selector;
|
|
98
|
+
}) || style.selectorsAdd === selector;
|
|
99
|
+
|
|
100
|
+
if (!hasSelector) return false;
|
|
101
|
+
|
|
102
|
+
// Check media query if specified
|
|
103
|
+
if (options?.mediaText && style.mediaText !== options.mediaText) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check state if specified
|
|
108
|
+
if (options?.state && style.state !== options.state) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return true;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (matchingStyles.length > 0) {
|
|
116
|
+
matchingStyles.forEach((style) => {
|
|
117
|
+
Object.assign(style.style, properties);
|
|
118
|
+
});
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get style properties for a selector
|
|
127
|
+
*/
|
|
128
|
+
getStyleProperties(selector: string, options?: { mediaText?: string; state?: string }): CSSProperties | null {
|
|
129
|
+
const styles = this.find({ selector, ...options });
|
|
130
|
+
if (styles.length > 0) {
|
|
131
|
+
return styles[0]!.style;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get all styles
|
|
138
|
+
*/
|
|
139
|
+
getAll(): Style[] {
|
|
140
|
+
return this.styles;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get style count
|
|
145
|
+
*/
|
|
146
|
+
count(): number {
|
|
147
|
+
return this.styles.length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Compile styles to CSS string
|
|
152
|
+
*/
|
|
153
|
+
compileToCSS(): string {
|
|
154
|
+
const cssRules: string[] = [];
|
|
155
|
+
|
|
156
|
+
for (const style of this.styles) {
|
|
157
|
+
const selector = this.buildSelector(style);
|
|
158
|
+
const properties = this.buildProperties(style.style);
|
|
159
|
+
const rule = `${selector}{${properties}}`;
|
|
160
|
+
|
|
161
|
+
if (style.atRuleType === 'media' && style.mediaText) {
|
|
162
|
+
cssRules.push(`@media ${style.mediaText}{${rule}}`);
|
|
163
|
+
} else {
|
|
164
|
+
cssRules.push(rule);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return cssRules.join('');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Build selector string from style
|
|
173
|
+
*/
|
|
174
|
+
private buildSelector(style: Style): string {
|
|
175
|
+
if (style.selectorsAdd) {
|
|
176
|
+
let selector = style.selectorsAdd;
|
|
177
|
+
if (style.state) {
|
|
178
|
+
selector += `:${style.state}`;
|
|
179
|
+
}
|
|
180
|
+
return selector;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const selectors = style.selectors.map((sel) => {
|
|
184
|
+
if (typeof sel === 'string') {
|
|
185
|
+
return sel;
|
|
186
|
+
}
|
|
187
|
+
return sel.name;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
let selector = selectors.join(', ');
|
|
191
|
+
if (style.state) {
|
|
192
|
+
selector = selectors.map((s) => `${s}:${style.state}`).join(', ');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return selector;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build CSS properties string
|
|
200
|
+
*/
|
|
201
|
+
private buildProperties(properties: CSSProperties): string {
|
|
202
|
+
return Object.entries(properties)
|
|
203
|
+
.map(([key, value]) => `${key}:${value};`)
|
|
204
|
+
.join('');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Sync changes back to page body
|
|
209
|
+
*/
|
|
210
|
+
sync(): void {
|
|
211
|
+
this.body.styles = this.styles;
|
|
212
|
+
this.body.css = this.compileToCSS();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Replace all styles
|
|
217
|
+
*/
|
|
218
|
+
replaceAll(styles: Style[]): void {
|
|
219
|
+
this.styles = styles;
|
|
220
|
+
}
|
|
221
|
+
}
|