## ## CSS PARSING ## ## CSS parsing routines. ## subroutine(css_to_node(string css_pathname, node cssnode), ( string file = open_file(css_pathname, "r"); string line; string group_names; bool outside_brackets = true; string value_before_brackets = ""; string value_inside_brackets = ""; while (!end_of_file(file)) ( line = read_line_from_file(file); while(ends_with(line, '\n')) line = substr(line, 0, length(line) - 1); # Keep going until we've handled this line completely while (line ne "") ( # Remove comments if (contains(line, '/*')) then ( # Get the line, up to the comment, in finalLine int commentStart = index(line, '/*'); string lineWithCommentRemoved = substr(line, 0, commentStart); line = substr(line, commentStart + 2); # Keep going until we're at the end of the comment bool commentDone = false; while (!commentDone) ( # If this contains */, that's the end of the comment. if (contains(line, '*/')) then ( # Add what's after the comment to lineWithCommentRemoved int commentEnd = index(line, '*/'); lineWithCommentRemoved .= substr(line, commentEnd + 2); # Don't show the line commentDone = true; ); # if end of comment # If this does not contain */, then this whole line is part of the comment. else ( # Get another line line = read_line_from_file(file); # Chop of trailing line breaks while(ends_with(line, '\n')) line = substr(line, 0, length(line) - 1); ); # if not end of comment ); # while line ne "" line = lineWithCommentRemoved; ); # if comment # Handle text outside brackets if (outside_brackets) then ( # Check if this contains an opening bracket int leftBracketPos = index(line, '{'); if (leftBracketPos != -1) then ( # Add the part before the { to value_before_brackets value_before_brackets .= substr(line, 0, leftBracketPos); # Continue parsing with the part after the { line = substr(line, leftBracketPos + 1); # We're now inside the brackets outside_brackets = false; ); # if left bracket found # If there's no {, and we're outside brackets, add this whole line to the value before the brackets else ( value_before_brackets .= line; line = ''; ); ); # if outside_brackets # We're inside brackets else ( # Check if this contains a closing bracket int rightBracketPos = index(line, '}'); if (rightBracketPos != -1) then ( # Add the part before the } to value_inside_brackets value_inside_brackets .= substr(line, 0, rightBracketPos); # Continue parsing with the part after the } line = substr(line, rightBracketPos + 1); # We're now outside the brackets outside_brackets = true; ## ## PUT THE ATTRIBUTES IN THE NODE ## string attributes = value_inside_brackets; # Strip out comments if (contains(attributes, '/*') and matches_regular_expression(attributes, '^(.*)/[*][^*]*[*]/(.*)$')) then attributes = $1 . $2; while (attributes ne "") ( if (matches_regular_expression(attributes, '^;?[ ]*([^ ]+) *: *([^;]*)(.*)$')) then ( string attribute = $1; string attribute_value = $2; attributes = $3; # Strip trailing spaces from attributes while (ends_with(attribute_value, ' ')) attribute_value = substr(attribute_value, 0, length(attribute_value) - 1); # Strip leading spaces from attributes while (starts_with(attribute_value, ' ')) attribute_value = substr(attribute_value, 1); # Iterate through each attribute; add it to each group string group_names_remainder = lowercase(value_before_brackets); while (group_names_remainder ne "") ( # Get the next group name if (matches_regular_expression(group_names_remainder, '^([^,]+)(.*)')) then ( string group_name = $1; if (starts_with(group_name, '.')) then group_name = substr(group_name, 1); # Strip leading and trailing spaces and tabs from group_name while (ends_with(group_name, ' ') or ends_with(group_name, ' ')) group_name = substr(group_name, 0, length(group_name) - 1); while (starts_with(group_name, ' ') or starts_with(group_name, ' ')) group_name = substr(group_name, 1); group_name = replace_all(group_name, '.', ':'); group_name = replace_all(group_name, ' ', '.'); @cssnode{group_name}{attribute} = attribute_value; group_names_remainder = $2; if (matches_regular_expression(group_names_remainder, '^, *([^ ].*)$')) then group_names_remainder = $1; ); # If matches else error("Can't parse CSS tags: '" . group_names_remainder . "'"); ); # while group_names_remainder ne "" ); # if extracted attribute else if (matches_regular_expression(attributes, '^;?[ ]*$')) then ( attributes = ''; ); else error("Can't parse CSS attribute: " . attributes); ); # while attributes ne "" value_before_brackets = ""; value_inside_brackets = ""; ); # if right bracket found # If there's no {, and we're inside brackets, add this whole line to the value inside the brackets else ( value_inside_brackets .= line; line = ''; ); ); # if inside brackets ); # while line != "" ); # while not eof close_file(file); cssnode; )); # css_to_node() subroutine(font_size_from_style(node style), ()); subroutine(ems_to_font_size_units(float ems, node style), ( float font_size_units = 0; float enclosing_style_font_size; if (style?{"enclosing_style"}) then ( node enclosing_style = @style{"enclosing_style"}; enclosing_style_font_size = font_size_from_style(enclosing_style); ); else ( enclosing_style_font_size = 14; ); font_size_units = enclosing_style_font_size * ems; font_size_units; )); # ems_to_font_size_units() # This converts a CSS size to units. subroutine(convert_to_units(string css_size, node style), ( # Pixels are units if (matches_regular_expression(css_size, "^([0-9.]+)px")) then ( $1; ); # Points are units else if (matches_regular_expression(css_size, "^([0-9.]+)pt")) then ( $1; ); # Zero is zero in any system else if (css_size eq "0") then ( 0; ); # If size is "auto", choose 12 point else if (css_size eq "auto") then ( 12; ); # If size is in "em" units, compute it as a font size else if (matches_regular_expression(css_size, "^([0-9.]+)em$")) then ( float font_size_units = ems_to_font_size_units($1, style); font_size_units; ); # if ems else error("Unrecognized CSS size: '" . css_size . "'"); )); # convert_to_units # This get the font size from the current style node subroutine(font_size_from_style(node style), ( # Get the font size from the style string font_size = "1em"; if (style?{"font-size"}) then font_size = @style{"font-size"}; # Convert ems to units float font_size_units; if (matches_regular_expression(font_size, "^([0-9.]+)em")) then ( float ems = $1; font_size_units = ems_to_font_size_units(ems, style); # Replace the original size with the computed size, so "em" calculations do not compound on inheritance. @style{"font-size"} = font_size_units . "px"; ); # if ems # Convert anything else to units else ( font_size_units = convert_to_units(font_size, style); ); font_size_units; )); # font_size_from_style() # This get the font from the current style node subroutine(font_from_style(node style), ( string font_family = "Helvetica"; if (style?{"font-family"}) then font_family = @style{"font-family"}; string font_weight = "normal"; if (style?{"font-weight"}) then font_weight = @style{"font-weight"}; # Convert to a PDF font name. node font; if (matches_regular_expression(font_family, "Helvetica")) then ( if (font_weight eq "bold") then font = @"v.stdfonts"{"/Helvetica-Bold"}; else if (font_weight eq "bolder") then font = @"v.stdfonts"{"/Helvetica-Bold"}; else if (font_weight eq "normal") then font = @"v.stdfonts"{"/Helvetica"}; else error("Unrecognized font weight: " . font_weight); ); else error("Unrecognized font family: " . font_family); font; )); # font_from_style() subroutine(set_color_node_from_color_name(node color_node, string color, bool ignoreWhite), ( if (matches_regular_expression(uppercase(color), '^#([0-9A-F][0-9A-F])([0-9A-F][0-9A-F])([0-9A-F][0-9A-F])')) then ( color = uppercase(color); float red = convert_base(substr(color, 1, 2), 16, 10); float green = convert_base(substr(color, 3, 2), 16, 10); float blue = convert_base(substr(color, 5, 2), 16, 10); @color_node{"red"} = red / 255; @color_node{"green"} = green / 255; @color_node{"blue"} = blue / 255; ); else if (color eq "Black") then ( @color_node{"red"} = 0; @color_node{"green"} = 0; @color_node{"blue"} = 0; ); else if (lowercase(color) eq "white") then ( @color_node{"red"} = 1; @color_node{"green"} = 1; @color_node{"blue"} = 1; ); else error("Unknown CSS color: '" . color . "'"); )); # set_color_node_from_color_name() # This gets the SDD text color from the current style node subroutine(sdd_text_color_from_style(node sdd, node style), ( if (style?{"color"}) then ( string color = @style{"color"}; set_color_node_from_color_name(sdd{"text_color"}, color, false); ); )); # sdd_text_color_from_style() # This gets the SDD stroke color from the current style node subroutine(set_stroke_color_from_style(node sdd, node style), ( if (style?{"color"}) then ( string color = @style{"color"}; set_color_node_from_color_name(sdd{"stroke_color"}, color, false); ); )); # sdd_stroke_color_from_style() # This gets the SDD background text color from the current style node subroutine(sdd_text_background_color_from_style(node sdd, node style), ( if (style?{"background-color"}) then ( string color = @style{"background-color"}; set_color_node_from_color_name(sdd{"background_color"}, color, true); ); )); # sdd_text_background_color_from_style() # This sets the SDD box fill color from the current style node's background color subroutine(sdd_box_background_color_from_style(node sdd, node style), ( if (style?{"background-color"}) then ( string color = @style{"background-color"}; set_color_node_from_color_name(sdd{"fill_color"}, color, false); ); )); # sdd_box_background_color_from_style() # This gets the SDD text align from the current style node subroutine(sdd_text_align_from_style(node sdd, node style), ( if (style?{"text-align"}) then ( string align = @style{"text-align"}; if (align eq "left") then ( @sdd{"alignment"} = align; ); else if (align eq "center") then ( @sdd{"alignment"} = "center"; ); else if (align eq "right") then ( @sdd{"alignment"} = "right"; ); else error("Unknown CSS align: '" . align . "'"); ); )); # sdd_text_align_from_style() # This gets the SDD text wrap setting from the current style node subroutine(sdd_text_wrap_from_style(node sdd, node style), ( if (style?{"white-space"}) then ( string white_space = @style{"white-space"}; if (white_space eq "nowrap") then ( @sdd{"wrap"} = false; ); else if (white_space eq "normal") then ( ); else error("Unknown CSS white-space: '" . white_space . "'"); ); )); # sdd_text_wrap_from_style() # This gets the SDD text align from the current style node subroutine(sdd_table_cell_horizontal_align_from_style(node tbl, int row, int column, node style), ( string align = "left"; if (style?{"text-align"}) then ( string align = @style{"text-align"}; ); if (align eq "left") then ( set_table_cell_horizontal_alignment(tbl, row, column, "left"); ); else if (align eq "right") then ( set_table_cell_horizontal_alignment(tbl, row, column, "right"); ); else if (align eq "center") then ( set_table_cell_horizontal_alignment(tbl, row, column, "center"); ); else error("Unknown CSS align: '" . align . "'"); )); # sdd_table_cell_horizontal_align_from_style() # This gets the SDD vertical text align from the current style node subroutine(sdd_table_cell_vertical_align_from_style(node tbl, int row, int column, node style), ( string vertical_align = "top"; if (style?{"vertical-align"}) then ( string vertical_align = @style{"vertical-align"}; ); if (vertical_align eq "top") then ( set_table_cell_vertical_alignment(tbl, row, column, "top"); ); else if (vertical_align eq "bottom") then ( set_table_cell_vertical_alignment(tbl, row, column, "bottom"); ); # For now, just center for "baseline", which is a good enough approximation for known Sawmill uses. else if (vertical_align eq "baseline") then ( # set_table_cell_vertical_alignment(tbl, row, column, "center"); # DEBUG: "center" is crashing; trying top set_table_cell_vertical_alignment(tbl, row, column, "top"); ); else if (vertical_align eq "center") then ( set_table_cell_vertical_alignment(tbl, row, column, "center"); ); else if (vertical_align eq "middle") then ( set_table_cell_vertical_alignment(tbl, row, column, "center"); ); else error("Unknown CSS vertical-align: '" . vertical_align . "'"); )); # sdd_table_cell_vertical_align_from_style() # This gets the SDD background table cell color from the current style node subroutine(sdd_table_cell_background_color_from_style(node tbl, int row, int column, node style), ( if (style?{"background-color"}) then ( node outercell = sdd_get_table_outercell(tbl, row, column); set_color_node_from_color_name(outercell{"fill_color"}, @style{"background-color"}, false); ); )); # sdd_table_cell_background_color_from_style() # This parses padding information from a style node, and sets subnods of padding to pixel padding info. subroutine(padding_from_style(node style, node paddingnode), ( float left_padding = 0; float right_padding = 0; float top_padding = 0; float bottom_padding = 0; string top_units; string bottom_units; string left_units; string right_units; if (style?{"padding"}) then ( string padding = @style{"padding"}; if (matches_regular_expression(padding, "^([0-9px]+) ([0-9px]+) ([0-9px]+) ([0-9px]+)$")) then ( top_units = $1; right_units = $2; bottom_units = $3; left_units = $4; ); else if (matches_regular_expression(padding, "^([0-9px]+) ([0-9px]+)$")) then ( top_units = $1; right_units = $2; bottom_units = $1; left_units = $2; ); else if (padding eq "0") then ( top_units = 0; bottom_units = 0; right_units = 0; left_units = 0; ); else if (matches_regular_expression(padding, "^([0-9]+px)$")) then ( top_units = $1; bottom_units = $1; right_units = $1; left_units = $1; ); else error("Can't parse CSS padding: '" . padding . "'"); ); if (style?{"padding-left"}) then left_units = @style{"padding-left"}; if (style?{"padding-right"}) then right_units = @style{"padding-right"}; if (style?{"padding-top"}) then top_units = @style{"padding-top"}; if (style?{"padding-bottom"}) then bottom_units = @style{"padding-bottom"}; if (left_units ne "") then @paddingnode{"left_padding"} = convert_to_units(left_units, style); if (bottom_units ne "") then @paddingnode{"bottom_padding"} = convert_to_units(bottom_units, style); if (right_units ne "") then @paddingnode{"right_padding"} = convert_to_units(right_units, style); if (top_units ne "") then @paddingnode{"top_padding"} = convert_to_units(top_units, style); )); # padding_from_style() # This parses margin information from a style node, and sets subnods of margin to pixel margin info. subroutine(margin_from_style(node style, node marginnode), ( float left_margin = 0; float right_margin = 0; float top_margin = 0; float bottom_margin = 0; string top_units; string bottom_units; string left_units; string right_units; if (style?{"margin"}) then ( string margin = @style{"margin"}; if (matches_regular_expression(margin, "^([0-9px]+) ([0-9px]+) ([0-9px]+) ([0-9px]+)$")) then ( top_units = $1; right_units = $2; bottom_units = $3; left_units = $4; ); else if (matches_regular_expression(margin, "^([0-9px]+) ([0-9px]+)$")) then ( top_units = $1; right_units = $2; bottom_units = $1; left_units = $2; ); else if (margin eq "0") then ( top_units = 0; bottom_units = 0; right_units = 0; left_units = 0; ); else if (matches_regular_expression(margin, "^([0-9]+px)$")) then ( top_units = $1; bottom_units = $1; right_units = $1; left_units = $1; ); else error("Can't parse CSS margin: '" . margin . "'"); ); if (style?{"margin-left"}) then left_units = @style{"margin-left"}; if (style?{"margin-right"}) then right_units = @style{"margin-right"}; if (style?{"margin-top"}) then top_units = @style{"margin-top"}; if (style?{"margin-bottom"}) then bottom_units = @style{"margin-bottom"}; if (left_units ne "") then @marginnode{"left_margin"} = convert_to_units(left_units, style); if (bottom_units ne "") then @marginnode{"bottom_margin"} = convert_to_units(bottom_units, style); if (right_units ne "") then @marginnode{"right_margin"} = convert_to_units(right_units, style); if (top_units ne "") then @marginnode{"top_margin"} = convert_to_units(top_units, style); )); # margin_from_style() # This sets the padding of a table cell from the current style subroutine(sdd_table_cell_padding_from_style(node tbl, int row, int column, node style), ( node outercell = sdd_get_table_outercell(tbl, row, column); node padding = new_node(); padding_from_style(style, padding); @outercell{"left_padding"} = @padding{"left_padding"}; @outercell{"right_padding"} = @padding{"right_padding"}; @outercell{"top_padding"} = @padding{"top_padding"}; @outercell{"bottom_padding"} = @padding{"bottom_padding"}; # Recompute the innercell location based on the new padding node innercell = sdd_get_table_innercell(tbl, row, column); set_sdd_ymax(innercell, sddsubtract(sddtop(outercell), sddfloat(@outercell{"top_padding"}))); register_sdd_dependency(innercell, "ymax", outercell, "ymax"); delete_node(padding); node margin = new_node(); margin_from_style(style, margin); @outercell{"left_margin"} = @margin{"left_margin"}; @outercell{"right_margin"} = @margin{"right_margin"}; @outercell{"top_margin"} = @margin{"top_margin"}; @outercell{"bottom_margin"} = @margin{"bottom_margin"}; )); # sdd_table_cell_padding_from_style() # This sets the dimensions of a table cell from the current style subroutine(sdd_table_cell_dimensions_from_style(node tbl, node cell_contents, int row, int column, node style), ( node outercell = sdd_get_table_outercell(tbl, row, column); if (style?{"width"}) then ( float width; if (matches_regular_expression(@style{"width"}, '^([0-9]+)%$')) then ( # 2007-08-01 - GMF - If multiple cells have 100% width, they will force the table to be wider than 100%. # If the table style has width=100%, this carries over to all cells, resulting in # a >100% table. This should probably ideally be handled by some sort of shrinking # algorithm which makes them smaller based on weight, but for now we're just going to auto-layout # all width=N% cells. That handles the table with=100% case, and we haven't seen a case that it breaks yet. # set_sdd_width(outercell, sddwidth(tbl)); # set_sdd_width(outercell, sddfloat($1)); # set_sdd_width(cell_contents, sddfloat($1)); # set_sdd_width(innercell, sddfloat($1)); ); # 2008-11-25 - GMF - Added support for style="width:-8px", found in the Single-page summary HTML. Is that actually valid? # Treating it as "width: 8px". else if (matches_regular_expression(@style{"width"}, '^-?([0-9]+)px$')) then ( node paddingnode = new_node(); float width = $1; padding_from_style(style, paddingnode); set_sdd_width(outercell, sddfloat(width + @paddingnode{"left_padding"} + @paddingnode{"right_padding"})); @cell_contents{"fixed_width"} = width; delete_node(paddingnode); ); # If the width is "auto", don't do anything else if (@style{"width"} eq "auto") then ( ); else error("Unable to parse td width: " . @style{"width"}); ); if (style?{"height"}) then ( # 2007-08-01 - GMF - convert_to_units() returns 12 for "auto", which is wrong for td height=auto. Instead, we should leave it unset # to cause autolayout. So, changed this to do nothing for "auto". if (@style{"height"} eq "auto") then ( ); else set_sdd_height(outercell, sddfloat(convert_to_units(@style{"height"}, style))); ); )); # sdd_table_cell_dimensions_from_style() # # set_margin_from_style() # # Purpose: This sets up the table margin SDD # # Parameters: marginbox: the margin SDD surrounding a table # tbl: the table itself # parentsdd: the SDD containing the table # style: the current style # subroutine(set_margin_from_style(node marginbox, node tbl, node parentsdd, node style), ( float left_margin = 0; float right_margin = 0; float top_margin = 0; float bottom_margin = 0; if (style?{"margin-left"}) then left_margin = convert_to_units(@style{"margin-left"}, style); if (style?{"margin-right"}) then right_margin = convert_to_units(@style{"margin-right"}, style); if (style?{"margin-top"}) then top_margin = convert_to_units(@style{"margin-top"}, style); if (style?{"margin-bottom"}) then bottom_margin = convert_to_units(@style{"margin-bottom"}, style); # Set the width and height to match the parent # 2010-09-15 - GMF - We can't have the marginbox get its height from the parent, because if its parent is a div, # then the div gets its height from the table, and the table gets its height from the div. # The table must compute its own height without reference to the parent. Let's use the height of the table. set_sdd_height(marginbox, sddheight(tbl)); set_sdd_width(marginbox, sddwidth(tbl)); set_sdd_xmin(tbl, sddadd(sddleft(marginbox), sddfloat(left_margin))); register_sdd_dependency(tbl, "xmin", marginbox, "xmin"); set_sdd_ymax(tbl, sddsubtract(sddtop(marginbox), sddfloat(top_margin))); register_sdd_dependency(tbl, "ymax", marginbox, "ymax"); )); # set_margin_from_style # This takes the value of line-height and returns it in units subroutine(get_line_height_units(node style), ( string line_height = "1"; if (style?{"line-height"}) then line_height = @style{"line-height"}; # If it's a floating point number, multiply it by the current font line spacing. if (matches_regular_expression(line_height, '^[0-9.]+')) then ( node font = font_from_style(style); float font_size = 12; # default for now to 12 float line_spacing = text_line_spacing(font, font_size); line_spacing * line_height; ); # Otherwise, treat it as units else ( convert_to_units(line_height, style); ); )); # get_line_height_units() subroutine(set_table_width_from_style(node tbl, node marginbox, node parent, node style), ( string width; if (style?{"width"}) then ( width = @style{"width"}; # Handle fixed specified width if (matches_regular_expression(width, "^([0-9]+)$")) then ( @tbl{"width_layout_type"} = "constant"; set_sdd_width(tbl, sddfloat($1)); set_sdd_width(marginbox, sddfloat($1)); ); # Handle fixed specified width else if (matches_regular_expression(width, "^([0-9]+)px$")) then ( @tbl{"width_layout_type"} = "constant"; set_sdd_width(tbl, sddwidth(parent)); ); # Handle percentage width else if (matches_regular_expression(width, "^([0-9]+)%$")) then ( string percent = $1; if (percent eq "100") then ( @tbl{"width_layout_type"} = "100%"; set_sdd_width(tbl, sddwidth(parent)); set_sdd_width(marginbox, sddwidth(parent)); ); else ( error("Unsupported table width percentage: " . percent); ); ); # if percent # If it's "auto", use auto table layout else if (width eq "auto") then ( set_sdd_width(tbl, sddwidth(parent)); set_sdd_width(marginbox, sddwidth(parent)); @tbl{"width_layout_type"} = "auto"; ); else error("Can't parse table width: '" . width . "'"); ); # if width # If there is no width attribute, use auto table layout else @tbl{"width_layout_type"} = "auto"; )); # set_table_width_from_style()