xbintsc 0.3.15 → 0.3.18
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/src/cli/main.js +4 -4
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/codegen/generator/state.js +8 -0
- package/dist/src/codegen/generator/state.js.map +1 -1
- package/dist/src/codegen/generator/tables.js +8 -0
- package/dist/src/codegen/generator/tables.js.map +1 -1
- package/dist/src/driver/bundler/graph.d.ts +8 -0
- package/dist/src/driver/bundler/graph.js +85 -0
- package/dist/src/driver/bundler/graph.js.map +1 -0
- package/dist/src/driver/bundler/merge.d.ts +11 -0
- package/dist/src/driver/bundler/merge.js +288 -0
- package/dist/src/driver/bundler/merge.js.map +1 -0
- package/dist/src/driver/bundler/resolve.d.ts +12 -0
- package/dist/src/driver/bundler/resolve.js +246 -0
- package/dist/src/driver/bundler/resolve.js.map +1 -0
- package/dist/src/driver/bundler/symbols.d.ts +15 -0
- package/dist/src/driver/bundler/symbols.js +51 -0
- package/dist/src/driver/bundler/symbols.js.map +1 -0
- package/dist/src/driver/bundler/types.d.ts +36 -0
- package/dist/src/driver/bundler/types.js +5 -0
- package/dist/src/driver/bundler/types.js.map +1 -0
- package/dist/src/driver/modules.d.ts +9 -16
- package/dist/src/driver/modules.js +8 -644
- package/dist/src/driver/modules.js.map +1 -1
- package/dist/src/extensions/catalog.js +2 -1
- package/dist/src/extensions/catalog.js.map +1 -1
- package/dist/src/extensions/gui/index.d.ts +24 -0
- package/dist/src/extensions/gui/index.js +101 -0
- package/dist/src/extensions/gui/index.js.map +1 -0
- package/dist/tests/e2e/compile-basics.test.d.ts +4 -0
- package/dist/tests/e2e/compile-basics.test.js +134 -0
- package/dist/tests/e2e/compile-basics.test.js.map +1 -0
- package/dist/tests/e2e/compile-builtins.test.d.ts +4 -0
- package/dist/tests/e2e/compile-builtins.test.js +191 -0
- package/dist/tests/e2e/compile-builtins.test.js.map +1 -0
- package/dist/tests/e2e/compile-control-flow.test.d.ts +4 -0
- package/dist/tests/e2e/compile-control-flow.test.js +190 -0
- package/dist/tests/e2e/compile-control-flow.test.js.map +1 -0
- package/dist/tests/e2e/compile-modules.test.d.ts +4 -0
- package/dist/tests/e2e/compile-modules.test.js +48 -0
- package/dist/tests/e2e/compile-modules.test.js.map +1 -0
- package/dist/tests/e2e/compile-objects-classes.test.d.ts +4 -0
- package/dist/tests/e2e/compile-objects-classes.test.js +362 -0
- package/dist/tests/e2e/compile-objects-classes.test.js.map +1 -0
- package/dist/tests/e2e/differential-helpers.d.ts +12 -0
- package/dist/tests/e2e/differential-helpers.js +16 -0
- package/dist/tests/e2e/differential-helpers.js.map +1 -0
- package/dist/tests/e2e/differential-language.test.d.ts +4 -0
- package/dist/tests/e2e/{differential.test.js → differential-language.test.js} +5 -287
- package/dist/tests/e2e/differential-language.test.js.map +1 -0
- package/dist/tests/e2e/differential-symbols.test.d.ts +4 -0
- package/dist/tests/e2e/differential-symbols.test.js +92 -0
- package/dist/tests/e2e/differential-symbols.test.js.map +1 -0
- package/dist/tests/e2e/differential-values.test.d.ts +4 -0
- package/dist/tests/e2e/differential-values.test.js +197 -0
- package/dist/tests/e2e/differential-values.test.js.map +1 -0
- package/dist/tests/e2e/gui.test.d.ts +12 -0
- package/dist/tests/e2e/gui.test.js +493 -0
- package/dist/tests/e2e/gui.test.js.map +1 -0
- package/dist/tests/e2e/runtime-coverage-buffer.test.d.ts +4 -0
- package/dist/tests/e2e/runtime-coverage-buffer.test.js +97 -0
- package/dist/tests/e2e/runtime-coverage-buffer.test.js.map +1 -0
- package/dist/tests/e2e/runtime-coverage-globals.test.d.ts +4 -0
- package/dist/tests/e2e/runtime-coverage-globals.test.js +149 -0
- package/dist/tests/e2e/runtime-coverage-globals.test.js.map +1 -0
- package/dist/tests/e2e/runtime-coverage-node-modules.test.d.ts +4 -0
- package/dist/tests/e2e/runtime-coverage-node-modules.test.js +177 -0
- package/dist/tests/e2e/runtime-coverage-node-modules.test.js.map +1 -0
- package/dist/tests/e2e/runtime-coverage-streams.test.d.ts +4 -0
- package/dist/tests/e2e/runtime-coverage-streams.test.js +161 -0
- package/dist/tests/e2e/runtime-coverage-streams.test.js.map +1 -0
- package/dist/tests/e2e/runtime-coverage-system.test.d.ts +4 -0
- package/dist/tests/e2e/runtime-coverage-system.test.js +91 -0
- package/dist/tests/e2e/runtime-coverage-system.test.js.map +1 -0
- package/dist/tests/e2e/timers.test.d.ts +7 -0
- package/dist/tests/e2e/timers.test.js +63 -0
- package/dist/tests/e2e/timers.test.js.map +1 -0
- package/package.json +2 -1
- package/runtime/ext_gui/css.cpp +743 -0
- package/runtime/ext_gui/css.h +98 -0
- package/runtime/ext_gui/document.cpp +264 -0
- package/runtime/ext_gui/document.h +89 -0
- package/runtime/ext_gui/dom.cpp +472 -0
- package/runtime/ext_gui/dom.h +71 -0
- package/runtime/ext_gui/gui.cpp +457 -0
- package/runtime/ext_gui/gui_engine.h +81 -0
- package/runtime/ext_gui/image.cpp +125 -0
- package/runtime/ext_gui/image.h +42 -0
- package/runtime/ext_gui/layout.cpp +748 -0
- package/runtime/ext_gui/layout.h +93 -0
- package/runtime/ext_gui/paint.cpp +137 -0
- package/runtime/ext_gui/paint.h +75 -0
- package/runtime/ext_gui/renderer.cpp +959 -0
- package/runtime/ext_gui/renderer.h +68 -0
- package/runtime/ext_gui/stb_image.h +7988 -0
- package/runtime/ext_gui/style.cpp +771 -0
- package/runtime/ext_gui/style.h +117 -0
- package/runtime/ext_gui/text.cpp +378 -0
- package/runtime/ext_gui/text.h +78 -0
- package/runtime/ext_gui/window.cpp +405 -0
- package/runtime/ext_node/fs/fs_common.h +13 -527
- package/runtime/ext_node/fs/parts/buffer-bridge.inc +6 -0
- package/runtime/ext_node/fs/parts/builtins.inc +69 -0
- package/runtime/ext_node/fs/parts/dir.inc +47 -0
- package/runtime/ext_node/fs/parts/encoding.inc +186 -0
- package/runtime/ext_node/fs/parts/errors.inc +66 -0
- package/runtime/ext_node/fs/parts/flags.inc +39 -0
- package/runtime/ext_node/fs/parts/platform.inc +86 -0
- package/runtime/ext_node/fs/parts/promisify.inc +15 -0
- package/runtime/ext_node/fs/parts/time.inc +9 -0
- package/runtime/rt.h +33 -0
- package/runtime/xt_builtins.c +4 -0
- package/runtime/xt_containers/access.inc +168 -0
- package/runtime/xt_containers/arrays.inc +108 -0
- package/runtime/xt_containers/boxes.inc +11 -0
- package/runtime/xt_containers/objects.inc +326 -0
- package/runtime/xt_containers.c +8 -618
- package/runtime/xt_io/base64.inc +87 -0
- package/runtime/xt_io/console.inc +119 -0
- package/runtime/xt_io/exception.inc +142 -0
- package/runtime/xt_io/inspect.inc +212 -0
- package/runtime/xt_io.c +8 -553
- package/runtime/xt_loop.c +262 -24
- package/runtime/xt_regex/api.inc +58 -0
- package/runtime/xt_regex/helpers.inc +68 -0
- package/runtime/xt_regex/matcher.inc +248 -0
- package/runtime/xt_regex/parser.inc +236 -0
- package/runtime/xt_regex/types.inc +58 -0
- package/runtime/xt_regex.h +9 -672
- package/runtime/xt_values/arithmetic.inc +150 -0
- package/runtime/xt_values/comparison.inc +112 -0
- package/runtime/xt_values/constructors.inc +41 -0
- package/runtime/xt_values/conversions.inc +115 -0
- package/runtime/xt_values/number-format.inc +180 -0
- package/runtime/xt_values/strings.inc +22 -0
- package/runtime/xt_values.c +10 -626
- package/scripts/build-gui.ts +317 -0
- package/src/cli/main.ts +4 -3
- package/src/codegen/generator/state.ts +8 -0
- package/src/codegen/generator/tables.ts +8 -0
- package/src/driver/bundler/graph.ts +95 -0
- package/src/driver/bundler/merge.ts +312 -0
- package/src/driver/bundler/resolve.ts +240 -0
- package/src/driver/bundler/symbols.ts +55 -0
- package/src/driver/bundler/types.ts +37 -0
- package/src/driver/modules.ts +9 -693
- package/src/extensions/catalog.ts +2 -1
- package/src/extensions/gui/index.ts +107 -0
- package/dist/tests/e2e/compile.test.d.ts +0 -8
- package/dist/tests/e2e/compile.test.js +0 -893
- package/dist/tests/e2e/compile.test.js.map +0 -1
- package/dist/tests/e2e/differential.test.d.ts +0 -7
- package/dist/tests/e2e/differential.test.js.map +0 -1
- package/dist/tests/e2e/runtime-coverage.test.d.ts +0 -11
- package/dist/tests/e2e/runtime-coverage.test.js +0 -646
- package/dist/tests/e2e/runtime-coverage.test.js.map +0 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — HTML tokenizer + tree builder.
|
|
3
|
+
*
|
|
4
|
+
* The tokenizer is a single pass that also swallows the contents of raw-text
|
|
5
|
+
* elements (<style>, <script>, ...). The tree builder keeps a stack of open
|
|
6
|
+
* elements and applies a small set of HTML "implied end tag" rules, which is
|
|
7
|
+
* all a UI document needs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include "dom.h"
|
|
11
|
+
|
|
12
|
+
#include <algorithm>
|
|
13
|
+
#include <cctype>
|
|
14
|
+
#include <cstdlib>
|
|
15
|
+
#include <cstring>
|
|
16
|
+
#include <unordered_map>
|
|
17
|
+
|
|
18
|
+
namespace xtgui {
|
|
19
|
+
|
|
20
|
+
/* -- Node helpers --------------------------------------------------------- */
|
|
21
|
+
|
|
22
|
+
const std::string *Node::attr(const std::string &name) const {
|
|
23
|
+
for (const Attr &entry : attrs) {
|
|
24
|
+
if (entry.name == name) return &entry.value;
|
|
25
|
+
}
|
|
26
|
+
return nullptr;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
bool Node::isTag(const char *name) const {
|
|
30
|
+
return type == NodeType::Element && tag == name;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Node *Node::append(std::unique_ptr<Node> child) {
|
|
34
|
+
child->parent = this;
|
|
35
|
+
Node *raw = child.get();
|
|
36
|
+
children.push_back(std::move(child));
|
|
37
|
+
return raw;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Node *Node::addElement(const std::string &tagName) {
|
|
41
|
+
auto node = std::make_unique<Node>(NodeType::Element);
|
|
42
|
+
node->tag = tagName;
|
|
43
|
+
return append(std::move(node));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Node *Node::addText(const std::string &data) {
|
|
47
|
+
if (data.empty()) return nullptr;
|
|
48
|
+
auto node = std::make_unique<Node>(NodeType::Text);
|
|
49
|
+
node->text = data;
|
|
50
|
+
return append(std::move(node));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Node *Node::firstElementChild() const {
|
|
54
|
+
for (const std::unique_ptr<Node> &child : children) {
|
|
55
|
+
if (child->type == NodeType::Element) return child.get();
|
|
56
|
+
}
|
|
57
|
+
return nullptr;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
int Node::elementIndex() const {
|
|
61
|
+
if (parent == nullptr) return 0;
|
|
62
|
+
int index = 0;
|
|
63
|
+
for (const std::unique_ptr<Node> &sibling : parent->children) {
|
|
64
|
+
if (sibling.get() == this) break;
|
|
65
|
+
if (sibling->type == NodeType::Element) index++;
|
|
66
|
+
}
|
|
67
|
+
return index;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
int Node::elementSiblingCount() const {
|
|
71
|
+
if (parent == nullptr) return 1;
|
|
72
|
+
int count = 0;
|
|
73
|
+
for (const std::unique_ptr<Node> &sibling : parent->children) {
|
|
74
|
+
if (sibling->type == NodeType::Element) count++;
|
|
75
|
+
}
|
|
76
|
+
return count;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* -- helpers -------------------------------------------------------------- */
|
|
80
|
+
|
|
81
|
+
static bool isAsciiAlpha(char c) {
|
|
82
|
+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static bool isWhitespace(char c) {
|
|
86
|
+
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static std::string toLower(const std::string &text) {
|
|
90
|
+
std::string out = text;
|
|
91
|
+
for (char &c : out) c = (char)std::tolower((unsigned char)c);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static std::string trim(const std::string &text) {
|
|
96
|
+
size_t begin = 0;
|
|
97
|
+
size_t end = text.size();
|
|
98
|
+
while (begin < end && isWhitespace(text[begin])) begin++;
|
|
99
|
+
while (end > begin && isWhitespace(text[end - 1])) end--;
|
|
100
|
+
return text.substr(begin, end - begin);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Decode the handful of entities that show up in UI text. */
|
|
104
|
+
static std::string decodeEntities(const std::string &text) {
|
|
105
|
+
if (text.find('&') == std::string::npos) return text;
|
|
106
|
+
std::string out;
|
|
107
|
+
out.reserve(text.size());
|
|
108
|
+
for (size_t i = 0; i < text.size(); i++) {
|
|
109
|
+
if (text[i] != '&') {
|
|
110
|
+
out.push_back(text[i]);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
size_t semi = text.find(';', i + 1);
|
|
114
|
+
if (semi == std::string::npos || semi - i > 12) {
|
|
115
|
+
out.push_back('&');
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
std::string name = text.substr(i + 1, semi - i - 1);
|
|
119
|
+
if (name == "amp") out.push_back('&');
|
|
120
|
+
else if (name == "lt") out.push_back('<');
|
|
121
|
+
else if (name == "gt") out.push_back('>');
|
|
122
|
+
else if (name == "quot") out.push_back('"');
|
|
123
|
+
else if (name == "apos") out.push_back('\'');
|
|
124
|
+
else if (name == "nbsp") out.push_back(' ');
|
|
125
|
+
else if (!name.empty() && name[0] == '#') {
|
|
126
|
+
long code = 0;
|
|
127
|
+
if (name.size() > 1 && (name[1] == 'x' || name[1] == 'X')) {
|
|
128
|
+
for (size_t k = 2; k < name.size(); k++) {
|
|
129
|
+
char digit = (char)std::tolower((unsigned char)name[k]);
|
|
130
|
+
int value = (digit >= '0' && digit <= '9') ? digit - '0'
|
|
131
|
+
: (digit >= 'a' && digit <= 'f') ? digit - 'a' + 10
|
|
132
|
+
: -1;
|
|
133
|
+
if (value < 0) break;
|
|
134
|
+
code = code * 16 + value;
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
code = strtol(name.c_str() + 1, nullptr, 10);
|
|
138
|
+
}
|
|
139
|
+
if (code > 0 && code < 128) out.push_back((char)code);
|
|
140
|
+
else out.push_back('?');
|
|
141
|
+
} else {
|
|
142
|
+
out.append(text, i, semi - i + 1);
|
|
143
|
+
i = semi;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
i = semi;
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
static const std::unordered_map<std::string, bool> &voidElements() {
|
|
152
|
+
static const std::unordered_map<std::string, bool> set = {
|
|
153
|
+
{"area", true}, {"base", true}, {"br", true}, {"col", true},
|
|
154
|
+
{"embed", true}, {"hr", true}, {"img", true}, {"input", true},
|
|
155
|
+
{"link", true}, {"meta", true}, {"param", true}, {"source", true},
|
|
156
|
+
{"track", true}, {"wbr", true},
|
|
157
|
+
};
|
|
158
|
+
return set;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Elements whose content is treated as raw text. `script` is discarded. */
|
|
162
|
+
static bool isRawText(const std::string &tag, bool *keepText) {
|
|
163
|
+
if (tag == "style" || tag == "title" || tag == "textarea") {
|
|
164
|
+
*keepText = true;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (tag == "script" || tag == "xmp" || tag == "iframe" || tag == "noembed" ||
|
|
168
|
+
tag == "noframes" || tag == "plaintext") {
|
|
169
|
+
*keepText = false;
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* -- tokenizer ------------------------------------------------------------ */
|
|
176
|
+
|
|
177
|
+
enum class TokenKind { Start, End, Text, EndOfFile };
|
|
178
|
+
|
|
179
|
+
struct Token {
|
|
180
|
+
TokenKind kind = TokenKind::EndOfFile;
|
|
181
|
+
std::string name;
|
|
182
|
+
std::vector<Attr> attrs;
|
|
183
|
+
bool selfClosing = false;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
class Tokenizer {
|
|
187
|
+
public:
|
|
188
|
+
explicit Tokenizer(const std::string &html) : html_(html) {}
|
|
189
|
+
|
|
190
|
+
std::vector<Token> run() {
|
|
191
|
+
std::vector<Token> tokens;
|
|
192
|
+
while (position_ < html_.size()) {
|
|
193
|
+
if (html_[position_] == '<' && position_ + 1 < html_.size()) {
|
|
194
|
+
char next = html_[position_ + 1];
|
|
195
|
+
if (next == '!') {
|
|
196
|
+
skipBogusComment();
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (next == '/') {
|
|
200
|
+
Token token = readEndTag();
|
|
201
|
+
if (!token.name.empty()) tokens.push_back(std::move(token));
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (isAsciiAlpha(next)) {
|
|
205
|
+
Token token = readStartTag();
|
|
206
|
+
tokens.push_back(token);
|
|
207
|
+
bool keepText = false;
|
|
208
|
+
if (isRawText(token.name, &keepText) && !token.selfClosing) {
|
|
209
|
+
readRawText(token.name, keepText, tokens);
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
std::string text = readText();
|
|
215
|
+
if (!text.empty()) {
|
|
216
|
+
Token token;
|
|
217
|
+
token.kind = TokenKind::Text;
|
|
218
|
+
token.name = decodeEntities(text);
|
|
219
|
+
tokens.push_back(std::move(token));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return tokens;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private:
|
|
226
|
+
const std::string &html_;
|
|
227
|
+
size_t position_ = 0;
|
|
228
|
+
|
|
229
|
+
void skipBogusComment() {
|
|
230
|
+
if (html_.compare(position_, 4, "<!--") == 0) {
|
|
231
|
+
size_t end = html_.find("-->", position_ + 4);
|
|
232
|
+
position_ = (end == std::string::npos) ? html_.size() : end + 3;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
size_t end = html_.find('>', position_);
|
|
236
|
+
position_ = (end == std::string::npos) ? html_.size() : end + 1;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
std::string readText() {
|
|
240
|
+
size_t start = position_;
|
|
241
|
+
while (position_ < html_.size() && html_[position_] != '<') position_++;
|
|
242
|
+
return html_.substr(start, position_ - start);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
std::string readName() {
|
|
246
|
+
size_t start = position_;
|
|
247
|
+
while (position_ < html_.size()) {
|
|
248
|
+
char c = html_[position_];
|
|
249
|
+
if (isWhitespace(c) || c == '/' || c == '>') break;
|
|
250
|
+
position_++;
|
|
251
|
+
}
|
|
252
|
+
return toLower(html_.substr(start, position_ - start));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
void skipWhitespace() {
|
|
256
|
+
while (position_ < html_.size() && isWhitespace(html_[position_])) position_++;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
Token readStartTag() {
|
|
260
|
+
Token token;
|
|
261
|
+
token.kind = TokenKind::Start;
|
|
262
|
+
position_++; // consume '<'
|
|
263
|
+
token.name = readName();
|
|
264
|
+
while (position_ < html_.size()) {
|
|
265
|
+
skipWhitespace();
|
|
266
|
+
if (position_ >= html_.size()) break;
|
|
267
|
+
char c = html_[position_];
|
|
268
|
+
if (c == '>') {
|
|
269
|
+
position_++;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
if (c == '/') {
|
|
273
|
+
token.selfClosing = true;
|
|
274
|
+
position_++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
readAttribute(token);
|
|
278
|
+
}
|
|
279
|
+
return token;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
void readAttribute(Token &token) {
|
|
283
|
+
size_t start = position_;
|
|
284
|
+
while (position_ < html_.size()) {
|
|
285
|
+
char c = html_[position_];
|
|
286
|
+
if (isWhitespace(c) || c == '=' || c == '>' || c == '/') break;
|
|
287
|
+
position_++;
|
|
288
|
+
}
|
|
289
|
+
std::string name = toLower(html_.substr(start, position_ - start));
|
|
290
|
+
if (name.empty()) {
|
|
291
|
+
position_++;
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
skipWhitespace();
|
|
295
|
+
std::string value;
|
|
296
|
+
if (position_ < html_.size() && html_[position_] == '=') {
|
|
297
|
+
position_++;
|
|
298
|
+
skipWhitespace();
|
|
299
|
+
if (position_ < html_.size() && (html_[position_] == '"' || html_[position_] == '\'')) {
|
|
300
|
+
char quote = html_[position_++];
|
|
301
|
+
size_t begin = position_;
|
|
302
|
+
while (position_ < html_.size() && html_[position_] != quote) position_++;
|
|
303
|
+
value = html_.substr(begin, position_ - begin);
|
|
304
|
+
if (position_ < html_.size()) position_++;
|
|
305
|
+
} else {
|
|
306
|
+
size_t begin = position_;
|
|
307
|
+
while (position_ < html_.size() && !isWhitespace(html_[position_]) && html_[position_] != '>') position_++;
|
|
308
|
+
value = html_.substr(begin, position_ - begin);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
token.attrs.push_back({name, decodeEntities(value)});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
Token readEndTag() {
|
|
315
|
+
Token token;
|
|
316
|
+
token.kind = TokenKind::End;
|
|
317
|
+
position_ += 2; // consume '</'
|
|
318
|
+
token.name = readName();
|
|
319
|
+
size_t end = html_.find('>', position_);
|
|
320
|
+
position_ = (end == std::string::npos) ? html_.size() : end + 1;
|
|
321
|
+
return token;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
void readRawText(const std::string &tag, bool keepText, std::vector<Token> &tokens) {
|
|
325
|
+
size_t start = position_;
|
|
326
|
+
std::string close = "</" + tag;
|
|
327
|
+
while (position_ < html_.size()) {
|
|
328
|
+
if (html_[position_] == '<' && toLower(html_.substr(position_, close.size())) == close) {
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
position_++;
|
|
332
|
+
}
|
|
333
|
+
std::string raw = html_.substr(start, position_ - start);
|
|
334
|
+
if (keepText && !raw.empty()) {
|
|
335
|
+
Token text;
|
|
336
|
+
text.kind = TokenKind::Text;
|
|
337
|
+
text.name = raw;
|
|
338
|
+
tokens.push_back(std::move(text));
|
|
339
|
+
}
|
|
340
|
+
if (position_ < html_.size()) {
|
|
341
|
+
Token end = readEndTag();
|
|
342
|
+
tokens.push_back(std::move(end));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/* -- tree builder --------------------------------------------------------- */
|
|
348
|
+
|
|
349
|
+
namespace {
|
|
350
|
+
|
|
351
|
+
bool isBlockTag(const std::string &tag) {
|
|
352
|
+
static const std::unordered_map<std::string, bool> set = {
|
|
353
|
+
{"address", true}, {"article", true}, {"aside", true}, {"blockquote", true},
|
|
354
|
+
{"div", true}, {"dl", true}, {"fieldset", true}, {"figure", true},
|
|
355
|
+
{"footer", true}, {"form", true}, {"h1", true}, {"h2", true},
|
|
356
|
+
{"h3", true}, {"h4", true}, {"h5", true}, {"h6", true},
|
|
357
|
+
{"header", true}, {"hr", true}, {"main", true}, {"nav", true},
|
|
358
|
+
{"ol", true}, {"p", true}, {"pre", true}, {"section", true},
|
|
359
|
+
{"table", true}, {"ul", true},
|
|
360
|
+
};
|
|
361
|
+
return set.count(tag) != 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
bool isHeading(const std::string &tag) {
|
|
365
|
+
return tag.size() == 2 && tag[0] == 'h' && tag[1] >= '1' && tag[1] <= '6';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Whether a start tag implicitly closes the currently open element. */
|
|
369
|
+
bool closesOpenElement(const std::string &open, const std::string &next) {
|
|
370
|
+
if (open.empty()) return false;
|
|
371
|
+
if (next == "li" && open == "li") return true;
|
|
372
|
+
if ((next == "dt" || next == "dd") && (open == "dt" || open == "dd")) return true;
|
|
373
|
+
if (next == "option" && open == "option") return true;
|
|
374
|
+
if (next == "p" && open == "p") return true;
|
|
375
|
+
if (isHeading(next) && (isHeading(open) || open == "p")) return true;
|
|
376
|
+
if (isBlockTag(next) && open == "p") return true;
|
|
377
|
+
if ((next == "td" || next == "th") && (open == "td" || open == "th")) return true;
|
|
378
|
+
if (next == "tr" && (open == "td" || open == "th" || open == "tr")) return true;
|
|
379
|
+
if ((next == "thead" || next == "tbody" || next == "tfoot") &&
|
|
380
|
+
(open == "td" || open == "th" || open == "tr" || open == "thead" ||
|
|
381
|
+
open == "tbody" || open == "tfoot")) {
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
} // namespace
|
|
388
|
+
|
|
389
|
+
std::unique_ptr<Node> xt_html_parse(const std::string &html) {
|
|
390
|
+
Tokenizer tokenizer(html);
|
|
391
|
+
std::vector<Token> tokens = tokenizer.run();
|
|
392
|
+
|
|
393
|
+
auto document = std::make_unique<Node>(NodeType::Document);
|
|
394
|
+
std::vector<Node *> stack;
|
|
395
|
+
stack.push_back(document.get());
|
|
396
|
+
|
|
397
|
+
for (Token &token : tokens) {
|
|
398
|
+
if (token.kind == TokenKind::Text) {
|
|
399
|
+
Node *current = stack.back();
|
|
400
|
+
/* Text inside <style>/<title> is kept verbatim; elsewhere keep it too so
|
|
401
|
+
* layout can collapse whitespace later. */
|
|
402
|
+
current->addText(token.name);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (token.kind == TokenKind::Start) {
|
|
406
|
+
while (stack.size() > 1 && closesOpenElement(stack.back()->tag, token.name)) {
|
|
407
|
+
stack.pop_back();
|
|
408
|
+
}
|
|
409
|
+
Node *element = stack.back()->addElement(token.name);
|
|
410
|
+
element->attrs = std::move(token.attrs);
|
|
411
|
+
bool isVoid = voidElements().count(token.name) != 0;
|
|
412
|
+
if (!isVoid && !token.selfClosing) stack.push_back(element);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
if (token.kind == TokenKind::End) {
|
|
416
|
+
for (size_t i = stack.size(); i-- > 1;) {
|
|
417
|
+
if (stack[i]->tag == token.name) {
|
|
418
|
+
stack.resize(i);
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return document;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* -- serialization (diagnostics) ------------------------------------------ */
|
|
429
|
+
|
|
430
|
+
static void serialize(const Node *node, std::string &out) {
|
|
431
|
+
if (node->type == NodeType::Document) {
|
|
432
|
+
for (const std::unique_ptr<Node> &child : node->children) serialize(child.get(), out);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (node->type == NodeType::Text) {
|
|
436
|
+
out += trim(node->text);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
out += "<" + node->tag;
|
|
440
|
+
for (const Attr &attr : node->attrs) out += " " + attr.name + "=\"" + attr.value + "\"";
|
|
441
|
+
out += ">";
|
|
442
|
+
for (const std::unique_ptr<Node> &child : node->children) serialize(child.get(), out);
|
|
443
|
+
out += "</" + node->tag + ">";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
std::string xt_dom_to_string(const Node *node) {
|
|
447
|
+
std::string out;
|
|
448
|
+
if (node != nullptr) serialize(node, out);
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
std::string xt_dom_describe(const Node *node) {
|
|
453
|
+
if (node == nullptr) return "";
|
|
454
|
+
if (node->isText()) return "#text";
|
|
455
|
+
if (!node->isElement()) return "#document";
|
|
456
|
+
std::string out = node->tag;
|
|
457
|
+
const std::string *id = node->attr("id");
|
|
458
|
+
if (id != nullptr && !id->empty()) out += "#" + *id;
|
|
459
|
+
const std::string *classes = node->attr("class");
|
|
460
|
+
if (classes != nullptr) {
|
|
461
|
+
size_t index = 0;
|
|
462
|
+
while (index < classes->size()) {
|
|
463
|
+
while (index < classes->size() && std::isspace((unsigned char)(*classes)[index])) index++;
|
|
464
|
+
size_t start = index;
|
|
465
|
+
while (index < classes->size() && !std::isspace((unsigned char)(*classes)[index])) index++;
|
|
466
|
+
if (index > start) out += "." + classes->substr(start, index - start);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
} // namespace xtgui
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — HTML parsing and the DOM tree.
|
|
3
|
+
*
|
|
4
|
+
* This is a deliberately small, self-written HTML subset: enough for a UI
|
|
5
|
+
* description (elements, attributes, text, comments, raw-text elements such as
|
|
6
|
+
* <style>) without the full HTML5 tree-construction algorithm. It never runs
|
|
7
|
+
* scripts; `<script>` content is discarded, `<style>` content is kept so the
|
|
8
|
+
* CSS pass can read it.
|
|
9
|
+
*/
|
|
10
|
+
#ifndef XT_GUI_DOM_H
|
|
11
|
+
#define XT_GUI_DOM_H
|
|
12
|
+
|
|
13
|
+
#include <memory>
|
|
14
|
+
#include <string>
|
|
15
|
+
#include <vector>
|
|
16
|
+
|
|
17
|
+
namespace xtgui {
|
|
18
|
+
|
|
19
|
+
enum class NodeType { Document, Element, Text };
|
|
20
|
+
|
|
21
|
+
struct Attr {
|
|
22
|
+
std::string name;
|
|
23
|
+
std::string value;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** A DOM node. Element/text/document are one type to keep the tree simple. */
|
|
27
|
+
struct Node {
|
|
28
|
+
NodeType type = NodeType::Element;
|
|
29
|
+
std::string tag; // lowercased element name (empty for document/text)
|
|
30
|
+
std::string text; // text content (text nodes only)
|
|
31
|
+
std::vector<Attr> attrs;
|
|
32
|
+
Node *parent = nullptr;
|
|
33
|
+
std::vector<std::unique_ptr<Node>> children;
|
|
34
|
+
|
|
35
|
+
Node() = default;
|
|
36
|
+
explicit Node(NodeType nodeType) : type(nodeType) {}
|
|
37
|
+
|
|
38
|
+
bool isDocument() const { return type == NodeType::Document; }
|
|
39
|
+
bool isElement() const { return type == NodeType::Element; }
|
|
40
|
+
bool isText() const { return type == NodeType::Text; }
|
|
41
|
+
|
|
42
|
+
/** Attribute value, or NULL when the attribute is absent. */
|
|
43
|
+
const std::string *attr(const std::string &name) const;
|
|
44
|
+
/** Element name comparison (`<div>` → `isTag("div")`). */
|
|
45
|
+
bool isTag(const char *name) const;
|
|
46
|
+
|
|
47
|
+
/** Create a child element/text node, append it and return it. */
|
|
48
|
+
Node *addElement(const std::string &tag);
|
|
49
|
+
Node *addText(const std::string &data);
|
|
50
|
+
Node *append(std::unique_ptr<Node> child);
|
|
51
|
+
|
|
52
|
+
/** First element child (skips text), or NULL. */
|
|
53
|
+
Node *firstElementChild() const;
|
|
54
|
+
/** Total number of element children with the same tag, and this node's index. */
|
|
55
|
+
int elementIndex() const;
|
|
56
|
+
int elementSiblingCount() const;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Parse an HTML document/fragment. Returns a Document node (never NULL). */
|
|
60
|
+
std::unique_ptr<Node> xt_html_parse(const std::string &html);
|
|
61
|
+
|
|
62
|
+
/** Serialize a subtree; used by diagnostics and tests. */
|
|
63
|
+
std::string xt_dom_to_string(const Node *node);
|
|
64
|
+
|
|
65
|
+
/** A short CSS-like descriptor for a node, e.g. `div#main.card` (events and
|
|
66
|
+
* diagnostics). Text/document nodes use `#text` / `#document`. */
|
|
67
|
+
std::string xt_dom_describe(const Node *node);
|
|
68
|
+
|
|
69
|
+
} // namespace xtgui
|
|
70
|
+
|
|
71
|
+
#endif /* XT_GUI_DOM_H */
|