/* clean.c -- clean up misuse of presentation markup (c) 1998 (W3C) MIT, INRIA, Keio University See tidy.c for the copyright notice. Filters from other formats such as Microsoft Word often make excessive use of presentation markup such as font tags, B, I, and the align attribute. By applying a set of production rules, it is straight forward to transform this to use CSS. Some rules replace some of the children of an element by style properties on the element, e.g.

...

->

...

Such rules are applied to the element's content and then to the element itself until none of the rules more apply. Having applied all the rules to an element, it will have a style attribute with one or more properties. Other rules strip the element they apply to, replacing it by style properties on the contents, e.g.
  • ...

  • ->

    ... These rules are applied to an element before processing its content and replace the current element by the first element in the exposed content. After applying both sets of rules, you can replace the style attribute by a class value and style rule in the document head. To support this, an association of styles and class names is built. A naive approach is to rely on string matching to test when two property lists are the same. A better approach would be to first sort the properties before matching. */ #include #include #include #include "platform.h" #include "html.h" Node *CleanNode(Lexer *lexer, Node *node); void FreeStyleProps(StyleProp *props) { StyleProp *next; while (props) { next = props->next; MemFree(props->name); MemFree(props->value); MemFree(props); props = next; } } StyleProp *InsertProperty(StyleProp *props, char *name, char *value) { StyleProp *first, *prev, *prop; int cmp; prev = null; first = props; while (props) { cmp = wstrcmp(props->name, name); if (cmp == 0) { /* need to merge values */ break; } if (cmp > 0) { /* insert before this */ prop = (StyleProp *)MemAlloc(sizeof(StyleProp)); prop->name = wstrdup(name); prop->value = wstrdup(value); prop->next = props; if (prev) prev->next = prop; else first = prop; return first; } prev = props; props = props->next; } prop = (StyleProp *)MemAlloc(sizeof(StyleProp)); prop->name = wstrdup(name); prop->value = wstrdup(value); prop->next = null; if (prev) prev->next = prop; else first = prop; return first; } /* Create sorted linked list of properties from style string It temporarily places nulls in place of ':' and ';' to delimit the strings for the property name and value. Some systems don't allow you to null literal strings, so to avoid this, a copy is made first. */ StyleProp *CreateProps(StyleProp *prop, char *style) { char *name, *value, *name_end, *value_end; Bool more; style = wstrdup(style); name = style; while (*name) { while (*name == ' ') ++name; name_end = name; while (*name_end) { if (*name_end == ':') { value = name_end + 1; break; } ++name_end; } if (*name_end != ':') break; while (*value == ' ') ++value; value_end = value; more = no; while (*value_end) { if (*value_end == ';') { more = yes; break; } ++value_end; } *name_end = '\0'; *value_end = '\0'; prop = InsertProperty(prop, name, value); *name_end = ':'; if (more) { *value_end = ';'; name = value_end + 1; continue; } break; } MemFree(style); /* free temporary copy */ return prop; } char *CreatePropString(StyleProp *props) { char *style, *p, *s; int len; StyleProp *prop; /* compute length */ for (len = 0, prop = props; prop; prop = prop->next) { len += wstrlen(prop->name) + 2; len += wstrlen(prop->value) + 2; } style = (char *)MemAlloc(len+1); for (p = style, prop = props; prop; prop = prop->next) { s = prop->name; while(*p++ = *s++); *--p = ':'; *++p = ' '; ++p; s = prop->value; while(*p++ = *s++); if (prop->next == null) break; *--p = ';'; *++p = ' '; ++p; } return style; } /* create string with merged properties */ char *AddProperty(char *style, char *property) { StyleProp *prop; prop = CreateProps(null, style); prop = CreateProps(prop, property); style = CreatePropString(prop); FreeStyleProps(prop); return style; } void FreeStyles(Lexer *lexer) { Style *style, *next; for (style = lexer->styles; style; style = next) { next = style->next; MemFree(style->tag); MemFree(style->tag_class); MemFree(style->properties); MemFree(style); } } char *GensymClass(char *tag) { static int n = 1; char buf[128]; sprintf(buf, "c%d", n++); return wstrdup(buf); } char *FindStyle(Lexer *lexer, char *tag, char *properties) { Style *style; for (style = lexer->styles; style; style=style->next) { if (wstrcmp(style->tag, tag) == 0 && wstrcmp(style->properties, properties) == 0) return style->tag_class; } style = (Style *)MemAlloc(sizeof(Style)); style->tag = wstrdup(tag); style->tag_class = GensymClass(tag); style->properties = wstrdup(properties); style->next = lexer->styles; lexer->styles = style; return style->tag_class; } /* Find style attribute in node, and replace it by corresponding class attribute. Search for class in style dictionary otherwise gensym new class and add to dictionary. Assumes that node doesn't have a class attribute */ void Style2Rule(Lexer *lexer, Node *node) { AttVal *av; char *classname; for (av = node->attributes; av; av = av->next) { if (wstrcmp(av->attribute, "style") == 0) break; } /* if style attribute already exists then append property */ if (av) { classname = FindStyle(lexer, node->element, av->value); MemFree(av->attribute); MemFree(av->value); av->attribute = wstrdup("class"); av->value = wstrdup(classname); } } /* create style element using rules from dictionary */ void CreateStyleElement(Lexer *lexer, Node *doc) { Node *node, *html, *head; Style *style; AttVal *av; if (lexer->styles == null) return; node = NewNode(); node->type = StartTag; node->implicit = yes; node->element = wstrdup("STYLE"); FindTag(node); /* insert type attribute */ av = (AttVal *)MemAlloc(sizeof(AttVal)); av->next = null; av->attribute = wstrdup("type"); av->value = wstrdup("text/css"); av->delim = '"'; av->dict = FindAttribute(av); node->attributes = av; lexer->txtstart = lexer->lexsize; for (style = lexer->styles; style; style = style->next) { AddCharToLexer(lexer, ' '); AddStringLiteral(lexer, style->tag); AddCharToLexer(lexer, '.'); AddStringLiteral(lexer, style->tag_class); AddCharToLexer(lexer, ' '); AddCharToLexer(lexer, '{'); AddStringLiteral(lexer, style->properties); AddCharToLexer(lexer, '}'); AddCharToLexer(lexer, '\n'); } lexer->txtend = lexer->lexsize; InsertNode(node, TextToken(lexer)); /* now insert style element into document head doc is root node. search its children for html node the head node should be first child of html node */ for (html = doc->content; html; html = html->next) { if (html->tag == tag_html) { head = html->content; InsertNode(head, node); break; } } } /* ensure bidirectional links are consistent */ void FixNodeLinks(Node *node) { if (node->prev) node->prev->next = node; else node->parent->content = node; if (node->next) node->next->prev = node; else node->parent->last = node; } /* used to strip child of node when the node has one and only one child */ void StripOnlyChild(Node *node) { Node *child; child = node->content; node->content = child->content; node->last = child->last; child->content = null; FreeNode(child); for (child = node->content; child; child = child->next) child->parent = node; } /* Add style property to element, creating style attribute as needed and adding ; delimiter */ void AddStyleProperty(Node *node, char *property) { AttVal *av; for (av = node->attributes; av; av = av->next) { if (wstrcmp(av->attribute, "style") == 0) break; } /* if style attribute already exists then insert property */ if (av) { char *s; s = AddProperty(av->value, property); MemFree(av->value); av->value = s; } else /* else create new style attribute */ { av = (AttVal *)MemAlloc(sizeof(AttVal)); av->attribute = wstrdup("style"); av->value = wstrdup(property); av->delim = '"'; av->dict = FindAttribute(av); av->next = node->attributes; node->attributes = av; } } /* Create new string that consists of the combined style properties in s1 and s2 To merge property lists, we build a linked list of property/values and insert properties into the list in order, merging values for the same property name. */ char *MergeProperties(char *s1, char *s2) { char *s; StyleProp *prop; prop = CreateProps(null, s1); prop = CreateProps(prop, s2); s = CreatePropString(prop); FreeStyleProps(prop); return s; } void MergeStyles(Node *node, Node *child) { AttVal *av; char *s1, *s2, *style; for (s2 = null, av = child->attributes; av; av = av->next) { if (wstrcmp(av->attribute, "style") == 0) { s2 = av->value; break; } } for (s1 = null, av = node->attributes; av; av = av->next) { if (wstrcmp(av->attribute, "style") == 0) { s1 = av->value; break; } } if (s1) { if (s2) /* merge styles from both */ { style = MergeProperties(s1, s2); MemFree(av->value); av->value = style; } } else if (s2) /* copy style of child */ { av = (AttVal *)MemAlloc(sizeof(AttVal)); av->attribute = wstrdup("style"); av->value = wstrdup(s2); av->delim = '"'; av->dict = FindAttribute(av); av->next = node->attributes; node->attributes = av; } } char *FontSize2Name(char *size) { char *sizes[7] = { "50%", "60%", "80%", null, "120%", "150%", "200%" }; if ('0' <= size[0] && size[0] <= '6') { int n = size[0] - '0'; return sizes[n]; } if (size[0] == '-') return "smaller"; return "larger"; } void AddFontFace(Node *node, char *face) { char buf[1024]; sprintf(buf, "font-family: %s", face); AddStyleProperty(node, buf); } void AddFontSize(Node *node, char *size) { char *value, buf[1024]; if (wstrcmp(size, "6") == 0 && node->tag == tag_p) { MemFree(node->element); node->element = wstrdup("H1"); FindTag(node); return; } if (wstrcmp(size, "5") == 0 && node->tag == tag_p) { MemFree(node->element); node->element = wstrdup("H2"); FindTag(node); return; } if (wstrcmp(size, "4") == 0 && node->tag == tag_p) { MemFree(node->element); node->element = wstrdup("H3"); FindTag(node); return; } value = FontSize2Name(size); if (value) { sprintf(buf, "font-size: %s", value); AddStyleProperty(node, buf); } } void AddFontColor(Node *node, char *color) { char buf[1024]; sprintf(buf, "color: %s", color); AddStyleProperty(node, buf); } void AddAlign(Node *node, char *align) { char buf[1024], *p, *q; /* force alignment value to lower case */ for (p = buf, q = "text-align: "; *p++ = *q++;); for (p = p-1; *p++ = ToLower(*align++);); AddStyleProperty(node, buf); } /* add style properties to node corresponding to the font face, size and color attributes */ void AddFontStyles(Node *node, AttVal *av) { while (av) { if (wstrcmp(av->attribute, "face") == 0) AddFontFace(node, av->value); else if (wstrcmp(av->attribute, "size") == 0) AddFontSize(node, av->value); else if (wstrcmp(av->attribute, "color") == 0) AddFontColor(node, av->value); av = av->next; } } /* Symptom:

    Action:

    */ void TextAlign(Lexer *lexer, Node *node) { AttVal *av, *prev; prev = null; for (av = node->attributes; av; av = av->next) { if (wstrcmp(av->attribute, "align") == 0) { if (prev) prev->next = av->next; else node->attributes = null; MemFree(av->attribute); if (av->value) { AddAlign(node, av->value); MemFree(av->value); } MemFree(av); break; } prev = av; } } /* The clean up rules use the pnode argument to return the next node when the orignal node has been deleted */ /* Symptom:

  • where
  • is only child Action: coerce
  • to
    with indent. */ Bool Dir2Div(Lexer *lexer, Node *node, Node **pnode) { Node *child; if (node->tag == tag_dir || node->tag == tag_ul || node->tag == tag_ol) { child = node->content; if (child == null) return no; /* check child has no peers */ if (child->next) return no; if (child->tag != tag_li) return no; if (!child->implicit) return no; /* coerce dir to div */ node->tag = tag_div; MemFree(node->element); node->element = wstrdup("DIV"); AddStyleProperty(node, "margin-left: 2em"); StripOnlyChild(node); return yes; #if 0 content = child->content; last = child->last; child->content = null; /* adjust parent and set margin on contents of
  • */ for (child = content; child; child = child->next) { child->parent = node->parent; AddStyleProperty(child, "margin-left: 1em"); } /* hook first/last into sequence */ if (content) { content->prev = node->prev; last->next = node->next; FixNodeLinks(content); FixNodeLinks(last); } node->next = null; FreeNode(node); /* ensure that new node is cleaned */ *pnode = CleanNode(lexer, content); return yes; #endif } return no; } /* Symptom: implicit blockquote due to
      some text
    Action: replace by
    with indent style */ Bool Blockquote2Div(Lexer *lexer, Node *node, Node **pnode) { Node *child; if (node->tag == tag_blockquote && node->implicit) { child = node->content; if (child == null) return no; /* coerce blockquote to div */ node->tag = tag_div; MemFree(node->element); node->element = wstrdup("DIV"); AddStyleProperty(node, "margin-left: 2em"); return yes; } return no; } /* Symptom:
    Action: replace
    by
    */ Bool Center2Div(Lexer *lexer, Node *node, Node **pnode) { if (node->tag == tag_center) { node->tag = tag_div; MemFree(node->element); node->element = wstrdup("DIV"); AddStyleProperty(node, "text-align: center"); return yes; } return no; } /* Symptom
    ...
    Action: merge the two divs This is useful after nested s used by Word for indenting have been converted to
    s */ Bool MergeDivs(Lexer *lexer, Node *node, Node **pnode) { Node *child; if (node->tag != tag_div) return no; child = node->content; if (!child) return no; if (child->tag != tag_div) return no; if (child->next != null) return no; MergeStyles(node, child); StripOnlyChild(node); return yes; } /* Symptom:
      • ...
    Action: discard outer list */ Bool NestedList(Lexer *lexer, Node *node, Node **pnode) { Node *child, *list; if (node->tag == tag_ul || node->tag == tag_ol) { child = node->content; if (child == null) return no; /* check child has no peers */ if (child->next) return no; list = child->content; if (!list) return no; if (list->tag != node->tag) return no; *pnode = node->next; /* move inner list node into position of outer node */ list->prev = node->prev; list->next = node->next; list->parent = node->parent; FixNodeLinks(list); /* get rid of outer ul and its li */ child->content = null; node->content = null; node->next = null; FreeNode(node); /* If prev node was a list the chances are this node should be appended to that list. Word has no way of recognizing nested lists and just uses indents */ if (list->prev) { node = list; list = node->prev; if (list->tag == tag_ul || list->tag == tag_ol) { list->next = node->next; if (list->next) list->next->prev = list; child = list->last; /*
  • */ node->parent = child; node->next = null; node->prev = child->last; FixNodeLinks(node); } } CleanNode(lexer, node); return yes; } return no; } /* Symptom: the only child of a block-level element is a presentation element such as B, I or FONT Action: add style "font-weight: bold" to the block and strip the element, leaving its children. example:

    Draft Recommended Practice

    becomes:

    Draft Recommended Practice

    */ Bool BlockStyle(Lexer *lexer, Node *node, Node **pnode) { Node *child; if (node->tag->model & (CM_BLOCK | CM_LIST | CM_DEFLIST | CM_TABLE)) { /* check for align attribute */ if (node->tag != tag_caption) TextAlign(lexer, node); child = node->content; if (child == null) return no; /* check child has no peers */ if (child->next) return no; if (child->tag == tag_b) { AddStyleProperty(node, "font-weight: bold"); StripOnlyChild(node); return yes; } if (child->tag == tag_i) { AddStyleProperty(node, "font-style: italic"); StripOnlyChild(node); return yes; } if (child->tag == tag_font) { AddFontStyles(node, child->attributes); StripOnlyChild(node); return yes; } } return no; } /* the only child of an inline element such as em */ Bool InlineStyle(Lexer *lexer, Node *node, Node **pnode) { Node *child; if (node->tag != tag_font && (node->tag->model & CM_INLINE)) { child = node->content; if (child == null) return no; /* check child has no peers */ if (child->next) return no; if (child->tag == tag_b) { AddStyleProperty(node, "font-weight: bold"); StripOnlyChild(node); return yes; } if (child->tag == tag_i) { AddStyleProperty(node, "font-style: italic"); StripOnlyChild(node); return yes; } if (child->tag == tag_font) { AddFontStyles(node, child->attributes); StripOnlyChild(node); return yes; } } return no; } /* Replace font elements by span elements, deleting the font element's attributes and replacing them by a single style attribute. */ Bool Font2Span(Lexer *lexer, Node *node, Node **pnode) { AttVal *av, *style, *next; if (node->tag == tag_font) { /* if FONT is only child of parent element then leave alone */ if (node->parent->content == node && node->next == null) return no; AddFontStyles(node, node->attributes); av = node->attributes; style = null; while (av) { next = av->next; if (wstrcmp(av->attribute, "style") == 0) { av->next = null; style = av; } else { if (av->attribute) MemFree(av->attribute); if (av->value) MemFree(av->value); MemFree(av); } av = next; } node->attributes = style; node->tag = tag_span; MemFree(node->element); node->element = wstrdup("SPAN"); return yes; } return no; } Bool IsElement(Node *node) { return (node->type == StartTag || node->type == StartEndTag ? yes : no); } /* Applies all matching rules to a node. */ Node *CleanNode(Lexer *lexer, Node *node) { Node *next; for (next = node; IsElement(node); node = next) { if (Dir2Div(lexer, node, &next)) continue; if (Blockquote2Div(lexer, node, &next)) continue; if (NestedList(lexer, node, &next)) continue; if (Center2Div(lexer, node, &next)) continue; if (MergeDivs(lexer, node, &next)) continue; if (BlockStyle(lexer, node, &next)) continue; if (InlineStyle(lexer, node, &next)) continue; if (Font2Span(lexer, node, &next)) continue; break; } return next; } Node *CreateStyleProperties(Lexer *lexer, Node *node) { Node *child; if (node->content) { for (child = node->content; child != null; child = child->next) { child = CreateStyleProperties(lexer, child); } } return CleanNode(lexer, node); } void DefineStyleRules(Lexer *lexer, Node *node) { Node *child; if (node->content) { for (child = node->content; child != null; child = child->next) { DefineStyleRules(lexer, child); } } Style2Rule(lexer, node); } void CleanTree(Lexer *lexer, Node *doc) { doc = CreateStyleProperties(lexer, doc); DefineStyleRules(lexer, doc); CreateStyleElement(lexer, doc); }