syntax.css.pp 12 KB

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