From 6e2174e1d680d1074b0d3a310e94d458bdea6044 Mon Sep 17 00:00:00 2001 From: Kristijan Husak Date: Wed, 30 Jun 2021 23:17:12 +0200 Subject: [PATCH] Add support for keyword faces. --- DOCS.md | 29 ++++++- doc/orgmode.txt | 33 +++++-- lua/orgmode/agenda/agenda_item.lua | 2 +- lua/orgmode/agenda/init.lua | 38 ++++---- lua/orgmode/colors/highlights.lua | 124 +++++++++++++++++++++------ lua/orgmode/config/defaults.lua | 1 + syntax/org.vim | 3 +- tests/plenary/colors/colors_spec.lua | 78 +++++++++++++++++ 8 files changed, 251 insertions(+), 57 deletions(-) diff --git a/DOCS.md b/DOCS.md index d7339fdfd..f8e87043a 100644 --- a/DOCS.md +++ b/DOCS.md @@ -46,6 +46,29 @@ Examples: * `{'TODO', 'NEXT', '|', 'DONE'}` * `{'TODO', 'WAITING', '|', 'DONE', 'DELEGATED'}` +#### **org_todo_keyword_faces** +*type*: `table`
+*default value*: `{}`
+Custom colors for todo keywords.
+Available options: +* foreground - `:foreground hex/colorname`. Examples: `:foreground #FF0000`, `:foreground blue` +* background - `:background hex/colorname`. Examples: `:background #FF0000`, `:background blue` +* weight - `:weight bold`. +* underline - `:underline on` +* italic - `:slant italic` + +Full configuration example with additional todo keywords and their colors: +```lua +require('orgmode').setup({ + org_todo_keywords = {'TODO', 'WAITING', '|', 'DONE', 'DELEGATED'}, + org_todo_keyword_faces = { + WAITING = ':foreground blue :weight bold', + DELEGATED = ':background #FFFFFF :slant italic :underline on', + TODO - ':background #000000 :foreground red', -- overrides builtin color for `TODO` keyword + } +}) +``` + #### **org_archive_location** *type*: `string`
*default value*: `'%s_archive::'`
@@ -467,8 +490,6 @@ If those colors are not suitable you can override them like this: autocmd ColorScheme * call s:setup_org_colors() function! s:setup_org_colors() abort - hi OrgTODO guifg=#FF0000 - hi OrgDONE guifg=#00FF00 hi OrgAgendaDeadline guifg=#FFAAAA hi OrgAgendaScheduled guifg=#AAFFAA hi OrgAgendaScheduledPast guifg=Orange @@ -479,10 +500,10 @@ or you can link it to another highlight group: ```vim function! s:setup_org_colors() abort - hi link OrgTODO ErrorMsg - hi link OrgDONE String hi link OrgAgendaDeadline Error hi link OrgAgendaScheduled DiffAdd hi link OrgAgendaScheduledPast Statement endfunction ``` + +For adding/changing todo keyword colors see [org-todo-keyword-faces](#org_todo_keyword_faces) diff --git a/doc/orgmode.txt b/doc/orgmode.txt index 574213328..ca226e1b4 100644 --- a/doc/orgmode.txt +++ b/doc/orgmode.txt @@ -11,7 +11,8 @@ CONTENTS *orgmode-content 1.1.1.1. org_agenda_files...................|orgmode-org_agenda_files| 1.1.1.2. org_default_notes_file.......|orgmode-org_default_notes_file| 1.1.1.3. org_todo_keywords.................|orgmode-org_todo_keywords| - 1.1.1.4. org_archive_location...........|orgmode-org_archive_location| + 1.1.1.4. org_todo_keyword_faces.......|orgmode-org_todo_keyword_faces| + 1.1.1.5. org_archive_location...........|orgmode-org_archive_location| 1.1.2. Agenda settings...........................|orgmode-agenda_settings| 1.1.2.1. org_deadline_warning_days.|orgmode-org_deadline_warning_days| 1.1.2.2. org_agenda_span.....................|orgmode-org_agenda_span| @@ -129,6 +130,30 @@ Examples: * `{'TODO', 'NEXT', '|', 'DONE'}` * `{'TODO', 'WAITING', '|', 'DONE', 'DELEGATED'}` +ORG_TODO_KEYWORD_FACES *orgmode-org_todo_keyword_faces* + +type: `table` +default value: `{}` +Custom colors for todo keywords. +Available options: +* foreground - `:foreground hex/colorname`. Examples: `:foreground #FF0000`, `:foreground blue` +* background - `:background hex/colorname`. Examples: `:background #FF0000`, `:background blue` +* weight - `:weight bold`. +* underline - `:underline on` +* italic - `:slant italic` + +Full configuration example with additional todo keywords and their colors: +> + require('orgmode').setup({ + org_todo_keywords = {'TODO', 'WAITING', '|', 'DONE', 'DELEGATED'}, + org_todo_keyword_faces = { + WAITING = ':foreground blue :weight bold', + DELEGATED = ':background #FFFFFF :slant italic :underline on', + TODO - ':background #000000 :foreground red', -- overrides builtin color for `TODO` keyword + } + }) +< + ORG_ARCHIVE_LOCATION *orgmode-org_archive_location* type: `string` @@ -644,8 +669,6 @@ If those colors are not suitable you can override them like this: > autocmd ColorScheme * call s:setup_org_colors() function! s:setup_org_colors() abort - hi OrgTODO guifg=#FF0000 - hi OrgDONE guifg=#00FF00 hi OrgAgendaDeadline guifg=#FFAAAA hi OrgAgendaScheduled guifg=#AAFFAA hi OrgAgendaScheduledPast guifg=Orange @@ -655,11 +678,11 @@ If those colors are not suitable you can override them like this: or you can link it to another highlight group: > function! s:setup_org_colors() abort - hi link OrgTODO ErrorMsg - hi link OrgDONE String hi link OrgAgendaDeadline Error hi link OrgAgendaScheduled DiffAdd hi link OrgAgendaScheduledPast Statement endfunction < +For adding/changing todo keyword colors see org-todo-keyword-faces (#org_todo_keyword_faces) + diff --git a/lua/orgmode/agenda/agenda_item.lua b/lua/orgmode/agenda/agenda_item.lua index 383fcb4f6..45007c026 100644 --- a/lua/orgmode/agenda/agenda_item.lua +++ b/lua/orgmode/agenda/agenda_item.lua @@ -161,7 +161,7 @@ end function AgendaItem:_add_keyword_highlight() if self.headline.todo_keyword.value == '' then return end - local hlgroup = hl_map[self.headline.todo_keyword.type] + local hlgroup = hl_map[self.headline.todo_keyword.value] if hlgroup then table.insert(self.highlights, { hlgroup = hlgroup, diff --git a/lua/orgmode/agenda/init.lua b/lua/orgmode/agenda/init.lua index 5116de368..0c01f1f44 100644 --- a/lua/orgmode/agenda/init.lua +++ b/lua/orgmode/agenda/init.lua @@ -132,13 +132,15 @@ function Agenda:render() date = string.format(' %-'..date_len..'s', agenda_item.label) end local todo_keyword = agenda_item.headline.todo_keyword.value + local todo_padding = '' if todo_keyword ~= '' and vim.trim(agenda_item.label):find(':$') then - todo_keyword = ' '..todo_keyword + todo_padding = ' ' end + todo_keyword = todo_padding..todo_keyword local line = string.format( '%s%s%s %s', category, date, todo_keyword, headline.title ) - local todo_keyword_pos = string.format('%s%s ', category, date):len() + local todo_keyword_pos = string.format('%s%s%s', category, date, todo_padding):len() if #headline.tags > 0 then line = string.format('%-99s %s', line, headline:tags_to_string()) end @@ -152,7 +154,7 @@ function Agenda:render() end_col = 0, }) if hl.todo_keyword then - hl.range.start_col = todo_keyword_pos + hl.range.start_col = todo_keyword_pos + 1 hl.range.end_col = todo_keyword_pos + hl.todo_keyword:len() + 1 end return hl @@ -220,7 +222,7 @@ function Agenda:todos() if #todo.tags > 0 then line = string.format('%-99s %s', line, todo:tags_to_string()) end - local todo_keyword_pos = category:len() + 3 + local todo_keyword_pos = category:len() + 4 table.insert(content, { line_content = line, line = i + 1, @@ -230,12 +232,12 @@ function Agenda:todos() }) table.insert(highlights, { - hlgroup = hl_map[todo.todo_keyword.type], + hlgroup = hl_map[todo.todo_keyword.value], range = Range:new({ start_line = i + 1, end_line = i + 1, start_col = todo_keyword_pos, - end_col = todo_keyword_pos + todo_keyword:len() + 1 + end_col = todo_keyword_pos + todo_keyword:len() }) }) end @@ -270,10 +272,8 @@ function Agenda:search(clear_search) for i, headline in ipairs(headlines) do local category = string.format(' %-'..(longest_category + 1)..'s', headline:get_category()..':') local todo_keyword = headline.todo_keyword.value - if todo_keyword ~= '' then - todo_keyword = ' '..todo_keyword - end - local line = string.format(' %s%s %s', category, todo_keyword, headline.title) + local todo_keyword_padding = todo_keyword ~= '' and ' ' or '' + local line = string.format(' %s%s%s %s', category, todo_keyword_padding, todo_keyword, headline.title) if #headline.tags > 0 then line = string.format('%-99s %s', line, headline:tags_to_string()) end @@ -286,14 +286,14 @@ function Agenda:search(clear_search) }) if headline.todo_keyword.value ~= '' then - local todo_keyword_pos = category:len() + 3 + local todo_keyword_pos = category:len() + 4 table.insert(highlights, { - hlgroup = hl_map[headline.todo_keyword.type], + hlgroup = hl_map[headline.todo_keyword.value], range = Range:new({ start_line = i + 2, end_line = i + 2, start_col = todo_keyword_pos, - end_col = todo_keyword_pos + todo_keyword:len() + 1 + end_col = todo_keyword_pos + todo_keyword:len() }) }) end @@ -338,10 +338,8 @@ function Agenda:tags(clear_search) for i, headline in ipairs(headlines) do local category = string.format(' %-'..(longest_category + 1)..'s', headline:get_category()..':') local todo_keyword = headline.todo_keyword.value - if todo_keyword ~= '' then - todo_keyword = ' '..todo_keyword - end - local line = string.format(' %s%s %s', category, todo_keyword, headline.title) + local todo_keyword_padding = todo_keyword ~= '' and ' ' or '' + local line = string.format(' %s%s%s %s', category, todo_keyword_padding, todo_keyword, headline.title) if #headline.tags > 0 then line = string.format('%-99s %s', line, headline:tags_to_string()) end @@ -354,14 +352,14 @@ function Agenda:tags(clear_search) }) if headline.todo_keyword.value ~= '' then - local todo_keyword_pos = category:len() + 3 + local todo_keyword_pos = category:len() + 4 table.insert(highlights, { - hlgroup = hl_map[headline.todo_keyword.type], + hlgroup = hl_map[headline.todo_keyword.value], range = Range:new({ start_line = i + 2, end_line = i + 2, start_col = todo_keyword_pos, - end_col = todo_keyword_pos + todo_keyword:len() + 1 + end_col = todo_keyword_pos + todo_keyword:len() }) }) end diff --git a/lua/orgmode/colors/highlights.lua b/lua/orgmode/colors/highlights.lua index b1e7b1151..ebcdb3b9e 100644 --- a/lua/orgmode/colors/highlights.lua +++ b/lua/orgmode/colors/highlights.lua @@ -3,53 +3,127 @@ local colors = require('orgmode.colors') local M = {} function M.define_agenda_colors() - local hl_map = M.get_agenda_hl_map() local keyword_colors = colors.get_todo_keywords_colors() - local todo_keywords = config:get_todo_keywords() - for type, hlname in pairs(hl_map) do - local bold = '' - if vim.tbl_contains(todo_keywords.ALL, type) then - bold = ' gui=bold' - end - vim.cmd(string.format('hi %s_builtin guifg=%s ctermfg=%s%s', hlname, keyword_colors[type].gui, keyword_colors[type].cterm, bold)) + local c = { + deadline = 'OrgAgendaDeadline', + ok = 'OrgAgendaScheduled', + warning = 'OrgAgendaScheduledPast' + } + for type, hlname in pairs(c) do + vim.cmd(string.format('hi %s_builtin guifg=%s ctermfg=%s', hlname, keyword_colors[type].gui, keyword_colors[type].cterm)) vim.cmd(string.format('hi default link %s %s_builtin', hlname, hlname)) end + M.define_org_todo_keyword_colors() end -function M.define_org_todo_keyword_colors() +function M.define_org_todo_keyword_colors(do_syn_match) local keyword_colors = colors.get_todo_keywords_colors() local todo_keywords = config:get_todo_keywords() - vim.cmd(string.format([[syn match OrgTODO "\<\(%s\)\>" contained]], table.concat(todo_keywords.TODO, [[\|]]))) - vim.cmd(string.format([[syn match OrgDONE "\<\(%s\)\>" contained]], table.concat(todo_keywords.DONE, [[\|]]))) - vim.cmd(string.format('hi OrgTODO_builtin guifg=%s ctermfg=%s', keyword_colors.TODO.gui, keyword_colors.TODO.cterm)) + if do_syn_match then + vim.cmd(string.format([[syn match OrgTODO "\<\(%s\)\>" contained]], table.concat(todo_keywords.TODO, [[\|]]))) + vim.cmd(string.format([[syn match OrgDONE "\<\(%s\)\>" contained]], table.concat(todo_keywords.DONE, [[\|]]))) + end + vim.cmd(string.format('hi OrgTODO_builtin guifg=%s ctermfg=%s gui=bold cterm=bold', keyword_colors.TODO.gui, keyword_colors.TODO.cterm)) vim.cmd('hi default link OrgTODO OrgTODO_builtin') - vim.cmd(string.format('hi OrgDONE_builtin guifg=%s ctermfg=%s', keyword_colors.DONE.gui, keyword_colors.DONE.cterm)) + vim.cmd(string.format('hi OrgDONE_builtin guifg=%s ctermfg=%s gui=bold cterm=bold', keyword_colors.DONE.gui, keyword_colors.DONE.cterm)) vim.cmd('hi default link OrgDONE OrgDONE_builtin') + return M.parse_todo_keyword_faces(do_syn_match) end -function M.define_org_headline_colors() -local headline_colors = {'Title', 'Constant', 'Identifier', 'Statement', 'PreProc', 'Type', 'Special', 'String'} -local todo_keywords = config:get_todo_keywords() -local all_keywords = table.concat(todo_keywords.ALL, [[\|]]) -for i, color in ipairs(headline_colors) do - local j = i - while j < 40 do - vim.cmd(string.format([[syn match OrgHeadlineLevel%d "^\*\{%d}\s\+\(\<\(%s\)\>\)\?.*$" contains=OrgTODO,OrgDONE]], j, j, all_keywords)) - vim.cmd(string.format('hi default link OrgHeadlineLevel%d %s', j, color)) - j = j + 8 +function M.define_org_headline_colors(faces) + local headline_colors = {'Title', 'Constant', 'Identifier', 'Statement', 'PreProc', 'Type', 'Special', 'String'} + local todo_keywords = config:get_todo_keywords() + local all_keywords = table.concat(todo_keywords.ALL, [[\|]]) + local contains = {'OrgTODO', 'OrgDONE'} + for _, face in pairs(faces) do + table.insert(contains, face) + end + contains = table.concat(contains, ',') + for i, color in ipairs(headline_colors) do + local j = i + while j < 40 do + vim.cmd(string.format([[syn match OrgHeadlineLevel%d "^\*\{%d}\s\+\(\<\(%s\)\>\)\?.*$" contains=%s]], j, j, all_keywords, contains)) + vim.cmd(string.format('hi default link OrgHeadlineLevel%d %s', j, color)) + j = j + 8 + end end end + +function M.define_highlights() + local faces = M.define_org_todo_keyword_colors(true) + return M.define_org_headline_colors(faces) +end + +function M.parse_todo_keyword_faces(do_syn_match) + local opts = { + underline = { + type = vim.o.termguicolors and 'gui' or 'cterm', + valid = 'on', + result = 'underline' + }, + weight = { + type = vim.o.termguicolors and 'gui' or 'cterm', + valid = 'bold' + }, + foreground = { + type = vim.o.termguicolors and 'guifg' or 'ctermfg', + }, + background = { + type = vim.o.termguicolors and 'guibg' or 'ctermbg', + }, + slant = { + type = vim.o.termguicolors and 'gui' or 'cterm', + valid = 'italic' + } + } + + local result = {} + + for name, values in pairs(config.org_todo_keyword_faces) do + local parts = vim.split(values, ':', true) + local hl_opts = {} + for _, part in ipairs(parts) do + local faces = vim.split(vim.trim(part), ' ') + if #faces == 2 then + local opt_name = vim.trim(faces[1]) + local opt_value = vim.trim(faces[2]) + opt_value = opt_value:gsub('^"*', ''):gsub('"*$', '') + local opt = opts[opt_name] + if opt and (not opt.valid or opt.valid == opt_value) then + if not hl_opts[opt.type] then + hl_opts[opt.type] = {} + end + table.insert(hl_opts[opt.type], opt.result or opt_value) + end + end + end + if not vim.tbl_isempty(hl_opts) then + local hl_name = 'OrgKeywordFace'..name + local hl = '' + for hl_item, hl_values in pairs(hl_opts) do + hl = hl..' '..hl_item..'='..table.concat(hl_values, ',') + end + if do_syn_match then + vim.cmd(string.format([[syn match %s "\<%s\>" contained]], hl_name, name)) + end + vim.cmd(string.format('hi %s %s', hl_name, hl)) + result[name] = hl_name + end + end + + return result end ---@return table function M.get_agenda_hl_map() - return { + local faces = M.parse_todo_keyword_faces() + return vim.tbl_extend('force', { TODO = 'OrgTODO', DONE = 'OrgDONE', deadline = 'OrgAgendaDeadline', ok = 'OrgAgendaScheduled', warning = 'OrgAgendaScheduledPast' - } + }, faces) end return M diff --git a/lua/orgmode/config/defaults.lua b/lua/orgmode/config/defaults.lua index 253fa4765..149a486a8 100644 --- a/lua/orgmode/config/defaults.lua +++ b/lua/orgmode/config/defaults.lua @@ -2,6 +2,7 @@ return { org_agenda_files = '', org_default_notes_file = '', org_todo_keywords = {'TODO', '|', 'DONE'}, + org_todo_keyword_faces = {}, org_deadline_warning_days = 14, org_agenda_span = 'week', -- day/week/month/year/number of days org_agenda_start_on_weekday = 1, diff --git a/syntax/org.vim b/syntax/org.vim index d720557b4..6268d206d 100644 --- a/syntax/org.vim +++ b/syntax/org.vim @@ -4,8 +4,7 @@ if exists('b:current_syntax') finish endif -lua require('orgmode.colors.highlights').define_org_todo_keyword_colors() -lua require('orgmode.colors.highlights').define_org_headline_colors() +lua require('orgmode.colors.highlights').define_highlights() " Support org authoring markup as closely as possible " (we're adding two markdown-like variants for =code= and blockquotes) diff --git a/tests/plenary/colors/colors_spec.lua b/tests/plenary/colors/colors_spec.lua index b269533bf..16cd7a4d5 100644 --- a/tests/plenary/colors/colors_spec.lua +++ b/tests/plenary/colors/colors_spec.lua @@ -1,4 +1,6 @@ local colors = require('orgmode.colors') +local highlights = require('orgmode.colors.highlights') +local config = require('orgmode.config') describe('Colors', function() it('should lighten the color', function() @@ -47,4 +49,80 @@ describe('Colors', function() warning = { gui = "#ff981a", cterm = 11 }, }, todo_keywords_colors) end) + + it('should parse todo keyword faces', function() + local get_color_opt = function(hlgroup, name, type) + return vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(hlgroup)), name, type) + end + config:extend({ + org_todo_keyword_faces = { + NEXT = ':foreground "blue" :underline on :weight bold :background red :slant italic', + CANCELED = ':foreground green :slant italic', + } + }) + + local result = highlights.parse_todo_keyword_faces() + + assert.are.same({ + NEXT = 'OrgKeywordFaceNEXT', + CANCELED = 'OrgKeywordFaceCANCELED' + }, result) + + assert.are.same('red', get_color_opt('OrgKeywordFaceNEXT', 'bg', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'bg', 'cterm')) + assert.are.same('blue', get_color_opt('OrgKeywordFaceNEXT', 'fg', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'fg', 'cterm')) + assert.are.same('1', get_color_opt('OrgKeywordFaceNEXT', 'bold', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'bold', 'cterm')) + assert.are.same('1', get_color_opt('OrgKeywordFaceNEXT', 'italic', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'italic', 'cterm')) + assert.are.same('1', get_color_opt('OrgKeywordFaceNEXT', 'underline', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'underline', 'cterm')) + + assert.are.same('green', get_color_opt('OrgKeywordFaceCANCELED', 'fg', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'fg', 'cterm')) + assert.are.same('1', get_color_opt('OrgKeywordFaceCANCELED', 'italic', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'italic', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'bg', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'bg', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'bold', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'bold', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'underline', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'underline', 'cterm')) + + vim.cmd[[ + hi clear OrgKeywordFaceNEXT + hi clear OrgKeywordFaceCANCELED + ]] + + vim.o.termguicolors = false + result = highlights.parse_todo_keyword_faces() + + assert.are.same({ + NEXT = 'OrgKeywordFaceNEXT', + CANCELED = 'OrgKeywordFaceCANCELED' + }, result) + + assert.are.same('9', get_color_opt('OrgKeywordFaceNEXT', 'bg', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'bg', 'gui')) + assert.are.same('12', get_color_opt('OrgKeywordFaceNEXT', 'fg', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'fg', 'gui')) + assert.are.same('1', get_color_opt('OrgKeywordFaceNEXT', 'bold', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'bold', 'gui')) + assert.are.same('1', get_color_opt('OrgKeywordFaceNEXT', 'italic', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'italic', 'gui')) + assert.are.same('1', get_color_opt('OrgKeywordFaceNEXT', 'underline', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceNEXT', 'underline', 'gui')) + + assert.are.same('10', get_color_opt('OrgKeywordFaceCANCELED', 'fg', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'fg', 'gui')) + assert.are.same('1', get_color_opt('OrgKeywordFaceCANCELED', 'italic', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'italic', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'bg', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'bg', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'bold', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'bold', 'cterm')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'underline', 'gui')) + assert.are.same('', get_color_opt('OrgKeywordFaceCANCELED', 'underline', 'cterm')) + end) end)