1. package html
    
  2. 
    
  3. import (
    
  4. 	"fmt"
    
  5. 	"html"
    
  6. 	"io"
    
  7. 	"sort"
    
  8. 	"strconv"
    
  9. 	"strings"
    
  10. 
    
  11. 	"github.com/alecthomas/chroma/v2"
    
  12. )
    
  13. 
    
  14. // Option sets an option of the HTML formatter.
    
  15. type Option func(f *Formatter)
    
  16. 
    
  17. // Standalone configures the HTML formatter for generating a standalone HTML document.
    
  18. func Standalone(b bool) Option { return func(f *Formatter) { f.standalone = b } }
    
  19. 
    
  20. // ClassPrefix sets the CSS class prefix.
    
  21. func ClassPrefix(prefix string) Option { return func(f *Formatter) { f.prefix = prefix } }
    
  22. 
    
  23. // WithClasses emits HTML using CSS classes, rather than inline styles.
    
  24. func WithClasses(b bool) Option { return func(f *Formatter) { f.Classes = b } }
    
  25. 
    
  26. // WithAllClasses disables an optimisation that omits redundant CSS classes.
    
  27. func WithAllClasses(b bool) Option { return func(f *Formatter) { f.allClasses = b } }
    
  28. 
    
  29. // WithCustomCSS sets user's custom CSS styles.
    
  30. func WithCustomCSS(css map[chroma.TokenType]string) Option {
    
  31. 	return func(f *Formatter) {
    
  32. 		f.customCSS = css
    
  33. 	}
    
  34. }
    
  35. 
    
  36. // TabWidth sets the number of characters for a tab. Defaults to 8.
    
  37. func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } }
    
  38. 
    
  39. // PreventSurroundingPre prevents the surrounding pre tags around the generated code.
    
  40. func PreventSurroundingPre(b bool) Option {
    
  41. 	return func(f *Formatter) {
    
  42. 		f.preventSurroundingPre = b
    
  43. 
    
  44. 		if b {
    
  45. 			f.preWrapper = nopPreWrapper
    
  46. 		} else {
    
  47. 			f.preWrapper = defaultPreWrapper
    
  48. 		}
    
  49. 	}
    
  50. }
    
  51. 
    
  52. // InlineCode creates inline code wrapped in a code tag.
    
  53. func InlineCode(b bool) Option {
    
  54. 	return func(f *Formatter) {
    
  55. 		f.inlineCode = b
    
  56. 		f.preWrapper = preWrapper{
    
  57. 			start: func(code bool, styleAttr string) string {
    
  58. 				if code {
    
  59. 					return fmt.Sprintf(`<code%s>`, styleAttr)
    
  60. 				}
    
  61. 
    
  62. 				return ``
    
  63. 			},
    
  64. 			end: func(code bool) string {
    
  65. 				if code {
    
  66. 					return `</code>`
    
  67. 				}
    
  68. 
    
  69. 				return ``
    
  70. 			},
    
  71. 		}
    
  72. 	}
    
  73. }
    
  74. 
    
  75. // WithPreWrapper allows control of the surrounding pre tags.
    
  76. func WithPreWrapper(wrapper PreWrapper) Option {
    
  77. 	return func(f *Formatter) {
    
  78. 		f.preWrapper = wrapper
    
  79. 	}
    
  80. }
    
  81. 
    
  82. // WrapLongLines wraps long lines.
    
  83. func WrapLongLines(b bool) Option {
    
  84. 	return func(f *Formatter) {
    
  85. 		f.wrapLongLines = b
    
  86. 	}
    
  87. }
    
  88. 
    
  89. // WithLineNumbers formats output with line numbers.
    
  90. func WithLineNumbers(b bool) Option {
    
  91. 	return func(f *Formatter) {
    
  92. 		f.lineNumbers = b
    
  93. 	}
    
  94. }
    
  95. 
    
  96. // LineNumbersInTable will, when combined with WithLineNumbers, separate the line numbers
    
  97. // and code in table td's, which make them copy-and-paste friendly.
    
  98. func LineNumbersInTable(b bool) Option {
    
  99. 	return func(f *Formatter) {
    
  100. 		f.lineNumbersInTable = b
    
  101. 	}
    
  102. }
    
  103. 
    
  104. // WithLinkableLineNumbers decorates the line numbers HTML elements with an "id"
    
  105. // attribute so they can be linked.
    
  106. func WithLinkableLineNumbers(b bool, prefix string) Option {
    
  107. 	return func(f *Formatter) {
    
  108. 		f.linkableLineNumbers = b
    
  109. 		f.lineNumbersIDPrefix = prefix
    
  110. 	}
    
  111. }
    
  112. 
    
  113. // HighlightLines higlights the given line ranges with the Highlight style.
    
  114. //
    
  115. // A range is the beginning and ending of a range as 1-based line numbers, inclusive.
    
  116. func HighlightLines(ranges [][2]int) Option {
    
  117. 	return func(f *Formatter) {
    
  118. 		f.highlightRanges = ranges
    
  119. 		sort.Sort(f.highlightRanges)
    
  120. 	}
    
  121. }
    
  122. 
    
  123. // BaseLineNumber sets the initial number to start line numbering at. Defaults to 1.
    
  124. func BaseLineNumber(n int) Option {
    
  125. 	return func(f *Formatter) {
    
  126. 		f.baseLineNumber = n
    
  127. 	}
    
  128. }
    
  129. 
    
  130. // New HTML formatter.
    
  131. func New(options ...Option) *Formatter {
    
  132. 	f := &Formatter{
    
  133. 		baseLineNumber: 1,
    
  134. 		preWrapper:     defaultPreWrapper,
    
  135. 	}
    
  136. 	for _, option := range options {
    
  137. 		option(f)
    
  138. 	}
    
  139. 	return f
    
  140. }
    
  141. 
    
  142. // PreWrapper defines the operations supported in WithPreWrapper.
    
  143. type PreWrapper interface {
    
  144. 	// Start is called to write a start <pre> element.
    
  145. 	// The code flag tells whether this block surrounds
    
  146. 	// highlighted code. This will be false when surrounding
    
  147. 	// line numbers.
    
  148. 	Start(code bool, styleAttr string) string
    
  149. 
    
  150. 	// End is called to write the end </pre> element.
    
  151. 	End(code bool) string
    
  152. }
    
  153. 
    
  154. type preWrapper struct {
    
  155. 	start func(code bool, styleAttr string) string
    
  156. 	end   func(code bool) string
    
  157. }
    
  158. 
    
  159. func (p preWrapper) Start(code bool, styleAttr string) string {
    
  160. 	return p.start(code, styleAttr)
    
  161. }
    
  162. 
    
  163. func (p preWrapper) End(code bool) string {
    
  164. 	return p.end(code)
    
  165. }
    
  166. 
    
  167. var (
    
  168. 	nopPreWrapper = preWrapper{
    
  169. 		start: func(code bool, styleAttr string) string { return "" },
    
  170. 		end:   func(code bool) string { return "" },
    
  171. 	}
    
  172. 	defaultPreWrapper = preWrapper{
    
  173. 		start: func(code bool, styleAttr string) string {
    
  174. 			if code {
    
  175. 				return fmt.Sprintf(`<pre%s><code>`, styleAttr)
    
  176. 			}
    
  177. 
    
  178. 			return fmt.Sprintf(`<pre%s>`, styleAttr)
    
  179. 		},
    
  180. 		end: func(code bool) string {
    
  181. 			if code {
    
  182. 				return `</code></pre>`
    
  183. 			}
    
  184. 
    
  185. 			return `</pre>`
    
  186. 		},
    
  187. 	}
    
  188. )
    
  189. 
    
  190. // Formatter that generates HTML.
    
  191. type Formatter struct {
    
  192. 	standalone            bool
    
  193. 	prefix                string
    
  194. 	Classes               bool // Exported field to detect when classes are being used
    
  195. 	allClasses            bool
    
  196. 	customCSS             map[chroma.TokenType]string
    
  197. 	preWrapper            PreWrapper
    
  198. 	inlineCode            bool
    
  199. 	preventSurroundingPre bool
    
  200. 	tabWidth              int
    
  201. 	wrapLongLines         bool
    
  202. 	lineNumbers           bool
    
  203. 	lineNumbersInTable    bool
    
  204. 	linkableLineNumbers   bool
    
  205. 	lineNumbersIDPrefix   string
    
  206. 	highlightRanges       highlightRanges
    
  207. 	baseLineNumber        int
    
  208. }
    
  209. 
    
  210. type highlightRanges [][2]int
    
  211. 
    
  212. func (h highlightRanges) Len() int           { return len(h) }
    
  213. func (h highlightRanges) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
    
  214. func (h highlightRanges) Less(i, j int) bool { return h[i][0] < h[j][0] }
    
  215. 
    
  216. func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
    
  217. 	return f.writeHTML(w, style, iterator.Tokens())
    
  218. }
    
  219. 
    
  220. // We deliberately don't use html/template here because it is two orders of magnitude slower (benchmarked).
    
  221. //
    
  222. // OTOH we need to be super careful about correct escaping...
    
  223. func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.Token) (err error) { // nolint: gocyclo
    
  224. 	css := f.styleToCSS(style)
    
  225. 	if !f.Classes {
    
  226. 		for t, style := range css {
    
  227. 			css[t] = compressStyle(style)
    
  228. 		}
    
  229. 	}
    
  230. 	if f.standalone {
    
  231. 		fmt.Fprint(w, "<html>\n")
    
  232. 		if f.Classes {
    
  233. 			fmt.Fprint(w, "<style type=\"text/css\">\n")
    
  234. 			err = f.WriteCSS(w, style)
    
  235. 			if err != nil {
    
  236. 				return err
    
  237. 			}
    
  238. 			fmt.Fprintf(w, "body { %s; }\n", css[chroma.Background])
    
  239. 			fmt.Fprint(w, "</style>")
    
  240. 		}
    
  241. 		fmt.Fprintf(w, "<body%s>\n", f.styleAttr(css, chroma.Background))
    
  242. 	}
    
  243. 
    
  244. 	wrapInTable := f.lineNumbers && f.lineNumbersInTable
    
  245. 
    
  246. 	lines := chroma.SplitTokensIntoLines(tokens)
    
  247. 	lineDigits := len(strconv.Itoa(f.baseLineNumber + len(lines) - 1))
    
  248. 	highlightIndex := 0
    
  249. 
    
  250. 	if wrapInTable {
    
  251. 		// List line numbers in its own <td>
    
  252. 		fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.PreWrapper))
    
  253. 		fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
    
  254. 		fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD))
    
  255. 		fmt.Fprintf(w, "%s", f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
    
  256. 		for index := range lines {
    
  257. 			line := f.baseLineNumber + index
    
  258. 			highlight, next := f.shouldHighlight(highlightIndex, line)
    
  259. 			if next {
    
  260. 				highlightIndex++
    
  261. 			}
    
  262. 			if highlight {
    
  263. 				fmt.Fprintf(w, "<span%s>", f.styleAttr(css, chroma.LineHighlight))
    
  264. 			}
    
  265. 
    
  266. 			fmt.Fprintf(w, "<span%s%s>%s\n</span>", f.styleAttr(css, chroma.LineNumbersTable), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(css, lineDigits, line))
    
  267. 
    
  268. 			if highlight {
    
  269. 				fmt.Fprintf(w, "</span>")
    
  270. 			}
    
  271. 		}
    
  272. 		fmt.Fprint(w, f.preWrapper.End(false))
    
  273. 		fmt.Fprint(w, "</td>\n")
    
  274. 		fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD, "width:100%"))
    
  275. 	}
    
  276. 
    
  277. 	fmt.Fprintf(w, "%s", f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
    
  278. 
    
  279. 	highlightIndex = 0
    
  280. 	for index, tokens := range lines {
    
  281. 		// 1-based line number.
    
  282. 		line := f.baseLineNumber + index
    
  283. 		highlight, next := f.shouldHighlight(highlightIndex, line)
    
  284. 		if next {
    
  285. 			highlightIndex++
    
  286. 		}
    
  287. 
    
  288. 		if !(f.preventSurroundingPre || f.inlineCode) {
    
  289. 			// Start of Line
    
  290. 			fmt.Fprint(w, `<span`)
    
  291. 
    
  292. 			if highlight {
    
  293. 				// Line + LineHighlight
    
  294. 				if f.Classes {
    
  295. 					fmt.Fprintf(w, ` class="%s %s"`, f.class(chroma.Line), f.class(chroma.LineHighlight))
    
  296. 				} else {
    
  297. 					fmt.Fprintf(w, ` style="%s %s"`, css[chroma.Line], css[chroma.LineHighlight])
    
  298. 				}
    
  299. 				fmt.Fprint(w, `>`)
    
  300. 			} else {
    
  301. 				fmt.Fprintf(w, "%s>", f.styleAttr(css, chroma.Line))
    
  302. 			}
    
  303. 
    
  304. 			// Line number
    
  305. 			if f.lineNumbers && !wrapInTable {
    
  306. 				fmt.Fprintf(w, "<span%s%s>%s</span>", f.styleAttr(css, chroma.LineNumbers), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(css, lineDigits, line))
    
  307. 			}
    
  308. 
    
  309. 			fmt.Fprintf(w, `<span%s>`, f.styleAttr(css, chroma.CodeLine))
    
  310. 		}
    
  311. 
    
  312. 		for _, token := range tokens {
    
  313. 			html := html.EscapeString(token.String())
    
  314. 			attr := f.styleAttr(css, token.Type)
    
  315. 			if attr != "" {
    
  316. 				html = fmt.Sprintf("<span%s>%s</span>", attr, html)
    
  317. 			}
    
  318. 			fmt.Fprint(w, html)
    
  319. 		}
    
  320. 
    
  321. 		if !(f.preventSurroundingPre || f.inlineCode) {
    
  322. 			fmt.Fprint(w, `</span>`) // End of CodeLine
    
  323. 
    
  324. 			fmt.Fprint(w, `</span>`) // End of Line
    
  325. 		}
    
  326. 	}
    
  327. 	fmt.Fprintf(w, "%s", f.preWrapper.End(true))
    
  328. 
    
  329. 	if wrapInTable {
    
  330. 		fmt.Fprint(w, "</td></tr></table>\n")
    
  331. 		fmt.Fprint(w, "</div>\n")
    
  332. 	}
    
  333. 
    
  334. 	if f.standalone {
    
  335. 		fmt.Fprint(w, "\n</body>\n")
    
  336. 		fmt.Fprint(w, "</html>\n")
    
  337. 	}
    
  338. 
    
  339. 	return nil
    
  340. }
    
  341. 
    
  342. func (f *Formatter) lineIDAttribute(line int) string {
    
  343. 	if !f.linkableLineNumbers {
    
  344. 		return ""
    
  345. 	}
    
  346. 	return fmt.Sprintf(" id=\"%s\"", f.lineID(line))
    
  347. }
    
  348. 
    
  349. func (f *Formatter) lineTitleWithLinkIfNeeded(css map[chroma.TokenType]string, lineDigits, line int) string {
    
  350. 	title := fmt.Sprintf("%*d", lineDigits, line)
    
  351. 	if !f.linkableLineNumbers {
    
  352. 		return title
    
  353. 	}
    
  354. 	return fmt.Sprintf("<a%s href=\"#%s\">%s</a>", f.styleAttr(css, chroma.LineLink), f.lineID(line), title)
    
  355. }
    
  356. 
    
  357. func (f *Formatter) lineID(line int) string {
    
  358. 	return fmt.Sprintf("%s%d", f.lineNumbersIDPrefix, line)
    
  359. }
    
  360. 
    
  361. func (f *Formatter) shouldHighlight(highlightIndex, line int) (bool, bool) {
    
  362. 	next := false
    
  363. 	for highlightIndex < len(f.highlightRanges) && line > f.highlightRanges[highlightIndex][1] {
    
  364. 		highlightIndex++
    
  365. 		next = true
    
  366. 	}
    
  367. 	if highlightIndex < len(f.highlightRanges) {
    
  368. 		hrange := f.highlightRanges[highlightIndex]
    
  369. 		if line >= hrange[0] && line <= hrange[1] {
    
  370. 			return true, next
    
  371. 		}
    
  372. 	}
    
  373. 	return false, next
    
  374. }
    
  375. 
    
  376. func (f *Formatter) class(t chroma.TokenType) string {
    
  377. 	for t != 0 {
    
  378. 		if cls, ok := chroma.StandardTypes[t]; ok {
    
  379. 			if cls != "" {
    
  380. 				return f.prefix + cls
    
  381. 			}
    
  382. 			return ""
    
  383. 		}
    
  384. 		t = t.Parent()
    
  385. 	}
    
  386. 	if cls := chroma.StandardTypes[t]; cls != "" {
    
  387. 		return f.prefix + cls
    
  388. 	}
    
  389. 	return ""
    
  390. }
    
  391. 
    
  392. func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType, extraCSS ...string) string {
    
  393. 	if f.Classes {
    
  394. 		cls := f.class(tt)
    
  395. 		if cls == "" {
    
  396. 			return ""
    
  397. 		}
    
  398. 		return fmt.Sprintf(` class="%s"`, cls)
    
  399. 	}
    
  400. 	if _, ok := styles[tt]; !ok {
    
  401. 		tt = tt.SubCategory()
    
  402. 		if _, ok := styles[tt]; !ok {
    
  403. 			tt = tt.Category()
    
  404. 			if _, ok := styles[tt]; !ok {
    
  405. 				return ""
    
  406. 			}
    
  407. 		}
    
  408. 	}
    
  409. 	css := []string{styles[tt]}
    
  410. 	css = append(css, extraCSS...)
    
  411. 	return fmt.Sprintf(` style="%s"`, strings.Join(css, ";"))
    
  412. }
    
  413. 
    
  414. func (f *Formatter) tabWidthStyle() string {
    
  415. 	if f.tabWidth != 0 && f.tabWidth != 8 {
    
  416. 		return fmt.Sprintf("-moz-tab-size: %[1]d; -o-tab-size: %[1]d; tab-size: %[1]d;", f.tabWidth)
    
  417. 	}
    
  418. 	return ""
    
  419. }
    
  420. 
    
  421. // WriteCSS writes CSS style definitions (without any surrounding HTML).
    
  422. func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
    
  423. 	css := f.styleToCSS(style)
    
  424. 	// Special-case background as it is mapped to the outer ".chroma" class.
    
  425. 	if _, err := fmt.Fprintf(w, "/* %s */ .%sbg { %s }\n", chroma.Background, f.prefix, css[chroma.Background]); err != nil {
    
  426. 		return err
    
  427. 	}
    
  428. 	// Special-case PreWrapper as it is the ".chroma" class.
    
  429. 	if _, err := fmt.Fprintf(w, "/* %s */ .%schroma { %s }\n", chroma.PreWrapper, f.prefix, css[chroma.PreWrapper]); err != nil {
    
  430. 		return err
    
  431. 	}
    
  432. 	// Special-case code column of table to expand width.
    
  433. 	if f.lineNumbers && f.lineNumbersInTable {
    
  434. 		if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s:last-child { width: 100%%; }",
    
  435. 			chroma.LineTableTD, f.prefix, f.class(chroma.LineTableTD)); err != nil {
    
  436. 			return err
    
  437. 		}
    
  438. 	}
    
  439. 	// Special-case line number highlighting when targeted.
    
  440. 	if f.lineNumbers || f.lineNumbersInTable {
    
  441. 		targetedLineCSS := StyleEntryToCSS(style.Get(chroma.LineHighlight))
    
  442. 		for _, tt := range []chroma.TokenType{chroma.LineNumbers, chroma.LineNumbersTable} {
    
  443. 			fmt.Fprintf(w, "/* %s targeted by URL anchor */ .%schroma .%s:target { %s }\n", tt, f.prefix, f.class(tt), targetedLineCSS)
    
  444. 		}
    
  445. 	}
    
  446. 	tts := []int{}
    
  447. 	for tt := range css {
    
  448. 		tts = append(tts, int(tt))
    
  449. 	}
    
  450. 	sort.Ints(tts)
    
  451. 	for _, ti := range tts {
    
  452. 		tt := chroma.TokenType(ti)
    
  453. 		switch tt {
    
  454. 		case chroma.Background, chroma.PreWrapper:
    
  455. 			continue
    
  456. 		}
    
  457. 		class := f.class(tt)
    
  458. 		if class == "" {
    
  459. 			continue
    
  460. 		}
    
  461. 		styles := css[tt]
    
  462. 		if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s { %s }\n", tt, f.prefix, class, styles); err != nil {
    
  463. 			return err
    
  464. 		}
    
  465. 	}
    
  466. 	return nil
    
  467. }
    
  468. 
    
  469. func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
    
  470. 	classes := map[chroma.TokenType]string{}
    
  471. 	bg := style.Get(chroma.Background)
    
  472. 	// Convert the style.
    
  473. 	for t := range chroma.StandardTypes {
    
  474. 		entry := style.Get(t)
    
  475. 		if t != chroma.Background {
    
  476. 			entry = entry.Sub(bg)
    
  477. 		}
    
  478. 
    
  479. 		// Inherit from custom CSS provided by user
    
  480. 		tokenCategory := t.Category()
    
  481. 		tokenSubCategory := t.SubCategory()
    
  482. 		if t != tokenCategory {
    
  483. 			if css, ok := f.customCSS[tokenCategory]; ok {
    
  484. 				classes[t] = css
    
  485. 			}
    
  486. 		}
    
  487. 		if tokenCategory != tokenSubCategory {
    
  488. 			if css, ok := f.customCSS[tokenSubCategory]; ok {
    
  489. 				classes[t] += css
    
  490. 			}
    
  491. 		}
    
  492. 		// Add custom CSS provided by user
    
  493. 		if css, ok := f.customCSS[t]; ok {
    
  494. 			classes[t] += css
    
  495. 		}
    
  496. 
    
  497. 		if !f.allClasses && entry.IsZero() && classes[t] == `` {
    
  498. 			continue
    
  499. 		}
    
  500. 
    
  501. 		styleEntryCSS := StyleEntryToCSS(entry)
    
  502. 		if styleEntryCSS != `` && classes[t] != `` {
    
  503. 			styleEntryCSS += `;`
    
  504. 		}
    
  505. 		classes[t] = styleEntryCSS + classes[t]
    
  506. 	}
    
  507. 	classes[chroma.Background] += `;` + f.tabWidthStyle()
    
  508. 	classes[chroma.PreWrapper] += classes[chroma.Background]
    
  509. 	// Make PreWrapper a grid to show highlight style with full width.
    
  510. 	if len(f.highlightRanges) > 0 && f.customCSS[chroma.PreWrapper] == `` {
    
  511. 		classes[chroma.PreWrapper] += `display: grid;`
    
  512. 	}
    
  513. 	// Make PreWrapper wrap long lines.
    
  514. 	if f.wrapLongLines {
    
  515. 		classes[chroma.PreWrapper] += `white-space: pre-wrap; word-break: break-word;`
    
  516. 	}
    
  517. 	lineNumbersStyle := `white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;`
    
  518. 	// All rules begin with default rules followed by user provided rules
    
  519. 	classes[chroma.Line] = `display: flex;` + classes[chroma.Line]
    
  520. 	classes[chroma.LineNumbers] = lineNumbersStyle + classes[chroma.LineNumbers]
    
  521. 	classes[chroma.LineNumbersTable] = lineNumbersStyle + classes[chroma.LineNumbersTable]
    
  522. 	classes[chroma.LineTable] = "border-spacing: 0; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTable]
    
  523. 	classes[chroma.LineTableTD] = "vertical-align: top; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTableTD]
    
  524. 	classes[chroma.LineLink] = "outline: none; text-decoration: none; color: inherit" + classes[chroma.LineLink]
    
  525. 	return classes
    
  526. }
    
  527. 
    
  528. // StyleEntryToCSS converts a chroma.StyleEntry to CSS attributes.
    
  529. func StyleEntryToCSS(e chroma.StyleEntry) string {
    
  530. 	styles := []string{}
    
  531. 	if e.Colour.IsSet() {
    
  532. 		styles = append(styles, "color: "+e.Colour.String())
    
  533. 	}
    
  534. 	if e.Background.IsSet() {
    
  535. 		styles = append(styles, "background-color: "+e.Background.String())
    
  536. 	}
    
  537. 	if e.Bold == chroma.Yes {
    
  538. 		styles = append(styles, "font-weight: bold")
    
  539. 	}
    
  540. 	if e.Italic == chroma.Yes {
    
  541. 		styles = append(styles, "font-style: italic")
    
  542. 	}
    
  543. 	if e.Underline == chroma.Yes {
    
  544. 		styles = append(styles, "text-decoration: underline")
    
  545. 	}
    
  546. 	return strings.Join(styles, "; ")
    
  547. }
    
  548. 
    
  549. // Compress CSS attributes - remove spaces, transform 6-digit colours to 3.
    
  550. func compressStyle(s string) string {
    
  551. 	parts := strings.Split(s, ";")
    
  552. 	out := []string{}
    
  553. 	for _, p := range parts {
    
  554. 		p = strings.Join(strings.Fields(p), " ")
    
  555. 		p = strings.Replace(p, ": ", ":", 1)
    
  556. 		if strings.Contains(p, "#") {
    
  557. 			c := p[len(p)-6:]
    
  558. 			if c[0] == c[1] && c[2] == c[3] && c[4] == c[5] {
    
  559. 				p = p[:len(p)-6] + c[0:1] + c[2:3] + c[4:5]
    
  560. 			}
    
  561. 		}
    
  562. 		out = append(out, p)
    
  563. 	}
    
  564. 	return strings.Join(out, ";")
    
  565. }