From 4b9f81a65aa1db8f1357890c9cf9be283a48774b Mon Sep 17 00:00:00 2001 From: Tim Stack Date: Thu, 27 Jul 2023 21:50:12 -0700 Subject: [PATCH] [markdown] some minor improvements --- NEWS.md | 5 + src/CMakeLists.txt | 8 +- src/Makefile.am | 4 + src/base/ansi_scrubber.cc | 4 +- src/base/string_util.hh | 10 + src/css-color-names.json | 150 +++++++++++++ src/field_overlay_source.cc | 3 + src/md2attr_line.cc | 210 +++++++++++++++--- src/md2attr_line.hh | 2 +- src/plain_text_source.cc | 6 +- src/sql_util.cc | 6 +- src/styling.cc | 35 ++- src/styling.hh | 2 +- test/expected/expected.am | 4 + ...a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out | 3 +- ...bee322ea3374690e44a88a16cb6b84feaa11d3.out | 1 - ...46094b6e005285dc0921ef9979e36240c5042d.err | 0 ...46094b6e005285dc0921ef9979e36240c5042d.out | 7 + ...acc1a8bb5028636fdbf08f077f9a835ab51bec.out | 1 - ...51b55dff7332c5bee2c9b797c401c5614d574a.out | 1 - ...24078983cf1b7a80b6fb65d5186cd125498136.out | 1 - ...486314c4e02e480d829ea2f077b86c49fedcec.out | 2 +- ...9b67113864ef5e77267d7fd8ad4072f5aef0fc.err | 0 ...9b67113864ef5e77267d7fd8ad4072f5aef0fc.out | 13 ++ ...88ea61a5382458cc48a2607e2639e52b0be1da.out | 1 - test/test_sql_anno.sh | 2 + test/test_text_file.sh | 3 + test/textfile_0.md | 9 + 28 files changed, 440 insertions(+), 53 deletions(-) create mode 100644 src/css-color-names.json create mode 100644 test/expected/test_sql_anno.sh_de46094b6e005285dc0921ef9979e36240c5042d.err create mode 100644 test/expected/test_sql_anno.sh_de46094b6e005285dc0921ef9979e36240c5042d.out create mode 100644 test/expected/test_text_file.sh_d59b67113864ef5e77267d7fd8ad4072f5aef0fc.err create mode 100644 test/expected/test_text_file.sh_d59b67113864ef5e77267d7fd8ad4072f5aef0fc.out diff --git a/NEWS.md b/NEWS.md index 3166af38..371c59f1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -44,6 +44,11 @@ Features: variables are now defined inside **lnav** and refer to the location of the user's configuration directory and the directory where cached data is stored, respectively. +* The `
` tag is now recognized in Markdown files.
+* The `style` attribute in `` tags is now supported.
+  Basic properties like `color`, `background-color`,
+  `font-weight`, and `text-decoration` can be used. CSS
+  color names should work as well.
 
 Bug Fixes:
 * Binary data piped into stdin should now be treated the same
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c14a4f58..c8a7aa26 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -103,7 +103,7 @@ function(bin2c)
             DEPENDS bin2c "${FILE_TO_LINK}")
 endfunction(bin2c)
 
-foreach (FILE_TO_LINK animals.json ansi-palette.json diseases.json emojis.json xml-entities.json xterm-palette.json help.txt help.md init.sql words.json)
+foreach (FILE_TO_LINK animals.json ansi-palette.json css-color-names.json diseases.json emojis.json xml-entities.json xterm-palette.json help.txt help.md init.sql words.json)
     string(REPLACE "." "-" DST_FILE "${FILE_TO_LINK}")
     add_custom_command(
             OUTPUT "${DST_FILE}.h" "${DST_FILE}.cc"
@@ -196,7 +196,7 @@ set(BUILTIN_LNAV_SCRIPTS
         scripts/partition-by-boot.lnav scripts/rename-stdin.lnav
         scripts/search-for.lnav
         scripts/workdir-url-handler.lnav
-        )
+)
 
 set(BUILTIN_LNAV_SCRIPT_PATHS ${BUILTIN_LNAV_SCRIPTS})
 
@@ -328,7 +328,7 @@ add_library(lnavfileio STATIC
         line_buffer.cc
         pollable.cc
         shared_buffer.cc
-        )
+)
 target_include_directories(lnavfileio PRIVATE . ${CMAKE_CURRENT_BINARY_DIR})
 target_link_libraries(lnavfileio cppfmt spookyhash pcrepp base BZip2::BZip2 ZLIB::ZLIB)
 
@@ -612,7 +612,7 @@ target_include_directories(diag PUBLIC . fmtlib ${CMAKE_CURRENT_BINARY_DIR}
         third-party
         third-party/base64/include
         third-party/rapidyaml
-        )
+)
 
 target_link_libraries(
         diag
diff --git a/src/Makefile.am b/src/Makefile.am
index 632fff8d..437805a9 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -95,6 +95,7 @@ LNAV_BUILT_FILES = \
     ansi-palette-json.cc \
     builtin-scripts.cc \
     builtin-sh-scripts.cc \
+    css-color-names-json.cc \
     default-config.cc \
     default-formats.cc \
     diseases-json.cc \
@@ -158,11 +159,13 @@ LDADD = \
 
 # emojis.json is from https://gist.github.com/oliveratgithub/0bf11a9aff0d6da7b46f1490f86a71eb/
 # xml-entities.json is from https://html.spec.whatwg.org/entities.json
+# css-color-names.json is from https://github.com/bahamas10/css-color-names/blob/master/css-color-names.json
 
 dist_noinst_DATA = \
 	alpha-release.sh \
 	animals.json \
 	ansi-palette.json \
+	css-color-names.json \
 	diseases.json \
 	emojis.json \
 	$(BUILTIN_LNAVSCRIPTS) \
@@ -524,6 +527,7 @@ DISTCLEANFILES = \
     ansi-palette-json.h \
     builtin-scripts.h \
     builtin-sh-scripts.h \
+    css-color-names-json.h \
     default-config.h \
     default-formats.h \
     diseases-json.h \
diff --git a/src/base/ansi_scrubber.cc b/src/base/ansi_scrubber.cc
index 3dc090a8..5c494ccd 100644
--- a/src/base/ansi_scrubber.cc
+++ b/src/base/ansi_scrubber.cc
@@ -242,8 +242,8 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa)
         auto seq = md[1].value();
         auto terminator = md[2].value();
         struct line_range lr;
-        bool has_attrs = false;
         text_attrs attrs;
+        bool has_attrs = false;
         nonstd::optional role;
         size_t lpc;
 
@@ -349,7 +349,7 @@ scrub_ansi_string(std::string& str, string_attrs_t* sa)
                 }
                 lr.lr_start = sf.sf_begin;
                 lr.lr_end = -1;
-                if (attrs.ta_attrs || attrs.ta_fg_color || attrs.ta_bg_color) {
+                if (!attrs.empty()) {
                     sa->emplace_back(lr, VC_STYLE.value(attrs));
                 }
                 role | [&lr, &sa](role_t r) {
diff --git a/src/base/string_util.hh b/src/base/string_util.hh
index 73a8b87c..d516de03 100644
--- a/src/base/string_util.hh
+++ b/src/base/string_util.hh
@@ -115,6 +115,16 @@ trim(const std::string& str)
     return str.substr(start, end - start);
 }
 
+inline const char*
+ltrim(const char* str)
+{
+    while (isspace(*str)) {
+        str += 1;
+    }
+
+    return str;
+}
+
 inline std::string
 rtrim(const std::string& str)
 {
diff --git a/src/css-color-names.json b/src/css-color-names.json
new file mode 100644
index 00000000..7a1480dc
--- /dev/null
+++ b/src/css-color-names.json
@@ -0,0 +1,150 @@
+{
+  "aliceblue": "#f0f8ff",
+  "antiquewhite": "#faebd7",
+  "aqua": "#00ffff",
+  "aquamarine": "#7fffd4",
+  "azure": "#f0ffff",
+  "beige": "#f5f5dc",
+  "bisque": "#ffe4c4",
+  "black": "#000000",
+  "blanchedalmond": "#ffebcd",
+  "blue": "#0000ff",
+  "blueviolet": "#8a2be2",
+  "brown": "#a52a2a",
+  "burlywood": "#deb887",
+  "cadetblue": "#5f9ea0",
+  "chartreuse": "#7fff00",
+  "chocolate": "#d2691e",
+  "coral": "#ff7f50",
+  "cornflowerblue": "#6495ed",
+  "cornsilk": "#fff8dc",
+  "crimson": "#dc143c",
+  "cyan": "#00ffff",
+  "darkblue": "#00008b",
+  "darkcyan": "#008b8b",
+  "darkgoldenrod": "#b8860b",
+  "darkgray": "#a9a9a9",
+  "darkgreen": "#006400",
+  "darkgrey": "#a9a9a9",
+  "darkkhaki": "#bdb76b",
+  "darkmagenta": "#8b008b",
+  "darkolivegreen": "#556b2f",
+  "darkorange": "#ff8c00",
+  "darkorchid": "#9932cc",
+  "darkred": "#8b0000",
+  "darksalmon": "#e9967a",
+  "darkseagreen": "#8fbc8f",
+  "darkslateblue": "#483d8b",
+  "darkslategray": "#2f4f4f",
+  "darkslategrey": "#2f4f4f",
+  "darkturquoise": "#00ced1",
+  "darkviolet": "#9400d3",
+  "deeppink": "#ff1493",
+  "deepskyblue": "#00bfff",
+  "dimgray": "#696969",
+  "dimgrey": "#696969",
+  "dodgerblue": "#1e90ff",
+  "firebrick": "#b22222",
+  "floralwhite": "#fffaf0",
+  "forestgreen": "#228b22",
+  "fuchsia": "#ff00ff",
+  "gainsboro": "#dcdcdc",
+  "ghostwhite": "#f8f8ff",
+  "goldenrod": "#daa520",
+  "gold": "#ffd700",
+  "gray": "#808080",
+  "green": "#008000",
+  "greenyellow": "#adff2f",
+  "grey": "#808080",
+  "honeydew": "#f0fff0",
+  "hotpink": "#ff69b4",
+  "indianred": "#cd5c5c",
+  "indigo": "#4b0082",
+  "ivory": "#fffff0",
+  "khaki": "#f0e68c",
+  "lavenderblush": "#fff0f5",
+  "lavender": "#e6e6fa",
+  "lawngreen": "#7cfc00",
+  "lemonchiffon": "#fffacd",
+  "lightblue": "#add8e6",
+  "lightcoral": "#f08080",
+  "lightcyan": "#e0ffff",
+  "lightgoldenrodyellow": "#fafad2",
+  "lightgray": "#d3d3d3",
+  "lightgreen": "#90ee90",
+  "lightgrey": "#d3d3d3",
+  "lightpink": "#ffb6c1",
+  "lightsalmon": "#ffa07a",
+  "lightseagreen": "#20b2aa",
+  "lightskyblue": "#87cefa",
+  "lightslategray": "#778899",
+  "lightslategrey": "#778899",
+  "lightsteelblue": "#b0c4de",
+  "lightyellow": "#ffffe0",
+  "lime": "#00ff00",
+  "limegreen": "#32cd32",
+  "linen": "#faf0e6",
+  "magenta": "#ff00ff",
+  "maroon": "#800000",
+  "mediumaquamarine": "#66cdaa",
+  "mediumblue": "#0000cd",
+  "mediumorchid": "#ba55d3",
+  "mediumpurple": "#9370db",
+  "mediumseagreen": "#3cb371",
+  "mediumslateblue": "#7b68ee",
+  "mediumspringgreen": "#00fa9a",
+  "mediumturquoise": "#48d1cc",
+  "mediumvioletred": "#c71585",
+  "midnightblue": "#191970",
+  "mintcream": "#f5fffa",
+  "mistyrose": "#ffe4e1",
+  "moccasin": "#ffe4b5",
+  "navajowhite": "#ffdead",
+  "navy": "#000080",
+  "oldlace": "#fdf5e6",
+  "olive": "#808000",
+  "olivedrab": "#6b8e23",
+  "orange": "#ffa500",
+  "orangered": "#ff4500",
+  "orchid": "#da70d6",
+  "palegoldenrod": "#eee8aa",
+  "palegreen": "#98fb98",
+  "paleturquoise": "#afeeee",
+  "palevioletred": "#db7093",
+  "papayawhip": "#ffefd5",
+  "peachpuff": "#ffdab9",
+  "peru": "#cd853f",
+  "pink": "#ffc0cb",
+  "plum": "#dda0dd",
+  "powderblue": "#b0e0e6",
+  "purple": "#800080",
+  "rebeccapurple": "#663399",
+  "red": "#ff0000",
+  "rosybrown": "#bc8f8f",
+  "royalblue": "#4169e1",
+  "saddlebrown": "#8b4513",
+  "salmon": "#fa8072",
+  "sandybrown": "#f4a460",
+  "seagreen": "#2e8b57",
+  "seashell": "#fff5ee",
+  "sienna": "#a0522d",
+  "silver": "#c0c0c0",
+  "skyblue": "#87ceeb",
+  "slateblue": "#6a5acd",
+  "slategray": "#708090",
+  "slategrey": "#708090",
+  "snow": "#fffafa",
+  "springgreen": "#00ff7f",
+  "steelblue": "#4682b4",
+  "tan": "#d2b48c",
+  "teal": "#008080",
+  "thistle": "#d8bfd8",
+  "tomato": "#ff6347",
+  "turquoise": "#40e0d0",
+  "violet": "#ee82ee",
+  "wheat": "#f5deb3",
+  "white": "#ffffff",
+  "whitesmoke": "#f5f5f5",
+  "yellow": "#ffff00",
+  "yellowgreen": "#9acd32"
+}
diff --git a/src/field_overlay_source.cc b/src/field_overlay_source.cc
index ad4c312a..090d41e3 100644
--- a/src/field_overlay_source.cc
+++ b/src/field_overlay_source.cc
@@ -474,6 +474,9 @@ field_overlay_source::build_meta_line(const listview_curses& lv,
         }
 
         auto comment_lines = al.rtrim().split_lines();
+        if (comment_lines.back().empty()) {
+            comment_lines.pop_back();
+        }
         for (size_t lpc = 0; lpc < comment_lines.size(); lpc++) {
             auto& comment_line = comment_lines[lpc];
 
diff --git a/src/md2attr_line.cc b/src/md2attr_line.cc
index a208616c..e0b9a451 100644
--- a/src/md2attr_line.cc
+++ b/src/md2attr_line.cc
@@ -488,11 +488,129 @@ md2attr_line::leave_span(const md4cpp::event_handler::span& sp)
     return Ok();
 }
 
+static attr_line_t
+to_attr_line(const pugi::xml_node& doc)
+{
+    static const auto NAME_SPAN = string_fragment::from_const("span");
+    static const auto NAME_PRE = string_fragment::from_const("pre");
+    static const auto NAME_FG = string_fragment::from_const("color");
+    static const auto NAME_BG = string_fragment::from_const("background-color");
+    static const auto NAME_FONT_WEIGHT
+        = string_fragment::from_const("font-weight");
+    static const auto NAME_TEXT_DECO
+        = string_fragment::from_const("text-decoration");
+    static const auto& vc = view_colors::singleton();
+
+    attr_line_t retval;
+    if (doc.children().empty()) {
+        retval.append(doc.text().get());
+    }
+    for (const auto& child : doc.children()) {
+        if (child.name() == NAME_SPAN) {
+            auto styled_span = attr_line_t(child.text().get());
+
+            auto span_class = child.attribute("class");
+            if (span_class) {
+                auto cl_iter = vc.vc_class_to_role.find(span_class.value());
+
+                if (cl_iter == vc.vc_class_to_role.end()) {
+                    log_error("unknown span class: %s", span_class.value());
+                } else {
+                    styled_span.with_attr_for_all(cl_iter->second);
+                }
+            }
+            text_attrs ta;
+            auto span_style = child.attribute("style");
+            if (span_style) {
+                auto style_sf = string_fragment::from_c_str(span_style.value());
+
+                while (!style_sf.empty()) {
+                    auto split_res
+                        = style_sf.split_when(string_fragment::tag1{';'});
+                    auto colon_split_res = split_res.first.split_pair(
+                        string_fragment::tag1{':'});
+                    if (colon_split_res) {
+                        auto key = colon_split_res->first.trim();
+                        auto value = colon_split_res->second.trim();
+
+                        if (key == NAME_FG) {
+                            auto color_res
+                                = styling::color_unit::from_str(value);
+
+                            if (color_res.isErr()) {
+                                log_error("invalid color: %.*s -- %s",
+                                          value.length(),
+                                          value.data(),
+                                          color_res.unwrapErr().c_str());
+                            } else {
+                                ta.ta_fg_color
+                                    = vc.match_color(color_res.unwrap());
+                            }
+                        } else if (key == NAME_BG) {
+                            auto color_res
+                                = styling::color_unit::from_str(value);
+
+                            if (color_res.isErr()) {
+                                log_error(
+                                    "invalid background-color: %.*s -- %s",
+                                    value.length(),
+                                    value.data(),
+                                    color_res.unwrapErr().c_str());
+                            } else {
+                                ta.ta_bg_color
+                                    = vc.match_color(color_res.unwrap());
+                            }
+                        } else if (key == NAME_FONT_WEIGHT) {
+                            if (value == "bold" || value == "bolder") {
+                                ta.ta_attrs |= A_BOLD;
+                            }
+                        } else if (key == NAME_TEXT_DECO) {
+                            auto deco_sf = value;
+
+                            while (!deco_sf.empty()) {
+                                auto deco_split_res = deco_sf.split_when(
+                                    string_fragment::tag1{' '});
+
+                                if (deco_split_res.first.trim() == "underline")
+                                {
+                                    ta.ta_attrs |= A_UNDERLINE;
+                                }
+
+                                deco_sf = deco_split_res.second;
+                            }
+                        }
+                    }
+                    style_sf = split_res.second;
+                }
+                if (!ta.empty()) {
+                    styled_span.with_attr_for_all(VC_STYLE.value(ta));
+                }
+            }
+            retval.append(styled_span);
+        } else if (child.name() == NAME_PRE) {
+            auto pre_al = attr_line_t();
+
+            for (const auto& sub : child.children()) {
+                auto child_al = to_attr_line(sub);
+                if (pre_al.empty() && startswith(child_al.get_string(), "\n")) {
+                    child_al.erase(0, 1);
+                }
+                pre_al.append(child_al);
+            }
+            pre_al.with_attr_for_all(SA_PREFORMATTED.value());
+            retval.append(pre_al);
+        } else {
+            retval.append(child.text().get());
+        }
+    }
+
+    return retval;
+}
+
 Result
 md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf)
 {
     static const auto& entity_map = md4cpp::get_xml_entity_map();
-    static const auto& vc = view_colors::singleton();
 
     auto& last_block = this->ml_blocks.back();
 
@@ -517,40 +635,64 @@ md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf)
         }
         case MD_TEXT_HTML: {
             last_block.append(sf);
-            if (sf.startswith("ml_html_span_starts.push_back(last_block.length()
-                                                    - sf.length());
-            } else if (sf == "" && !this->ml_html_span_starts.empty()) {
-                std::string html_span = last_block.get_string().substr(
-                    this->ml_html_span_starts.back());
-
-                pugi::xml_document doc;
-
-                auto load_res = doc.load_string(html_span.c_str());
-                if (load_res) {
-                    auto span = doc.child("span");
-                    if (span) {
-                        auto styled_span = attr_line_t(span.text().get());
-
-                        auto span_class = span.attribute("class");
-                        if (span_class) {
-                            auto cl_iter
-                                = vc.vc_class_to_role.find(span_class.value());
-
-                            if (cl_iter == vc.vc_class_to_role.end()) {
-                                log_error("unknown span class: %s",
-                                          span_class.value());
-                            } else {
-                                styled_span.with_attr_for_all(cl_iter->second);
-                            }
+
+            struct open_tag {
+                std::string ot_name;
+            };
+            struct close_tag {
+                std::string ct_name;
+            };
+
+            mapbox::util::variant tag;
+
+            if (sf.startswith("'})
+                        .first.to_string(),
+                };
+            } else if (sf.startswith("<")) {
+                tag = open_tag{
+                    sf.substr(1)
+                        .split_when(
+                            [](char ch) { return ch == ' ' || ch == '>'; })
+                        .first.to_string(),
+                };
+            }
+
+            if (tag.valid()) {
+                tag.match(
+                    [this, &sf, &last_block](const open_tag& ot) {
+                        if (!this->ml_html_starts.empty()) {
+                            return;
                         }
-                        last_block.erase(this->ml_html_span_starts.back());
-                        last_block.append(styled_span);
-                    }
-                } else {
-                    log_error("failed to parse: %s", load_res.description());
-                }
-                this->ml_html_span_starts.pop_back();
+                        this->ml_html_starts.emplace_back(
+                            ot.ot_name, last_block.length() - sf.length());
+                    },
+                    [this, &last_block](const close_tag& ct) {
+                        if (this->ml_html_starts.empty()) {
+                            return;
+                        }
+                        if (this->ml_html_starts.back().first != ct.ct_name) {
+                            return;
+                        }
+
+                        const auto html_span = last_block.get_string().substr(
+                            this->ml_html_starts.back().second);
+
+                        pugi::xml_document doc;
+
+                        auto load_res = doc.load_string(html_span.c_str());
+                        if (!load_res) {
+                            log_error("failed to parse: %s",
+                                      load_res.description());
+                        } else {
+                            last_block.erase(
+                                this->ml_html_starts.back().second);
+                            last_block.append(to_attr_line(doc));
+                        }
+                        this->ml_html_starts.pop_back();
+                    });
             }
             break;
         }
diff --git a/src/md2attr_line.hh b/src/md2attr_line.hh
index 9f1f977f..6a2ef8e6 100644
--- a/src/md2attr_line.hh
+++ b/src/md2attr_line.hh
@@ -83,7 +83,7 @@ private:
     std::vector ml_list_stack;
     std::vector ml_tables;
     std::vector ml_span_starts;
-    std::vector ml_html_span_starts;
+    std::vector> ml_html_starts;
     std::vector ml_footnotes;
     int32_t ml_code_depth{0};
 };
diff --git a/src/plain_text_source.cc b/src/plain_text_source.cc
index 632a5413..70b3d0e3 100644
--- a/src/plain_text_source.cc
+++ b/src/plain_text_source.cc
@@ -81,7 +81,11 @@ plain_text_source::replace_with(const attr_line_t& text_lines)
     this->tds_doc_sections = lnav::document::discover_metadata(text_lines);
 
     file_off_t off = 0;
-    for (auto& line : text_lines.split_lines()) {
+    auto lines = text_lines.split_lines();
+    while (!lines.empty() && lines.back().empty()) {
+        lines.pop_back();
+    }
+    for (auto& line : lines) {
         auto line_len = line.length() + 1;
         this->tds_lines.emplace_back(off, std::move(line));
         off += line_len;
diff --git a/src/sql_util.cc b/src/sql_util.cc
index 8f80ffc6..70f38bb5 100644
--- a/src/sql_util.cc
+++ b/src/sql_util.cc
@@ -1023,9 +1023,13 @@ annotate_sql_statement(attr_line_t& al)
             lnav::pcre2pp::code::from_const(R"(\A'[^']*('(?:'[^']*')*|$))"),
             &SQL_STRING_ATTR,
         },
+        {
+            lnav::pcre2pp::code::from_const(R"(\A0x[0-9a-fA-F]+)"),
+            &SQL_NUMBER_ATTR,
+        },
         {
             lnav::pcre2pp::code::from_const(
-                R"(\A-?\d+(?:\.\d*(?:[eE][\-\+]?\d+)?)?|0x[0-9a-fA-F]+$)"),
+                R"(\A-?\d+(?:\.\d+)?(?:[eE][\-\+]?\d+)?)"),
             &SQL_NUMBER_ATTR,
         },
         {
diff --git a/src/styling.cc b/src/styling.cc
index 0e94cfc7..ecd09a10 100644
--- a/src/styling.cc
+++ b/src/styling.cc
@@ -33,6 +33,7 @@
 
 #include "ansi-palette-json.h"
 #include "config.h"
+#include "css-color-names-json.h"
 #include "fmt/format.h"
 #include "xterm-palette-json.h"
 #include "yajlpp/yajlpp.hh"
@@ -67,6 +68,29 @@ static const typed_json_path_container>
             .with_children(term_color_handler),
 };
 
+struct css_color_names {
+    std::map ccn_name_to_color;
+};
+
+static const typed_json_path_container css_color_names_handlers
+    = {
+        yajlpp::pattern_property_handler("(?.*)")
+            .for_field(&css_color_names::ccn_name_to_color),
+};
+
+static const css_color_names&
+get_css_color_names()
+{
+    static const intern_string_t iname
+        = intern_string::lookup(css_color_names_json.get_name());
+    static const auto INSTANCE
+        = css_color_names_handlers.parser_for(iname)
+              .of(css_color_names_json.to_string_fragment())
+              .unwrap();
+
+    return INSTANCE;
+}
+
 term_color_palette*
 xterm_colors()
 {
@@ -86,12 +110,21 @@ ansi_colors()
 }
 
 Result
-rgb_color::from_str(const string_fragment& sf)
+rgb_color::from_str(string_fragment sf)
 {
     if (sf.empty()) {
         return Ok(rgb_color());
     }
 
+    if (sf[0] != '#') {
+        const auto& css_colors = get_css_color_names();
+        const auto& iter = css_colors.ccn_name_to_color.find(sf.to_string());
+
+        if (iter != css_colors.ccn_name_to_color.end()) {
+            sf = string_fragment::from_str(iter->second);
+        }
+    }
+
     rgb_color rgb_out;
 
     if (sf[0] == '#') {
diff --git a/src/styling.hh b/src/styling.hh
index ae95cee9..6ce0a81e 100644
--- a/src/styling.hh
+++ b/src/styling.hh
@@ -43,7 +43,7 @@
 #include "yajlpp/yajlpp_def.hh"
 
 struct rgb_color {
-    static Result from_str(const string_fragment& sf);
+    static Result from_str(string_fragment sf);
 
     explicit rgb_color(short r = -1, short g = -1, short b = -1)
         : rc_r(r), rc_g(g), rc_b(b)
diff --git a/test/expected/expected.am b/test/expected/expected.am
index e9e987b4..091e218c 100644
--- a/test/expected/expected.am
+++ b/test/expected/expected.am
@@ -596,6 +596,8 @@ EXPECTED_FILES = \
     $(srcdir)/%reldir%/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.out \
     $(srcdir)/%reldir%/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.err \
     $(srcdir)/%reldir%/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.out \
+    $(srcdir)/%reldir%/test_sql_anno.sh_de46094b6e005285dc0921ef9979e36240c5042d.err \
+    $(srcdir)/%reldir%/test_sql_anno.sh_de46094b6e005285dc0921ef9979e36240c5042d.out \
     $(srcdir)/%reldir%/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.err \
     $(srcdir)/%reldir%/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.out \
     $(srcdir)/%reldir%/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.err \
@@ -1064,6 +1066,8 @@ EXPECTED_FILES = \
     $(srcdir)/%reldir%/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.out \
     $(srcdir)/%reldir%/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.err \
     $(srcdir)/%reldir%/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.out \
+    $(srcdir)/%reldir%/test_text_file.sh_d59b67113864ef5e77267d7fd8ad4072f5aef0fc.err \
+    $(srcdir)/%reldir%/test_text_file.sh_d59b67113864ef5e77267d7fd8ad4072f5aef0fc.out \
     $(srcdir)/%reldir%/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.err \
     $(srcdir)/%reldir%/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out \
     $()
diff --git a/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out
index 9b29971b..96f6ffa3 100644
--- a/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out
+++ b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out
@@ -2086,7 +2086,7 @@ For support questions, email:
   unparse_url(), upper(), xpath()
 Example
 #1 To get a string with the code points 0x48 and 0x49:
-   ;SELECT char(0x48, 0x49)                           
+   ;SELECT char(0x48, 0x49)                           
    
 
 
@@ -4807,4 +4807,3 @@ For support questions, email:
   cte-table-name   The name for the temporary table.
   select-stmt      The SELECT statement used to
                    populate the temporary table.
-
diff --git a/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.out b/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.out
index 0ac4c9a8..f9264f7f 100644
--- a/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.out
+++ b/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.out
@@ -1,3 +1,2 @@
 Hello
 World
-
diff --git a/test/expected/test_sql_anno.sh_de46094b6e005285dc0921ef9979e36240c5042d.err b/test/expected/test_sql_anno.sh_de46094b6e005285dc0921ef9979e36240c5042d.err
new file mode 100644
index 00000000..e69de29b
diff --git a/test/expected/test_sql_anno.sh_de46094b6e005285dc0921ef9979e36240c5042d.out b/test/expected/test_sql_anno.sh_de46094b6e005285dc0921ef9979e36240c5042d.out
new file mode 100644
index 00000000..0dd89028
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_de46094b6e005285dc0921ef9979e36240c5042d.out
@@ -0,0 +1,7 @@
+                 SELECT 0x77, 123, 123e4
+     sql_keyword ------
+      sql_number        ----
+       sql_comma            -
+      sql_number              ---
+       sql_comma                 -
+      sql_number                   -----
diff --git a/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.out b/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.out
index 710f6687..4543a4cf 100644
--- a/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.out
+++ b/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.out
@@ -16,4 +16,3 @@ command-line. If you're familiar with the SumoLogic query language,
 you might find this tool more comfortable to work with.
 
  ▌[1] - https://github.com/rcoh/angle-grinder 
-
diff --git a/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.out b/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.out
index a39be1ec..0a25e844 100644
--- a/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.out
+++ b/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.out
@@ -175,4 +175,3 @@ command-line. If you're familiar with the SumoLogic query language,
 you might find this tool more comfortable to work with.
 
  ▌[1] - https://github.com/rcoh/angle-grinder 
-
diff --git a/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.out b/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.out
index accb1c3e..8b8da47e 100644
--- a/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.out
+++ b/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.out
@@ -146,4 +146,3 @@ command-line. If you're familiar with the SumoLogic query language,
 you might find this tool more comfortable to work with.
 
  ▌[1] - https://github.com/rcoh/angle-grinder 
-
diff --git a/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.out b/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.out
index 5a1b89ae..243bfc79 100644
--- a/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.out
+++ b/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.out
@@ -1,4 +1,4 @@
+command-line. If you're familiar with the SumoLogic query language,
 you might find this tool more comfortable to work with.
 
  ▌[1] - https://github.com/rcoh/angle-grinder 
-
diff --git a/test/expected/test_text_file.sh_d59b67113864ef5e77267d7fd8ad4072f5aef0fc.err b/test/expected/test_text_file.sh_d59b67113864ef5e77267d7fd8ad4072f5aef0fc.err
new file mode 100644
index 00000000..e69de29b
diff --git a/test/expected/test_text_file.sh_d59b67113864ef5e77267d7fd8ad4072f5aef0fc.out b/test/expected/test_text_file.sh_d59b67113864ef5e77267d7fd8ad4072f5aef0fc.out
new file mode 100644
index 00000000..d1915413
--- /dev/null
+++ b/test/expected/test_text_file.sh_d59b67113864ef5e77267d7fd8ad4072f5aef0fc.out
@@ -0,0 +1,13 @@
+
+Test
+
+ • One
+ • Two
+ • Three
+
+Bold red
+
+Underline
+
+  Hello,
+  World!
diff --git a/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out b/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out
index accb1c3e..8b8da47e 100644
--- a/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out
+++ b/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out
@@ -146,4 +146,3 @@ command-line. If you're familiar with the SumoLogic query language,
 you might find this tool more comfortable to work with.
 
  ▌[1] - https://github.com/rcoh/angle-grinder 
-
diff --git a/test/test_sql_anno.sh b/test/test_sql_anno.sh
index 8c26fe8a..27896c16 100644
--- a/test/test_sql_anno.sh
+++ b/test/test_sql_anno.sh
@@ -50,3 +50,5 @@ run_cap_test ./drive_sql_anno \
 run_cap_test ./drive_sql_anno "SELECT * FROM foo.bar"
 
 run_cap_test ./drive_sql_anno "SELECT json_object('abc', 'def') ->> '$.abc'"
+
+run_cap_test ./drive_sql_anno "SELECT 0x77, 123, 123e4"
diff --git a/test/test_text_file.sh b/test/test_text_file.sh
index 166f7907..22c69df7 100644
--- a/test/test_text_file.sh
+++ b/test/test_text_file.sh
@@ -34,3 +34,6 @@ run_cap_test ${lnav_test} -n \
 
 run_cap_test ${lnav_test} -n \
     ${test_dir}/textfile_ansi_expanding.0
+
+run_cap_test ${lnav_test} -n \
+    ${test_dir}/textfile_0.md
diff --git a/test/textfile_0.md b/test/textfile_0.md
index 377e46ff..5f8f20c1 100644
--- a/test/textfile_0.md
+++ b/test/textfile_0.md
@@ -7,3 +7,12 @@
 * One
 * Two
 * Three
+
+Bold red
+
+Underline
+
+
+  Hello,
+  World!
+