1 #include <libcroco/cr-parser.h>
2 #include "xml/node-event-vector.h"
3 #include "xml/repr.h"
4 #include "document.h"
5 #include "sp-style-elem.h"
6 #include "attributes.h"
7 using Inkscape::XML::TEXT_NODE;
9 static void sp_style_elem_init(SPStyleElem *style_elem);
10 static void sp_style_elem_class_init(SPStyleElemClass *klass);
11 static void sp_style_elem_build(SPObject *object, SPDocument *doc, Inkscape::XML::Node *repr);
12 static void sp_style_elem_set(SPObject *object, unsigned const key, gchar const *const value);
13 static void sp_style_elem_read_content(SPObject *);
14 static Inkscape::XML::Node *sp_style_elem_write(SPObject *, Inkscape::XML::Node *, guint flags);
16 static SPObjectClass *parent_class;
18 GType
19 sp_style_elem_get_type()
20 {
21 static GType type = 0;
22 if (!type) {
23 GTypeInfo info = {
24 sizeof(SPStyleElemClass),
25 NULL, /* base_init */
26 NULL, /* base_finalize */
27 (GClassInitFunc) sp_style_elem_class_init,
28 NULL, /* class_finalize */
29 NULL, /* class_data */
30 sizeof(SPStyleElem),
31 16, /* n_preallocs */
32 (GInstanceInitFunc) sp_style_elem_init,
33 NULL, /* value_table */
34 };
35 type = g_type_register_static(SP_TYPE_OBJECT, "SPStyleElem", &info, (GTypeFlags) 0);
36 }
38 return type;
39 }
41 static void
42 sp_style_elem_class_init(SPStyleElemClass *klass)
43 {
44 parent_class = (SPObjectClass *)g_type_class_ref(SP_TYPE_OBJECT);
45 /* FIXME */
47 klass->build = sp_style_elem_build;
48 klass->set = sp_style_elem_set;
49 klass->read_content = sp_style_elem_read_content;
50 klass->write = sp_style_elem_write;
51 }
53 static void
54 sp_style_elem_init(SPStyleElem *style_elem)
55 {
56 media_set_all(style_elem->media);
57 style_elem->is_css = false;
58 }
60 static void
61 sp_style_elem_set(SPObject *object, unsigned const key, gchar const *const value)
62 {
63 g_return_if_fail(object);
64 SPStyleElem &style_elem = *SP_STYLE_ELEM(object);
66 switch (key) {
67 case SP_ATTR_TYPE: {
68 if (!value) {
69 /* TODO: `type' attribute is required. Give error message as per
70 http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. */
71 style_elem.is_css = false;
72 } else {
73 /* fixme: determine what whitespace is allowed. Will probably need to ask on SVG
74 * list; though the relevant RFC may give info on its lexer. */
75 style_elem.is_css = ( g_ascii_strncasecmp(value, "text/css", 8) == 0
76 && ( value[8] == '\0' ||
77 value[8] == ';' ) );
78 }
79 break;
80 }
82 #if 0 /* unfinished */
83 case SP_ATTR_MEDIA: {
84 parse_media(style_elem, value);
85 break;
86 }
87 #endif
89 /* title is ignored. */
90 default: {
91 if (parent_class->set) {
92 parent_class->set(object, key, value);
93 }
94 break;
95 }
96 }
97 }
99 static void
100 child_add_rm_cb(Inkscape::XML::Node *, Inkscape::XML::Node *, Inkscape::XML::Node *,
101 void *const data)
102 {
103 sp_style_elem_read_content(static_cast<SPObject *>(data));
104 }
106 static void
107 content_changed_cb(Inkscape::XML::Node *, gchar const *, gchar const *,
108 void *const data)
109 {
110 sp_style_elem_read_content(static_cast<SPObject *>(data));
111 }
113 static void
114 child_order_changed_cb(Inkscape::XML::Node *, Inkscape::XML::Node *,
115 Inkscape::XML::Node *, Inkscape::XML::Node *,
116 void *const data)
117 {
118 sp_style_elem_read_content(static_cast<SPObject *>(data));
119 }
121 static Inkscape::XML::Node *
122 sp_style_elem_write(SPObject *const object, Inkscape::XML::Node *repr, guint const flags)
123 {
124 if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) {
125 Inkscape::XML::Document *xml_doc = sp_document_repr_doc(SP_OBJECT_DOCUMENT(object));
126 repr = xml_doc->createElement("svg:style");
127 }
129 g_return_val_if_fail(object, repr);
130 SPStyleElem &style_elem = *SP_STYLE_ELEM(object);
131 if (flags & SP_OBJECT_WRITE_BUILD) {
132 g_warning("nyi: Forming <style> content for SP_OBJECT_WRITE_BUILD.");
133 /* fixme: Consider having the CRStyleSheet be a member of SPStyleElem, and then
134 pretty-print to a string s, then repr->addChild(xml_doc->createTextNode(s), NULL). */
135 }
136 if (style_elem.is_css) {
137 repr->setAttribute("type", "text/css");
138 }
139 /* todo: media */
141 if (((SPObjectClass *) parent_class)->write)
142 ((SPObjectClass *) parent_class)->write(object, repr, flags);
144 return repr;
145 }
148 /** Returns the concatenation of the content of the text children of the specified object. */
149 static GString *
150 concat_children(Inkscape::XML::Node const &repr)
151 {
152 GString *ret = g_string_sized_new(0);
153 // effic: 0 is just to catch bugs. Increase to something reasonable.
154 for (Inkscape::XML::Node const *rch = repr.firstChild(); rch != NULL; rch = rch->next()) {
155 if ( rch->type() == TEXT_NODE ) {
156 ret = g_string_append(ret, rch->content());
157 }
158 }
159 return ret;
160 }
164 /* Callbacks for SAC-style libcroco parser. */
166 enum StmtType { NO_STMT, FONT_FACE_STMT, NORMAL_RULESET_STMT };
168 struct ParseTmp
169 {
170 CRStyleSheet *const stylesheet;
171 StmtType stmtType;
172 CRStatement *currStmt;
173 unsigned magic;
174 static unsigned const ParseTmp_magic = 0x23474397; // from /dev/urandom
176 ParseTmp(CRStyleSheet *const stylesheet) :
177 stylesheet(stylesheet),
178 stmtType(NO_STMT),
179 currStmt(NULL),
180 magic(ParseTmp_magic)
181 { }
183 bool hasMagic() const {
184 return magic == ParseTmp_magic;
185 }
187 ~ParseTmp()
188 {
189 g_return_if_fail(hasMagic());
190 magic = 0;
191 }
192 };
194 static void
195 start_selector_cb(CRDocHandler *a_handler,
196 CRSelector *a_sel_list)
197 {
198 g_return_if_fail(a_handler && a_sel_list);
199 g_return_if_fail(a_handler->app_data != NULL);
200 ParseTmp &parse_tmp = *static_cast<ParseTmp *>(a_handler->app_data);
201 g_return_if_fail(parse_tmp.hasMagic());
202 if ( (parse_tmp.currStmt != NULL)
203 || (parse_tmp.stmtType != NO_STMT) ) {
204 g_warning("Expecting currStmt==NULL and stmtType==0 (NO_STMT) at start of ruleset, but found currStmt=%p, stmtType=%u",
205 static_cast<void *>(parse_tmp.currStmt), unsigned(parse_tmp.stmtType));
206 // fixme: Check whether we need to unref currStmt if non-NULL.
207 }
208 CRStatement *ruleset = cr_statement_new_ruleset(parse_tmp.stylesheet, a_sel_list, NULL, NULL);
209 g_return_if_fail(ruleset && ruleset->type == RULESET_STMT);
210 parse_tmp.stmtType = NORMAL_RULESET_STMT;
211 parse_tmp.currStmt = ruleset;
212 }
214 static void
215 end_selector_cb(CRDocHandler *a_handler,
216 CRSelector *a_sel_list)
217 {
218 g_return_if_fail(a_handler && a_sel_list);
219 g_return_if_fail(a_handler->app_data != NULL);
220 ParseTmp &parse_tmp = *static_cast<ParseTmp *>(a_handler->app_data);
221 g_return_if_fail(parse_tmp.hasMagic());
222 CRStatement *const ruleset = parse_tmp.currStmt;
223 if (parse_tmp.stmtType == NORMAL_RULESET_STMT
224 && ruleset
225 && ruleset->type == RULESET_STMT
226 && ruleset->kind.ruleset->sel_list == a_sel_list)
227 {
228 parse_tmp.stylesheet->statements = cr_statement_append(parse_tmp.stylesheet->statements,
229 ruleset);
230 } else {
231 g_warning("Found stmtType=%u, stmt=%p, stmt.type=%u, ruleset.sel_list=%p, a_sel_list=%p.",
232 unsigned(parse_tmp.stmtType),
233 ruleset,
234 unsigned(ruleset->type),
235 ruleset->kind.ruleset->sel_list,
236 a_sel_list);
237 }
238 parse_tmp.currStmt = NULL;
239 parse_tmp.stmtType = NO_STMT;
240 }
242 static void
243 start_font_face_cb(CRDocHandler *a_handler,
244 CRParsingLocation *)
245 {
246 g_return_if_fail(a_handler->app_data != NULL);
247 ParseTmp &parse_tmp = *static_cast<ParseTmp *>(a_handler->app_data);
248 g_return_if_fail(parse_tmp.hasMagic());
249 if (parse_tmp.stmtType != NO_STMT || parse_tmp.currStmt != NULL) {
250 g_warning("Expecting currStmt==NULL and stmtType==0 (NO_STMT) at start of @font-face, but found currStmt=%p, stmtType=%u",
251 static_cast<void *>(parse_tmp.currStmt), unsigned(parse_tmp.stmtType));
252 // fixme: Check whether we need to unref currStmt if non-NULL.
253 }
254 parse_tmp.stmtType = FONT_FACE_STMT;
255 parse_tmp.currStmt = NULL;
256 }
258 static void
259 end_font_face_cb(CRDocHandler *a_handler)
260 {
261 g_return_if_fail(a_handler->app_data != NULL);
262 ParseTmp &parse_tmp = *static_cast<ParseTmp *>(a_handler->app_data);
263 g_return_if_fail(parse_tmp.hasMagic());
264 if (parse_tmp.stmtType != FONT_FACE_STMT || parse_tmp.currStmt != NULL) {
265 g_warning("Expecting currStmt==NULL and stmtType==1 (FONT_FACE_STMT) at end of @font-face, but found currStmt=%p, stmtType=%u",
266 static_cast<void *>(parse_tmp.currStmt), unsigned(parse_tmp.stmtType));
267 // fixme: Check whether we need to unref currStmt if non-NULL.
268 parse_tmp.currStmt = NULL;
269 }
270 parse_tmp.stmtType = NO_STMT;
271 }
273 static void
274 property_cb(CRDocHandler *const a_handler,
275 CRString *const a_name,
276 CRTerm *const a_value, gboolean const a_important)
277 {
278 g_return_if_fail(a_handler && a_name);
279 g_return_if_fail(a_handler->app_data != NULL);
280 ParseTmp &parse_tmp = *static_cast<ParseTmp *>(a_handler->app_data);
281 g_return_if_fail(parse_tmp.hasMagic());
282 if (parse_tmp.stmtType == FONT_FACE_STMT) {
283 if (parse_tmp.currStmt != NULL) {
284 g_warning("Found non-NULL currStmt %p though stmtType==FONT_FACE_STMT.", parse_tmp.currStmt);
285 }
286 /* We currently ignore @font-face descriptors. */
287 return;
288 }
289 CRStatement *const ruleset = parse_tmp.currStmt;
290 g_return_if_fail(ruleset
291 && ruleset->type == RULESET_STMT
292 && parse_tmp.stmtType == NORMAL_RULESET_STMT);
293 CRDeclaration *const decl = cr_declaration_new(ruleset, cr_string_dup(a_name), a_value);
294 g_return_if_fail(decl);
295 decl->important = a_important;
296 CRStatus const append_status = cr_statement_ruleset_append_decl(ruleset, decl);
297 g_return_if_fail(append_status == CR_OK);
298 }
300 static void
301 sp_style_elem_read_content(SPObject *const object)
302 {
303 SPStyleElem &style_elem = *SP_STYLE_ELEM(object);
305 /* fixme: If there's more than one <style> element in a document, then the document stylesheet
306 * will be set to a random one of them, even switching between them.
307 *
308 * However, I don't see in the spec what's supposed to happen when there are multiple <style>
309 * elements. The wording suggests that <style>'s content should be a full stylesheet.
310 * http://www.w3.org/TR/REC-CSS2/cascade.html#cascade says that "The author specifies style
311 * sheets for a source document according to the conventions of the document language. For
312 * instance, in HTML, style sheets may be included in the document or linked externally."
313 * (Note the plural in both sentences.) Whereas libcroco's CRCascade allows only one author
314 * stylesheet. CRStyleSheet has no next/prev members that I can see, nor can I see any append
315 * stuff.
316 *
317 * Dodji replies "right, that's *bug*"; just an unexpected oversight.
318 */
320 GString *const text = concat_children(*style_elem.repr);
321 CRParser *parser = cr_parser_new_from_buf(reinterpret_cast<guchar *>(text->str), text->len,
322 CR_UTF_8, FALSE);
324 /* I see a cr_statement_parse_from_buf for returning a CRStatement*, but no corresponding
325 cr_stylesheet_parse_from_buf. And cr_statement_parse_from_buf takes a char*, not a
326 CRInputBuf, and doesn't provide a way for calling it in a loop over the one buffer.
327 (I.e. it doesn't tell us where it got up to in the buffer.)
329 There's also the generic cr_parser_parse_stylesheet (or just cr_parser_parse), but that
330 just calls user-supplied callbacks rather than constructing a CRStylesheet.
331 */
332 CRDocHandler *sac_handler = cr_doc_handler_new();
333 // impl: ref_count inited to 0, so cr_parser_destroy suffices to delete sac_handler.
334 g_return_if_fail(sac_handler); // out of memory
335 CRStyleSheet *const stylesheet = cr_stylesheet_new(NULL);
336 ParseTmp parse_tmp(stylesheet);
337 sac_handler->app_data = &parse_tmp;
338 sac_handler->start_selector = start_selector_cb;
339 sac_handler->end_selector = end_selector_cb;
340 sac_handler->start_font_face = start_font_face_cb;
341 sac_handler->end_font_face = end_font_face_cb;
342 sac_handler->property = property_cb;
343 /* todo: start_media, end_media. */
344 /* todo: Test error condition. */
345 cr_parser_set_sac_handler(parser, sac_handler);
346 CRStatus const parse_status = cr_parser_parse(parser);
347 g_assert(sac_handler->app_data == &parse_tmp);
348 if (parse_status == CR_OK) {
349 cr_cascade_set_sheet(style_elem.document->style_cascade, stylesheet, ORIGIN_AUTHOR);
350 } else {
351 if (parse_status != CR_PARSING_ERROR) {
352 g_printerr("parsing error code=%u\n", unsigned(parse_status));
353 /* Better than nothing. TODO: Improve libcroco's error handling. At a minimum, add a
354 strerror-like function so that we can give a string rather than an integer. */
355 /* TODO: Improve error diagnosis stuff in inkscape. We'd like a panel showing the
356 errors/warnings/unsupported features of the current document. */
357 }
358 }
359 cr_parser_destroy(parser);
360 //object->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
361 }
363 /**
364 * Does addListener(fns, data) on \a repr and all of its descendents.
365 */
366 static void
367 rec_add_listener(Inkscape::XML::Node &repr,
368 Inkscape::XML::NodeEventVector const *const fns, void *const data)
369 {
370 repr.addListener(fns, data);
371 for (Inkscape::XML::Node *child = repr.firstChild(); child != NULL; child = child->next()) {
372 rec_add_listener(*child, fns, data);
373 }
374 }
376 static void
377 sp_style_elem_build(SPObject *object, SPDocument *document, Inkscape::XML::Node *repr)
378 {
379 sp_style_elem_read_content(object);
381 sp_object_read_attr(object, "type");
382 sp_object_read_attr(object, "media");
384 static Inkscape::XML::NodeEventVector const nodeEventVector = {
385 child_add_rm_cb, // child_added
386 child_add_rm_cb, // child_removed
387 NULL, // attr_changed
388 content_changed_cb, // content_changed
389 child_order_changed_cb, // order_changed
390 };
391 rec_add_listener(*repr, &nodeEventVector, object);
393 if (((SPObjectClass *) parent_class)->build) {
394 ((SPObjectClass *) parent_class)->build(object, document, repr);
395 }
396 }
399 /*
400 Local Variables:
401 mode:c++
402 c-file-style:"stroustrup"
403 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
404 indent-tabs-mode:nil
405 fill-column:99
406 End:
407 */
408 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :