aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/man.lua
blob: 0c67e45dc436b49281745e1bc88b7b2a5fd0c2f4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
local buf_hls = {}
local unpack = table.unpack or unpack -- luacheck: ignore

local function highlight_line(line, linenr)
  local chars = {}
  local prev_char = ''
  local overstrike, escape = false, false
  local hls = {} -- Store highlight groups as { attr, start, final }
  local NONE, BOLD, UNDERLINE, ITALIC = 0, 1, 2, 3
  local hl_groups = {[BOLD]="manBold", [UNDERLINE]="manUnderline", [ITALIC]="manItalic"}
  local attr = NONE
  local byte = 0 -- byte offset

  local function end_attr_hl(attr)
    for i, hl in ipairs(hls) do
      if hl.attr == attr and hl.final == -1 then
        hl.final = byte
        hls[i] = hl
      end
    end
  end

  local function add_attr_hl(code)
    local continue_hl = true
    if code == 0 then
      attr = NONE
      continue_hl = false
    elseif code == 1 then
      attr = BOLD
    elseif code == 22 then
      attr = BOLD
      continue_hl = false
    elseif code == 3 then
      attr = ITALIC
    elseif code == 23 then
      attr = ITALIC
      continue_hl = false
    elseif code == 4 then
      attr = UNDERLINE
    elseif code == 24 then
      attr = UNDERLINE
      continue_hl = false
    else
      attr = NONE
      return
    end

    if continue_hl then
      hls[#hls + 1] = {attr=attr, start=byte, final=-1}
    else
      if attr == NONE then
        for a, _ in pairs(hl_groups) do
          end_attr_hl(a)
        end
      else
        end_attr_hl(attr)
      end
    end
  end

  -- Break input into UTF8 code points. ASCII code points (from 0x00 to 0x7f)
  -- can be represented in one byte. Any code point above that is represented by
  -- a leading byte (0xc0 and above) and continuation bytes (0x80 to 0xbf, or
  -- decimal 128 to 191).
  for char in line:gmatch("[^\128-\191][\128-\191]*") do
    if overstrike then
      local last_hl = hls[#hls]
      if char == prev_char then
        if char == '_' and attr == UNDERLINE and last_hl and last_hl.final == byte then
          -- This underscore is in the middle of an underlined word
          attr = UNDERLINE
        else
          attr = BOLD
        end
      elseif prev_char == '_' then
        -- char is underlined
        attr = UNDERLINE
      elseif prev_char == '+' and char == 'o' then
        -- bullet (overstrike text '+^Ho')
        attr = BOLD
        char = '·'
      elseif prev_char == '·' and char == 'o' then
        -- bullet (additional handling for '+^H+^Ho^Ho')
        attr = BOLD
        char = '·'
      else
        -- use plain char
        attr = NONE
      end

      -- Grow the previous highlight group if possible
      if last_hl and last_hl.attr == attr and last_hl.final == byte then
        last_hl.final = byte + #char
      else
        hls[#hls + 1] = {attr=attr, start=byte, final=byte + #char}
      end

      overstrike = false
      prev_char = ''
      byte = byte + #char
      chars[#chars + 1] = char
    elseif escape then
      -- Use prev_char to store the escape sequence
      prev_char = prev_char .. char
      -- We only want to match against SGR sequences, which consist of ESC
      -- followed by '[', then a series of parameter and intermediate bytes in
      -- the range 0x20 - 0x3f, then 'm'. (See ECMA-48, sections 5.4 & 8.3.117)
      local sgr = prev_char:match("^%[([\032-\063]*)m$")
      if sgr then
        local match = ''
        while sgr and #sgr > 0 do
          -- Match against SGR parameters, which may be separated by ';'
          match, sgr = sgr:match("^(%d*);?(.*)")
          add_attr_hl(match + 0) -- coerce to number
        end
        escape = false
      elseif not prev_char:match("^%[[\032-\063]*$") then
        -- Stop looking if this isn't a partial CSI sequence
        escape = false
      end
    elseif char == "\027" then
      escape = true
      prev_char = ''
    elseif char == "\b" then
      overstrike = true
      prev_char = chars[#chars]
      byte = byte - #prev_char
      chars[#chars] = nil
    else
      byte = byte + #char
      chars[#chars + 1] = char
    end
  end

  for _, hl in ipairs(hls) do
    if hl.attr ~= NONE then
      buf_hls[#buf_hls + 1] = {
        0,
        -1,
        hl_groups[hl.attr],
        linenr - 1,
        hl.start,
        hl.final
      }
    end
  end

  return table.concat(chars, '')
end

local function highlight_man_page()
  local mod = vim.api.nvim_buf_get_option(0, "modifiable")
  vim.api.nvim_buf_set_option(0, "modifiable", true)

  local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
  for i, line in ipairs(lines) do
    lines[i] = highlight_line(line, i)
  end
  vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)

  for _, args in ipairs(buf_hls) do
    vim.api.nvim_buf_add_highlight(unpack(args))
  end
  buf_hls = {}

  vim.api.nvim_buf_set_option(0, "modifiable", mod)
end

return { highlight_man_page = highlight_man_page }