diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/object/sp-style-elem.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-upstream.tar.xz inkscape-upstream.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/object/sp-style-elem.cpp | 579 |
1 files changed, 579 insertions, 0 deletions
diff --git a/src/object/sp-style-elem.cpp b/src/object/sp-style-elem.cpp new file mode 100644 index 0000000..412c0ef --- /dev/null +++ b/src/object/sp-style-elem.cpp @@ -0,0 +1,579 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <3rdparty/libcroco/cr-parser.h> +#include "xml/node-event-vector.h" +#include "xml/repr.h" +#include "document.h" +#include "sp-style-elem.h" +#include "sp-root.h" +#include "attributes.h" +#include "style.h" + +// For external style sheets +#include "io/resource.h" +#include <iostream> +#include <fstream> + +// For font-rule +#include "libnrtype/FontFactory.h" + +SPStyleElem::SPStyleElem() : SPObject() { + media_set_all(this->media); + this->is_css = false; + this->style_sheet = nullptr; +} + +SPStyleElem::~SPStyleElem() = default; + +void SPStyleElem::set(SPAttr key, const gchar* value) { + switch (key) { + case SPAttr::TYPE: { + if (!value) { + /* TODO: `type' attribute is required. Give error message as per + http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. */ + is_css = false; + } else { + /* fixme: determine what whitespace is allowed. Will probably need to ask on SVG + list; though the relevant RFC may give info on its lexer. */ + is_css = ( g_ascii_strncasecmp(value, "text/css", 8) == 0 + && ( value[8] == '\0' || + value[8] == ';' ) ); + } + break; + } + +#if 0 /* unfinished */ + case SPAttr::MEDIA: { + parse_media(style_elem, value); + break; + } +#endif + + /* title is ignored. */ + default: { + SPObject::set(key, value); + break; + } + } +} + +/** + * Callback for changing the content of a <style> child text node + */ +static void content_changed_cb(Inkscape::XML::Node *, gchar const *, gchar const *, void *const data) +{ + SPObject *obj = reinterpret_cast<SPObject *>(data); + g_assert(data != nullptr); + obj->read_content(); + obj->document->getRoot()->emitModified(SP_OBJECT_MODIFIED_CASCADE); +} + +static Inkscape::XML::NodeEventVector const textNodeEventVector = { + nullptr, // child_added + nullptr, // child_removed + nullptr, // attr_changed + content_changed_cb, // content_changed + nullptr, // order_changed +}; + +/** + * Callback for adding a text child to a <style> node + */ +static void child_add_cb(Inkscape::XML::Node *, Inkscape::XML::Node *child, Inkscape::XML::Node *, void *const data) +{ + SPObject *obj = reinterpret_cast<SPObject *>(data); + g_assert(data != nullptr); + + if (child->type() == Inkscape::XML::NodeType::TEXT_NODE) { + child->addListener(&textNodeEventVector, obj); + } + + obj->read_content(); +} + +/** + * Callback for removing a (text) child from a <style> node + */ +static void child_rm_cb(Inkscape::XML::Node *, Inkscape::XML::Node *child, Inkscape::XML::Node *, void *const data) +{ + SPObject *obj = reinterpret_cast<SPObject *>(data); + + child->removeListenerByData(obj); + + obj->read_content(); +} + +/** + * Callback for rearranging text nodes inside a <style> node + */ +static void +child_order_changed_cb(Inkscape::XML::Node *, Inkscape::XML::Node *, + Inkscape::XML::Node *, Inkscape::XML::Node *, + void *const data) +{ + SPObject *obj = reinterpret_cast<SPObject *>(data); + g_assert(data != nullptr); + obj->read_content(); +} + +Inkscape::XML::Node* SPStyleElem::write(Inkscape::XML::Document* xml_doc, Inkscape::XML::Node* repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("svg:style"); + } + + if (flags & SP_OBJECT_WRITE_BUILD) { + g_warning("nyi: Forming <style> content for SP_OBJECT_WRITE_BUILD."); + /* fixme: Consider having the CRStyleSheet be a member of SPStyleElem, and then + pretty-print to a string s, then repr->addChild(xml_doc->createTextNode(s), NULL). */ + } + if (is_css) { + repr->setAttribute("type", "text/css"); + } + /* todo: media */ + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + + +/** Returns the concatenation of the content of the text children of the specified object. */ +static Glib::ustring +concat_children(Inkscape::XML::Node const &repr) +{ + Glib::ustring ret; + // effic: Initialising ret to a reasonable starting size could speed things up. + for (Inkscape::XML::Node const *rch = repr.firstChild(); rch != nullptr; rch = rch->next()) { + if ( rch->type() == Inkscape::XML::NodeType::TEXT_NODE ) { + ret += rch->content(); + } + } + return ret; +} + + + +/* Callbacks for SAC-style libcroco parser. */ + +enum StmtType { NO_STMT, FONT_FACE_STMT, NORMAL_RULESET_STMT }; + +/** + * Helper class which owns the parser and tracks the current statement + */ +struct ParseTmp +{ +private: + static constexpr unsigned ParseTmp_magic = 0x23474397; // from /dev/urandom + unsigned const magic = ParseTmp_magic; + +public: + CRParser *const parser; + CRStyleSheet *const stylesheet; + SPDocument *const document; // Need file location for '@import' + + // Current statement + StmtType stmtType = NO_STMT; + CRStatement *currStmt = nullptr; + + ParseTmp() = delete; + ParseTmp(ParseTmp const &) = delete; + ParseTmp(CRStyleSheet *const stylesheet, SPDocument *const document); + ~ParseTmp() { cr_parser_destroy(parser); } + + /** + * @pre a_handler->app_data points to a ParseTmp instance + */ + static ParseTmp *cast(CRDocHandler *a_handler) + { + assert(a_handler && a_handler->app_data); + auto self = static_cast<ParseTmp *>(a_handler->app_data); + assert(self->magic == ParseTmp_magic); + return self; + } +}; + +static void +import_style_cb (CRDocHandler *a_handler, + GList *a_media_list, + CRString *a_uri, + CRString *a_uri_default_ns, + CRParsingLocation *a_location) +{ + /* a_uri_default_ns is set to NULL and is unused by libcroco */ + + // Get document + g_return_if_fail(a_handler && a_uri); + auto &parse_tmp = *ParseTmp::cast(a_handler); + + SPDocument* document = parse_tmp.document; + if (!document) { + std::cerr << "import_style_cb: No document!" << std::endl; + return; + } + if (!document->getDocumentFilename()) { + std::cerr << "import_style_cb: Document filename is NULL" << std::endl; + return; + } + + // Get file + auto import_file = + Inkscape::IO::Resource::get_filename (document->getDocumentFilename(), a_uri->stryng->str); + + // Parse file + CRStyleSheet *stylesheet = cr_stylesheet_new (nullptr); + ParseTmp parse_new(stylesheet, document); + CRStatus const parse_status = + cr_parser_parse_file(parse_new.parser, reinterpret_cast<const guchar *>(import_file.c_str()), CR_UTF_8); + if (parse_status == CR_OK) { + g_assert(parse_tmp.stylesheet); + g_assert(parse_tmp.stylesheet != stylesheet); + stylesheet->origin = ORIGIN_AUTHOR; + // Append import statement + CRStatement *ruleset = + cr_statement_new_at_import_rule(parse_tmp.stylesheet, cr_string_dup(a_uri), nullptr, stylesheet); + parse_tmp.stylesheet->statements = cr_statement_append(parse_tmp.stylesheet->statements, ruleset); + } else { + std::cerr << "import_style_cb: Could not parse: " << import_file << std::endl; + cr_stylesheet_destroy (stylesheet); + } +}; + +static void +start_selector_cb(CRDocHandler *a_handler, + CRSelector *a_sel_list) +{ + g_return_if_fail(a_handler && a_sel_list); + auto &parse_tmp = *ParseTmp::cast(a_handler); + + if ( (parse_tmp.currStmt != nullptr) + || (parse_tmp.stmtType != NO_STMT) ) { + g_warning("Expecting currStmt==NULL and stmtType==0 (NO_STMT) at start of ruleset, but found currStmt=%p, stmtType=%u", + static_cast<void *>(parse_tmp.currStmt), unsigned(parse_tmp.stmtType)); + // fixme: Check whether we need to unref currStmt if non-NULL. + } + CRStatement *ruleset = cr_statement_new_ruleset(parse_tmp.stylesheet, a_sel_list, nullptr, nullptr); + g_return_if_fail(ruleset && ruleset->type == RULESET_STMT); + parse_tmp.stmtType = NORMAL_RULESET_STMT; + parse_tmp.currStmt = ruleset; +} + +static void +end_selector_cb(CRDocHandler *a_handler, + CRSelector *a_sel_list) +{ + g_return_if_fail(a_handler && a_sel_list); + auto &parse_tmp = *ParseTmp::cast(a_handler); + + CRStatement *const ruleset = parse_tmp.currStmt; + if (parse_tmp.stmtType == NORMAL_RULESET_STMT + && ruleset + && ruleset->type == RULESET_STMT + && ruleset->kind.ruleset->sel_list == a_sel_list) + { + parse_tmp.stylesheet->statements = cr_statement_append(parse_tmp.stylesheet->statements, + ruleset); + } else { + g_warning("Found stmtType=%u, stmt=%p, stmt.type=%u, ruleset.sel_list=%p, a_sel_list=%p.", + unsigned(parse_tmp.stmtType), + ruleset, + unsigned(ruleset->type), + ruleset->kind.ruleset->sel_list, + a_sel_list); + } + parse_tmp.currStmt = nullptr; + parse_tmp.stmtType = NO_STMT; +} + +static void +start_font_face_cb(CRDocHandler *a_handler, + CRParsingLocation *) +{ + auto &parse_tmp = *ParseTmp::cast(a_handler); + + if (parse_tmp.stmtType != NO_STMT || parse_tmp.currStmt != nullptr) { + g_warning("Expecting currStmt==NULL and stmtType==0 (NO_STMT) at start of @font-face, but found currStmt=%p, stmtType=%u", + static_cast<void *>(parse_tmp.currStmt), unsigned(parse_tmp.stmtType)); + // fixme: Check whether we need to unref currStmt if non-NULL. + } + CRStatement *font_face_rule = cr_statement_new_at_font_face_rule (parse_tmp.stylesheet, nullptr); + g_return_if_fail(font_face_rule && font_face_rule->type == AT_FONT_FACE_RULE_STMT); + parse_tmp.stmtType = FONT_FACE_STMT; + parse_tmp.currStmt = font_face_rule; +} + +static void +end_font_face_cb(CRDocHandler *a_handler) +{ + auto &parse_tmp = *ParseTmp::cast(a_handler); + + CRStatement *const font_face_rule = parse_tmp.currStmt; + if (parse_tmp.stmtType == FONT_FACE_STMT + && font_face_rule + && font_face_rule->type == AT_FONT_FACE_RULE_STMT) + { + parse_tmp.stylesheet->statements = cr_statement_append(parse_tmp.stylesheet->statements, + font_face_rule); + } else { + g_warning("Found stmtType=%u, stmt=%p, stmt.type=%u.", + unsigned(parse_tmp.stmtType), + font_face_rule, + unsigned(font_face_rule->type)); + } + + g_warning("end_font_face_cb: font face rule limited support."); + cr_declaration_dump(font_face_rule->kind.font_face_rule->decl_list, stderr, 2, TRUE); + std::cerr << std::endl; + + // Get document + SPDocument* document = parse_tmp.document; + if (!document) { + std::cerr << "end_font_face_cb: No document!" << std::endl; + return; + } + if (!document->getDocumentFilename()) { + std::cerr << "end_font_face_cb: Document filename is NULL" << std::endl; + return; + } + + // Add ttf or otf fonts. + CRDeclaration const *cur = nullptr; + for (cur = font_face_rule->kind.font_face_rule->decl_list; cur; cur = cur->next) { + if (cur->property && + cur->property->stryng && + cur->property->stryng->str && + strcmp(cur->property->stryng->str, "src") == 0 ) { + + if (cur->value && + cur->value->content.str && + cur->value->content.str->stryng && + cur->value->content.str->stryng->str) { + + Glib::ustring value = cur->value->content.str->stryng->str; + + if (value.rfind("ttf") == (value.length() - 3) || + value.rfind("otf") == (value.length() - 3)) { + + // Get file + Glib::ustring ttf_file = + Inkscape::IO::Resource::get_filename (document->getDocumentFilename(), value); + + if (!ttf_file.empty()) { + font_factory *factory = font_factory::Default(); + factory->AddFontFile( ttf_file.c_str() ); + g_info("end_font_face_cb: Added font: %s", ttf_file.c_str()); + + // FIX ME: Need to refresh font list. + } else { + g_error("end_font_face_cb: Failed to add: %s", value.c_str()); + } + } + } + } + } + + parse_tmp.currStmt = nullptr; + parse_tmp.stmtType = NO_STMT; + +} + +static void +property_cb(CRDocHandler *const a_handler, + CRString *const a_name, + CRTerm *const a_value, gboolean const a_important) +{ + // std::cerr << "property_cb: Entrance: " << a_name->stryng->str << ": " << cr_term_to_string(a_value) << std::endl; + g_return_if_fail(a_handler && a_name); + auto &parse_tmp = *ParseTmp::cast(a_handler); + + CRStatement *const ruleset = parse_tmp.currStmt; + g_return_if_fail(ruleset); + + CRDeclaration *const decl = cr_declaration_new (ruleset, cr_string_dup(a_name), a_value); + g_return_if_fail(decl); + decl->important = a_important; + + switch (parse_tmp.stmtType) { + + case NORMAL_RULESET_STMT: { + g_return_if_fail (ruleset->type == RULESET_STMT); + CRStatus const append_status = cr_statement_ruleset_append_decl (ruleset, decl); + g_return_if_fail (append_status == CR_OK); + break; + } + case FONT_FACE_STMT: { + g_return_if_fail (ruleset->type == AT_FONT_FACE_RULE_STMT); + CRDeclaration *new_decls = cr_declaration_append (ruleset->kind.font_face_rule->decl_list, decl); + g_return_if_fail (new_decls); + ruleset->kind.font_face_rule->decl_list = new_decls; + break; + } + default: + g_warning ("property_cb: Unhandled stmtType: %u", parse_tmp.stmtType); + return; + } +} + +ParseTmp::ParseTmp(CRStyleSheet *const stylesheet, SPDocument *const document) + : parser(cr_parser_new(nullptr)) + , stylesheet(stylesheet) + , document(document) +{ + CRDocHandler *sac_handler = cr_doc_handler_new(); + sac_handler->app_data = this; + sac_handler->import_style = import_style_cb; + sac_handler->start_selector = start_selector_cb; + sac_handler->end_selector = end_selector_cb; + sac_handler->start_font_face = start_font_face_cb; + sac_handler->end_font_face = end_font_face_cb; + sac_handler->property = property_cb; + cr_parser_set_sac_handler(parser, sac_handler); + cr_doc_handler_unref(sac_handler); +} + +/** + * Get the list of styles. + * Currently only used for testing. + */ +std::vector<std::unique_ptr<SPStyle>> SPStyleElem::get_styles() const +{ + std::vector<std::unique_ptr<SPStyle>> styles; + + if (style_sheet) { + auto count = cr_stylesheet_nr_rules(style_sheet); + for (int x = 0; x < count; ++x) { + CRStatement *statement = cr_stylesheet_statement_get_from_list(style_sheet, x); + styles.emplace_back(new SPStyle(document)); + styles.back()->mergeStatement(statement); + } + } + + return styles; +} + +/** + * Remove `style_sheet` from the document style cascade and destroy it. + * @post style_sheet is NULL + */ +static void clear_style_sheet(SPStyleElem &self) +{ + if (!self.style_sheet) { + return; + } + + auto *next = self.style_sheet->next; + auto *cascade = self.document->getStyleCascade(); + auto *topsheet = cr_cascade_get_sheet(cascade, ORIGIN_AUTHOR); + + cr_stylesheet_unlink(self.style_sheet); + + if (topsheet == self.style_sheet) { + // will unref style_sheet + cr_cascade_set_sheet(cascade, next, ORIGIN_AUTHOR); + } else if (!topsheet) { + cr_stylesheet_unref(self.style_sheet); + } + + self.style_sheet = nullptr; +} + +void SPStyleElem::read_content() { + // TODO On modification (observer callbacks), clearing and re-appending to + // the cascade can change the position of a stylesheet relative to other + // sheets in the document. We need a better way to update a style sheet + // which preserves the position. + clear_style_sheet(*this); + + // First, create the style-sheet object and track it in this + // element so that it can be edited. It'll be combined with + // the document's style sheet later. + style_sheet = cr_stylesheet_new (nullptr); + + ParseTmp parse_tmp(style_sheet, document); + + //XML Tree being used directly here while it shouldn't be. + Glib::ustring const text = concat_children(*getRepr()); + if (!(text.find_first_not_of(" \t\r\n") != std::string::npos)) { + return; + } + CRStatus const parse_status = + cr_parser_parse_buf(parse_tmp.parser, reinterpret_cast<const guchar *>(text.c_str()), text.bytes(), CR_UTF_8); + + if (parse_status == CR_OK) { + auto *cascade = document->getStyleCascade(); + auto *topsheet = cr_cascade_get_sheet(cascade, ORIGIN_AUTHOR); + if (!topsheet) { + // if the style is the first style sheet that we've seen, set the document's + // first style sheet to this style and create a cascade object with it. + cr_cascade_set_sheet(cascade, style_sheet, ORIGIN_AUTHOR); + } else { + // If not the first, then chain up this style_sheet + cr_stylesheet_append_stylesheet(topsheet, style_sheet); + } + } else { + cr_stylesheet_destroy (style_sheet); + style_sheet = nullptr; + if (parse_status != CR_PARSING_ERROR) { + g_printerr("parsing error code=%u\n", unsigned(parse_status)); + } + } + // If style sheet has changed, we need to cascade the entire object tree, top down + // Get root, read style, loop through children + document->getRoot()->requestDisplayUpdate(SP_OBJECT_STYLESHEET_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_MODIFIED_FLAG); +} + +static Inkscape::XML::NodeEventVector const nodeEventVector = { + child_add_cb, // child_added + child_rm_cb, // child_removed + nullptr, // attr_changed + nullptr, // content_changed + child_order_changed_cb, // order_changed +}; + +void SPStyleElem::build(SPDocument *document, Inkscape::XML::Node *repr) { + read_content(); + + readAttr(SPAttr::TYPE); +#if 0 + readAttr( "media" ); +#endif + + repr->addListener(&nodeEventVector, this); + for (Inkscape::XML::Node *child = repr->firstChild(); child != nullptr; child = child->next()) { + if (child->type() == Inkscape::XML::NodeType::TEXT_NODE) { + child->addListener(&textNodeEventVector, this); + } + } + + SPObject::build(document, repr); +} + +void SPStyleElem::release() { + getRepr()->removeListenerByData(this); + for (Inkscape::XML::Node *child = getRepr()->firstChild(); child != nullptr; child = child->next()) { + child->removeListenerByData(this); + } + + clear_style_sheet(*this); + + SPObject::release(); +} + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : |