## ## 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() subroutine(percent_to_font_size_units(float percent, 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 * percent) / 100; font_size_units; )); # percent_to_font_size_units() # This converts a CSS size to units. subroutine(convert_to_units(string css_size, node style), ( float font_size_units; # 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 ( font_size_units = ems_to_font_size_units($1, style); font_size_units; ); # if ems # KHP 04/01/2013 # If size is in "%" units, compute it as a font size - ToDO, revise, could be combined with ems_to_font_size_units! else if (matches_regular_expression(css_size, "^([0-9]+)%$")) then ( font_size_units = percent_to_font_size_units($1, style); font_size_units; ); # if percent # KHP 04/01/2013 # If size is in "inherit" ? - ToDO, revise which size to use when size is set to inherit else if (css_size eq "inherit") then ( # KHP 04/Jan/2013 # ToDo, treat this as 100% for now, fix ASAP font_size_units = percent_to_font_size_units(100, style); font_size_units; ); 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%"; # 2013-12-05 - GMF - This was using sddwidth(parent), but then if there is a 100% table within a div, it creates a loop, as the table tries to get its width from the group width of the div, which contains only the table, so the group width uses the table width. I'm not sure the best solution here, but for now I'm just using the whole content box width when 100% table width is specified. set_sdd_width(tbl, sddwidth(v.content_box)); 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 ( # 2013-12-05 - GMF - This was using sddwidth(parent), but then if there is a 100% table within a div, it creates a loop, as the table tries to get its width from the group width of the div, which contains only the table, so the group width uses the table width. I'm not sure the best solution here, but for now I'm just using the whole content box width when 100% table width is specified. set_sdd_width(tbl, sddwidth(v.content_box)); # 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()