syntax.css.pp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. {
  2. This file is part of the Free Component Library (FCL)
  3. Copyright (c) 2025 by Michael Van Canneyt
  4. CSS syntax highlighter
  5. See the file COPYING.FPC, included in this distribution,
  6. for details about the copyright.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  10. **********************************************************************}
  11. {$MODE objfpc}
  12. {$H+}
  13. unit syntax.css;
  14. interface
  15. uses
  16. {$IFDEF FPC_DOTTEDUNITS}
  17. System.Types, System.SysUtils, syntax.highlighter;
  18. {$ELSE}
  19. Types, SysUtils, syntax.highlighter;
  20. {$ENDIF}
  21. type
  22. { TCssSyntaxHighlighter }
  23. TCssSyntaxHighlighter = class(TSyntaxHighlighter)
  24. private
  25. FSource: string;
  26. FPos: integer;
  27. protected
  28. procedure ProcessSingleQuoteString(var endPos: integer);
  29. procedure ProcessDoubleQuoteString(var endPos: integer);
  30. procedure ProcessMultiLineComment(var endPos: integer);
  31. procedure ProcessSelector(var endPos: integer);
  32. procedure ProcessProperty(var endPos: integer);
  33. procedure ProcessColor(var endPos: integer);
  34. function CheckForAtRule(var endPos: integer): boolean;
  35. function CheckForProperty(var endPos: integer): boolean;
  36. procedure ProcessNumber(var endPos: integer);
  37. procedure ProcessUrl(var endPos: integer);
  38. function IsWordChar(ch: char): boolean;
  39. function IsHexChar(ch: char): boolean;
  40. class procedure CheckCategories;
  41. class procedure RegisterDefaultCategories; override;
  42. class function GetLanguages : TStringDynarray; override;
  43. public
  44. constructor Create; override;
  45. class var
  46. CategoryCSS : Integer;
  47. CategoryEmbeddedCSS : Integer;
  48. function Execute(const Source: string): TSyntaxTokenArray; override;
  49. end;
  50. const
  51. MaxKeywordLength = 20;
  52. MaxKeyword = 41;
  53. CssAtRuleTable: array[0..MaxKeyword] of string = (
  54. '@charset', '@import', '@namespace', '@media', '@supports', '@page', '@font-face',
  55. '@keyframes', '@webkit-keyframes', '@moz-keyframes', '@ms-keyframes', '@o-keyframes',
  56. '@document', '@font-feature-values', '@viewport', '@counter-style', '@property',
  57. '@layer', '@container', '@scope', '@starting-style', '@position-try',
  58. 'animation', 'background', 'border', 'color', 'display', 'font', 'height',
  59. 'margin', 'padding', 'position', 'width', 'flex', 'grid', 'transform',
  60. 'transition', 'opacity', 'z-index', 'top', 'right', 'bottom'
  61. );
  62. function DoCssHighlighting(const Source: string): TSyntaxTokenArray;
  63. implementation
  64. { TCssSyntaxHighlighter }
  65. procedure TCssSyntaxHighlighter.ProcessSingleQuoteString(var endPos: integer);
  66. var
  67. startPos: integer;
  68. begin
  69. startPos := FPos;
  70. Inc(FPos); // Skip opening quote
  71. while FPos <= Length(FSource) do
  72. begin
  73. if FSource[FPos] = '''' then
  74. begin
  75. Inc(FPos);
  76. break;
  77. end
  78. else if FSource[FPos] = '\' then
  79. begin
  80. if FPos < Length(FSource) then Inc(FPos); // Skip escaped character
  81. end;
  82. Inc(FPos);
  83. end;
  84. endPos := FPos - 1;
  85. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shStrings);
  86. end;
  87. procedure TCssSyntaxHighlighter.ProcessDoubleQuoteString(var endPos: integer);
  88. var
  89. startPos: integer;
  90. begin
  91. startPos := FPos;
  92. Inc(FPos); // Skip opening quote
  93. while FPos <= Length(FSource) do
  94. begin
  95. if FSource[FPos] = '"' then
  96. begin
  97. Inc(FPos);
  98. break;
  99. end
  100. else if FSource[FPos] = '\' then
  101. begin
  102. if FPos < Length(FSource) then
  103. Inc(FPos); // Skip escaped character
  104. end;
  105. Inc(FPos);
  106. end;
  107. endPos := FPos - 1;
  108. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shStrings);
  109. end;
  110. procedure TCssSyntaxHighlighter.ProcessMultiLineComment(var endPos: integer);
  111. var
  112. startPos: integer;
  113. begin
  114. startPos := FPos;
  115. Inc(FPos, 2); // Skip the opening /*
  116. while FPos < Length(FSource) do
  117. begin
  118. if (FSource[FPos] = '*') and (FSource[FPos + 1] = '/') then
  119. begin
  120. Inc(FPos, 2);
  121. break;
  122. end;
  123. Inc(FPos);
  124. end;
  125. endPos := FPos - 1;
  126. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shComment);
  127. end;
  128. procedure TCssSyntaxHighlighter.ProcessSelector(var endPos: integer);
  129. var
  130. startPos: integer;
  131. begin
  132. startPos := FPos;
  133. // Handle class selectors (.class)
  134. if FSource[FPos] = '.' then
  135. begin
  136. Inc(FPos);
  137. while (FPos <= Length(FSource)) and IsWordChar(FSource[FPos]) do
  138. Inc(FPos);
  139. end
  140. // Handle ID selectors (#id)
  141. else if FSource[FPos] = '#' then
  142. begin
  143. Inc(FPos);
  144. while (FPos <= Length(FSource)) and IsWordChar(FSource[FPos]) do
  145. Inc(FPos);
  146. end
  147. // Handle attribute selectors ([attr])
  148. else if FSource[FPos] = '[' then
  149. begin
  150. Inc(FPos);
  151. while (FPos <= Length(FSource)) and (FSource[FPos] <> ']') do
  152. Inc(FPos);
  153. if (FPos <= Length(FSource)) and (FSource[FPos] = ']') then
  154. Inc(FPos);
  155. end
  156. // Handle pseudo-selectors (:hover, ::before)
  157. else if FSource[FPos] = ':' then
  158. begin
  159. Inc(FPos);
  160. if (FPos <= Length(FSource)) and (FSource[FPos] = ':') then
  161. Inc(FPos); // Handle ::
  162. while (FPos <= Length(FSource)) and IsWordChar(FSource[FPos]) do
  163. Inc(FPos);
  164. end
  165. // Handle element selectors
  166. else
  167. begin
  168. while (FPos <= Length(FSource)) and IsWordChar(FSource[FPos]) do
  169. Inc(FPos);
  170. end;
  171. endPos := FPos - 1;
  172. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shDefault);
  173. end;
  174. procedure TCssSyntaxHighlighter.ProcessProperty(var endPos: integer);
  175. var
  176. startPos: integer;
  177. begin
  178. startPos := FPos;
  179. while (FPos <= Length(FSource)) and (FSource[FPos] in ['a'..'z', 'A'..'Z', '0'..'9', '-', '_']) do
  180. Inc(FPos);
  181. endPos := FPos - 1;
  182. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shKeyword);
  183. end;
  184. procedure TCssSyntaxHighlighter.ProcessColor(var endPos: integer);
  185. var
  186. startPos: integer;
  187. digitCount: integer;
  188. begin
  189. startPos := FPos;
  190. Inc(FPos); // Skip #
  191. digitCount := 0;
  192. while (FPos <= Length(FSource)) and IsHexChar(FSource[FPos]) and (digitCount < 8) do
  193. begin
  194. Inc(FPos);
  195. Inc(digitCount);
  196. end;
  197. // Valid hex colors are 3, 4, 6, or 8 digits
  198. if digitCount in [3, 4, 6, 8] then
  199. begin
  200. endPos := FPos - 1;
  201. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shNumbers);
  202. end
  203. else
  204. begin
  205. // Not a valid color, treat as selector
  206. FPos := startPos;
  207. ProcessSelector(endPos);
  208. end;
  209. end;
  210. function TCssSyntaxHighlighter.CheckForAtRule(var endPos: integer): boolean;
  211. var
  212. i, j: integer;
  213. atRule: string;
  214. begin
  215. Result := False;
  216. if FSource[FPos] <> '@' then Exit;
  217. i := 0;
  218. while (FPos + i <= Length(FSource)) and (i < MaxKeywordLength) and
  219. (FSource[FPos + i] in ['@', 'a'..'z', 'A'..'Z', '0'..'9', '-', '_']) do
  220. begin
  221. Inc(i);
  222. end;
  223. atRule := Copy(FSource, FPos, i);
  224. for j := 0 to 21 do // Only check @-rules (first 22 entries)
  225. if CssAtRuleTable[j] = atRule then
  226. begin
  227. Result := True;
  228. break;
  229. end;
  230. if Result then
  231. begin
  232. Inc(FPos, i);
  233. endPos := FPos - 1;
  234. AddToken(atRule, shDirective);
  235. end;
  236. end;
  237. function TCssSyntaxHighlighter.CheckForProperty(var endPos: integer): boolean;
  238. var
  239. i, j: integer;
  240. prop: string;
  241. begin
  242. Result := False;
  243. i := 0;
  244. while (FPos + i <= Length(FSource)) and (i < MaxKeywordLength) and
  245. (FSource[FPos + i] in ['a'..'z', 'A'..'Z', '0'..'9', '-', '_']) do
  246. begin
  247. Inc(i);
  248. end;
  249. prop := Copy(FSource, FPos, i);
  250. for j := 22 to MaxKeyword do // Check properties (from index 22 onwards)
  251. if CssAtRuleTable[j] = prop then
  252. begin
  253. Result := True;
  254. break;
  255. end;
  256. if Result then
  257. begin
  258. Inc(FPos, i);
  259. endPos := FPos - 1;
  260. AddToken(prop, shKeyword);
  261. end;
  262. end;
  263. procedure TCssSyntaxHighlighter.ProcessNumber(var endPos: integer);
  264. var
  265. startPos: integer;
  266. begin
  267. startPos := FPos;
  268. // Handle numbers (including decimals)
  269. while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
  270. Inc(FPos);
  271. // Handle decimal point
  272. if (FPos <= Length(FSource)) and (FSource[FPos] = '.') then
  273. begin
  274. Inc(FPos);
  275. while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
  276. Inc(FPos);
  277. end;
  278. // Handle CSS units (px, em, rem, %, etc.)
  279. if (FPos <= Length(FSource)) and (FSource[FPos] in ['a'..'z', 'A'..'Z', '%']) then
  280. begin
  281. while (FPos <= Length(FSource)) and (FSource[FPos] in ['a'..'z', 'A'..'Z', '%']) do
  282. Inc(FPos);
  283. end;
  284. endPos := FPos - 1;
  285. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shNumbers);
  286. end;
  287. procedure TCssSyntaxHighlighter.ProcessUrl(var endPos: integer);
  288. var
  289. startPos: integer;
  290. parenCount: integer;
  291. begin
  292. startPos := FPos;
  293. Inc(FPos, 4); // Skip 'url('
  294. parenCount := 1;
  295. while (FPos <= Length(FSource)) and (parenCount > 0) do
  296. begin
  297. if FSource[FPos] = '(' then
  298. Inc(parenCount)
  299. else if FSource[FPos] = ')' then
  300. Dec(parenCount);
  301. Inc(FPos);
  302. end;
  303. endPos := FPos - 1;
  304. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shStrings);
  305. end;
  306. function TCssSyntaxHighlighter.IsWordChar(ch: char): boolean;
  307. begin
  308. Result := ch in ['a'..'z', 'A'..'Z', '0'..'9', '_', '-'];
  309. end;
  310. function TCssSyntaxHighlighter.IsHexChar(ch: char): boolean;
  311. begin
  312. Result := ch in ['0'..'9', 'A'..'F', 'a'..'f'];
  313. end;
  314. class procedure TCssSyntaxHighlighter.CheckCategories;
  315. begin
  316. if CategoryCSS=0 then
  317. RegisterDefaultCategories;
  318. end;
  319. class procedure TCssSyntaxHighlighter.RegisterDefaultCategories;
  320. begin
  321. CategoryCSS:=RegisterCategory('CSS');
  322. CategoryEmbeddedCSS:=RegisterCategory('EmbeddedCSS');
  323. end;
  324. class function TCssSyntaxHighlighter.GetLanguages: TStringDynarray;
  325. begin
  326. Result:=['css']
  327. end;
  328. constructor TCssSyntaxHighlighter.Create;
  329. begin
  330. inherited Create;
  331. CheckCategories;
  332. DefaultCategory:=CategoryCSS;
  333. end;
  334. function TCssSyntaxHighlighter.Execute(const Source: string): TSyntaxTokenArray;
  335. var
  336. lLen, endPos, startPos: integer;
  337. ch: char;
  338. begin
  339. Result:=Nil;
  340. CheckCategories;
  341. lLen:=Length(Source);
  342. if lLen = 0 then
  343. Exit;
  344. FSource := Source;
  345. FTokens.Reset;
  346. FPos := 1;
  347. EndPos:=0;
  348. while FPos <= lLen do
  349. begin
  350. ch := FSource[FPos];
  351. case ch of
  352. '''':
  353. ProcessSingleQuoteString(endPos);
  354. '"':
  355. ProcessDoubleQuoteString(endPos);
  356. '/':
  357. begin
  358. if (FPos < Length(FSource)) and (FSource[FPos + 1] = '*') then
  359. ProcessMultiLineComment(endPos)
  360. else
  361. begin
  362. AddToken('/', shOperator);
  363. endPos := FPos;
  364. Inc(FPos);
  365. end;
  366. end;
  367. '#':
  368. begin
  369. if (FPos < Length(FSource)) and IsHexChar(FSource[FPos + 1]) then
  370. ProcessColor(endPos)
  371. else
  372. ProcessSelector(endPos);
  373. end;
  374. '@':
  375. begin
  376. if not CheckForAtRule(endPos) then
  377. begin
  378. AddToken('@', shSymbol);
  379. endPos := FPos;
  380. Inc(FPos);
  381. end;
  382. end;
  383. '0'..'9':
  384. ProcessNumber(endPos);
  385. 'a'..'t', 'v'..'z', 'A'..'Z':
  386. begin
  387. if not CheckForProperty(endPos) then
  388. begin
  389. startPos := FPos;
  390. while (FPos <= Length(FSource)) and IsWordChar(FSource[FPos]) do
  391. Inc(FPos);
  392. endPos := FPos - 1;
  393. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shDefault);
  394. end;
  395. end;
  396. 'u':
  397. begin
  398. if (FPos + 3 <= Length(FSource)) and
  399. (Copy(FSource, FPos, 4) = 'url(') then
  400. ProcessUrl(endPos)
  401. else if not CheckForProperty(endPos) then
  402. begin
  403. startPos := FPos;
  404. while (FPos <= Length(FSource)) and IsWordChar(FSource[FPos]) do
  405. Inc(FPos);
  406. endPos := FPos - 1;
  407. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shDefault);
  408. end;
  409. end;
  410. '.', ':', '[', ']': ProcessSelector(endPos);
  411. '{', '}', ';', '(', ')', ',':
  412. begin
  413. AddToken(ch, shSymbol);
  414. endPos := FPos;
  415. Inc(FPos);
  416. end;
  417. '>', '+', '~', '*', '=', '!':
  418. begin
  419. AddToken(ch, shOperator);
  420. endPos := FPos;
  421. Inc(FPos);
  422. end;
  423. ' ', #9, #10, #13:
  424. begin
  425. startPos := FPos;
  426. while (FPos <= Length(FSource)) and (FSource[FPos] in [' ', #9, #10, #13]) do
  427. Inc(FPos);
  428. endPos := FPos - 1;
  429. AddToken(Copy(FSource, startPos, endPos - startPos + 1), shDefault);
  430. end;
  431. else
  432. AddToken(ch, shInvalid);
  433. endPos := FPos;
  434. Inc(FPos);
  435. end;
  436. if FPos = endPos then Inc(FPos);
  437. end;
  438. Result := FTokens.GetTokens;
  439. end;
  440. function DoCssHighlighting(const Source: string): TSyntaxTokenArray;
  441. var
  442. highlighter: TCssSyntaxHighlighter;
  443. begin
  444. highlighter := TCssSyntaxHighlighter.Create;
  445. try
  446. Result := highlighter.Execute(Source);
  447. finally
  448. highlighter.Free;
  449. end;
  450. end;
  451. initialization
  452. TCssSyntaxHighlighter.Register;
  453. end.