@@ -54,9 +54,10 @@ import {
5454 emoji ,
5555 emoticon ,
5656 emojiUnicode ,
57+ color ,
5758} from './utils' ;
5859import { Scanner } from './scanner' ;
59- import { isNewline , isPlainChar , isSpace , isAlpha , isAlphaNum , isDigit , EMOTICON_KEYS , EMOTICONS } from './chars' ;
60+ import { isNewline , isPlainChar , isSpace , isAlpha , isAlphaNum , isDigit , EMOTICON_KEYS , EMOTICONS , isHexDigit } from './chars' ;
6061
6162// ─── Constants ────────────────────────────────────────────────────────────────
6263
@@ -93,6 +94,17 @@ export function matchEmoticon(scanner: Scanner): Inlines | null {
9394 return null ;
9495}
9596
97+ function isAnyText ( ch : string ) : boolean {
98+ if ( ch === '' ) return false ;
99+ return (
100+ ( ch >= ' ' && ch <= "'" ) || // space ! " # $ % & '
101+ ( ch >= '+' && ch <= '@' ) || // + , - . / 0-9 : ; < = > ? @
102+ ( ch >= 'A' && ch <= 'Z' ) || // A-Z
103+ ( ch >= 'a' && ch <= 'z' ) || // a-z
104+ ch . charCodeAt ( 0 ) > 127 // any non-ASCII character
105+ ) ;
106+ }
107+
96108// ─── Re-entrancy guards ───────────────────────────────────────────────────
97109
98110let skipBold = false ;
@@ -346,6 +358,16 @@ function parseInline(scanner: Scanner, options: Options): Inlines[] {
346358 }
347359 }
348360
361+ // Color (color:#rgb / rgba / rrggbb / rrggbbaa) — only when enabled
362+ if ( scanner . matches ( 'color:#' ) ) {
363+ const result = tryColor ( scanner , options ) ;
364+ if ( result !== null ) {
365+ nodes . push ( result ) ;
366+ prevChar = '' ;
367+ continue ;
368+ }
369+ }
370+
349371 // Email (local@domain)
350372 if ( isAlpha ( ch ) || isDigit ( ch ) || ch . charCodeAt ( 0 ) > 127 ) {
351373 const result = tryEmail ( scanner ) ;
@@ -576,6 +598,16 @@ function parseInlineContent(scanner: Scanner, options: Options, stopChar: string
576598 }
577599 }
578600
601+ // Color (color:#rgb / rgba / rrggbb / rrggbbaa)
602+ if ( scanner . matches ( 'color:#' ) ) {
603+ const result = tryColor ( scanner , options ) ;
604+ if ( result !== null ) {
605+ nodes . push ( result ) ;
606+ prevChar = '' ;
607+ continue ;
608+ }
609+ }
610+
579611 // Email (local@domain)
580612 if ( isAlpha ( ch ) || isDigit ( ch ) || ch . charCodeAt ( 0 ) > 127 ) {
581613 const result = tryEmail ( scanner ) ;
@@ -1521,6 +1553,44 @@ function tryUnicodeEmoji(scanner: Scanner): Inlines | null {
15211553 return emojiUnicode ( m [ 0 ] ) ;
15221554}
15231555
1556+ function tryColor ( scanner : Scanner , options : Options ) : Inlines | null {
1557+ if ( ! options . colors ) return null ;
1558+ const delimiter = 'color:#' ;
1559+
1560+ if ( ! scanner . matches ( delimiter ) ) return null ;
1561+
1562+ const startPos = scanner . position ( ) ;
1563+ scanner . consume ( delimiter . length ) ; // consume "color:#"
1564+
1565+ const hexStart = scanner . position ( ) ;
1566+ while ( ! scanner . isEnd ( ) && isHexDigit ( scanner . char ( ) ) ) {
1567+ scanner . consume ( ) ;
1568+ }
1569+ const hex = scanner . sliceFrom ( hexStart ) ;
1570+
1571+ let rgba : [ number , number , number , number ] | null = null ;
1572+
1573+ if ( hex . length === 6 || hex . length === 8 ) {
1574+ // byte pairs: c7 -> 0xc7
1575+ const b : number [ ] = [ ] ;
1576+ for ( let i = 0 ; i < hex . length ; i += 2 ) b . push ( parseInt ( hex . slice ( i , i + 2 ) , 16 ) ) ;
1577+ rgba = [ b [ 0 ] , b [ 1 ] , b [ 2 ] , b [ 3 ] ?? 255 ] ;
1578+ } else if ( hex . length === 3 || hex . length === 4 ) {
1579+ // single nibbles doubled: c -> cc -> 0xcc
1580+ const n : number [ ] = [ ] ;
1581+ for ( let i = 0 ; i < hex . length ; i ++ ) n . push ( parseInt ( hex [ i ] + hex [ i ] , 16 ) ) ;
1582+ rgba = [ n [ 0 ] , n [ 1 ] , n [ 2 ] , n [ 3 ] ?? 255 ] ;
1583+ }
1584+
1585+ // Invalid digit count, or color is immediately followed by text -> not a color.
1586+ if ( rgba === null || isAnyText ( scanner . char ( ) ) ) {
1587+ scanner . backtrack ( startPos ) ;
1588+ return null ;
1589+ }
1590+
1591+ return color ( rgba [ 0 ] , rgba [ 1 ] , rgba [ 2 ] , rgba [ 3 ] ) ;
1592+ }
1593+
15241594// ─── Block methods ──────────────────────────────────────────────────────────────
15251595
15261596function tryCodeFence ( scanner : Scanner ) : Code | null {
0 commit comments