css-font-property-parser.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /*
  2. * The MIT License (MIT)
  3. *
  4. * Author: Gildas Lormeau
  5. *
  6. * Permission is hereby granted, free of charge, to any person obtaining a copy
  7. * of this software and associated documentation files (the "Software"), to deal
  8. * in the Software without restriction, including without limitation the rights
  9. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. * copies of the Software, and to permit persons to whom the Software is
  11. * furnished to do so, subject to the following conditions:
  12. * The above copyright notice and this permission notice shall be included in all
  13. * copies or substantial portions of the Software.
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. // derived from https://github.com/jedmao/parse-css-font/
  23. /*
  24. * The MIT License (MIT)
  25. *
  26. * Copyright (c) 2015 Jed Mao
  27. *
  28. * Permission is hereby granted, free of charge, to any person obtaining a copy
  29. * of this software and associated documentation files (the "Software"), to deal
  30. * in the Software without restriction, including without limitation the rights
  31. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  32. * copies of the Software, and to permit persons to whom the Software is
  33. * furnished to do so, subject to the following conditions:
  34. * The above copyright notice and this permission notice shall be included in all
  35. * copies or substantial portions of the Software.
  36. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  37. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  38. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  39. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  40. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  41. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  42. * SOFTWARE.
  43. */
  44. const REGEXP_SIMPLE_QUOTES_STRING = /^'(.*?)'$/;
  45. const REGEXP_DOUBLE_QUOTES_STRING = /^"(.*?)"$/;
  46. const globalKeywords = [
  47. "inherit",
  48. "initial",
  49. "unset"
  50. ];
  51. const systemFontKeywords = [
  52. "caption",
  53. "icon",
  54. "menu",
  55. "message-box",
  56. "small-caption",
  57. "status-bar"
  58. ];
  59. const fontWeightKeywords = [
  60. "normal",
  61. "bold",
  62. "bolder",
  63. "lighter",
  64. "100",
  65. "200",
  66. "300",
  67. "400",
  68. "500",
  69. "600",
  70. "700",
  71. "800",
  72. "900"
  73. ];
  74. const fontStyleKeywords = [
  75. "normal",
  76. "italic",
  77. "oblique"
  78. ];
  79. const fontStretchKeywords = [
  80. "normal",
  81. "condensed",
  82. "semi-condensed",
  83. "extra-condensed",
  84. "ultra-condensed",
  85. "expanded",
  86. "semi-expanded",
  87. "extra-expanded",
  88. "ultra-expanded"
  89. ];
  90. const cssFontSizeKeywords = [
  91. "xx-small",
  92. "x-small",
  93. "small",
  94. "medium",
  95. "large",
  96. "x-large",
  97. "xx-large",
  98. "larger",
  99. "smaller"
  100. ];
  101. const cssListHelpers = {
  102. splitBySpaces,
  103. split,
  104. splitByCommas
  105. };
  106. const helpers = {
  107. isSize
  108. };
  109. const errorPrefix = "[parse-css-font] ";
  110. export {
  111. parse
  112. };
  113. function parse(value) {
  114. if (typeof value !== "string") {
  115. throw new TypeError(errorPrefix + "Expected a string.");
  116. }
  117. if (value === "") {
  118. throw error("Cannot parse an empty string.");
  119. }
  120. if (systemFontKeywords.indexOf(value) !== -1) {
  121. return { system: value };
  122. }
  123. const font = {
  124. lineHeight: "normal",
  125. stretch: "normal",
  126. style: "normal",
  127. variant: "normal",
  128. weight: "normal",
  129. };
  130. let isLocked = false;
  131. const tokens = cssListHelpers.splitBySpaces(value);
  132. let token = tokens.shift();
  133. for (; token; token = tokens.shift()) {
  134. if (token === "normal" || globalKeywords.indexOf(token) !== -1) {
  135. ["style", "variant", "weight", "stretch"].forEach((prop) => {
  136. font[prop] = token;
  137. });
  138. isLocked = true;
  139. continue;
  140. }
  141. if (fontWeightKeywords.indexOf(token) !== -1) {
  142. if (isLocked) {
  143. continue;
  144. }
  145. font.weight = token;
  146. continue;
  147. }
  148. if (fontStyleKeywords.indexOf(token) !== -1) {
  149. if (isLocked) {
  150. continue;
  151. }
  152. font.style = token;
  153. continue;
  154. }
  155. if (fontStretchKeywords.indexOf(token) !== -1) {
  156. if (isLocked) {
  157. continue;
  158. }
  159. font.stretch = token;
  160. continue;
  161. }
  162. if (helpers.isSize(token)) {
  163. const parts = cssListHelpers.split(token, ["/"]);
  164. font.size = parts[0];
  165. if (parts[1]) {
  166. font.lineHeight = parseLineHeight(parts[1]);
  167. } else if (tokens[0] === "/") {
  168. tokens.shift();
  169. font.lineHeight = parseLineHeight(tokens.shift());
  170. }
  171. if (!tokens.length) {
  172. throw error("Missing required font-family.");
  173. }
  174. font.family = cssListHelpers.splitByCommas(tokens.join(" ")).map(removeQuotes);
  175. return font;
  176. }
  177. if (font.variant !== "normal") {
  178. throw error("Unknown or unsupported font token: " + font.variant);
  179. }
  180. if (isLocked) {
  181. continue;
  182. }
  183. font.variant = token;
  184. }
  185. throw error("Missing required font-size.");
  186. }
  187. function error(message) {
  188. return new Error(errorPrefix + message);
  189. }
  190. function parseLineHeight(value) {
  191. const parsed = parseFloat(value);
  192. if (parsed.toString() === value) {
  193. return parsed;
  194. }
  195. return value;
  196. }
  197. /**
  198. * Splits a CSS declaration value (shorthand) using provided separators
  199. * as the delimiters.
  200. */
  201. function split(
  202. /**
  203. * A CSS declaration value (shorthand).
  204. */
  205. value,
  206. /**
  207. * Any number of separator characters used for splitting.
  208. */
  209. separators,
  210. {
  211. last = false,
  212. } = {},
  213. ) {
  214. if (typeof value !== "string") {
  215. throw new TypeError("expected a string");
  216. }
  217. if (!Array.isArray(separators)) {
  218. throw new TypeError("expected a string array of separators");
  219. }
  220. if (typeof last !== "boolean") {
  221. throw new TypeError("expected a Boolean value for options.last");
  222. }
  223. const array = [];
  224. let current = "";
  225. let splitMe = false;
  226. let func = 0;
  227. let quote = false;
  228. let escape = false;
  229. for (const char of value) {
  230. if (quote) {
  231. if (escape) {
  232. escape = false;
  233. } else if (char === "\\") {
  234. escape = true;
  235. } else if (char === quote) {
  236. quote = false;
  237. }
  238. } else if (char === "\"" || char === "'") {
  239. quote = char;
  240. } else if (char === "(") {
  241. func += 1;
  242. } else if (char === ")") {
  243. if (func > 0) {
  244. func -= 1;
  245. }
  246. } else if (func === 0) {
  247. if (separators.indexOf(char) !== -1) {
  248. splitMe = true;
  249. }
  250. }
  251. if (splitMe) {
  252. if (current !== "") {
  253. array.push(current.trim());
  254. }
  255. current = "";
  256. splitMe = false;
  257. } else {
  258. current += char;
  259. }
  260. }
  261. if (last || current !== "") {
  262. array.push(current.trim());
  263. }
  264. return array;
  265. }
  266. /**
  267. * Splits a CSS declaration value (shorthand) using whitespace characters
  268. * as the delimiters.
  269. */
  270. function splitBySpaces(
  271. /**
  272. * A CSS declaration value (shorthand).
  273. */
  274. value,
  275. ) {
  276. const spaces = [" ", "\n", "\t"];
  277. return split(value, spaces);
  278. }
  279. /**
  280. * Splits a CSS declaration value (shorthand) using commas as the delimiters.
  281. */
  282. function splitByCommas(
  283. /**
  284. * A CSS declaration value (shorthand).
  285. */
  286. value,
  287. ) {
  288. const comma = ",";
  289. return split(value, [comma], { last: true });
  290. }
  291. function isSize(value) {
  292. return !isNaN(parseFloat(value))
  293. || value.indexOf("/") !== -1
  294. || cssFontSizeKeywords.indexOf(value) !== -1;
  295. }
  296. function removeQuotes(string) {
  297. if (string.match(REGEXP_SIMPLE_QUOTES_STRING)) {
  298. string = string.replace(REGEXP_SIMPLE_QUOTES_STRING, "$1");
  299. } else {
  300. string = string.replace(REGEXP_DOUBLE_QUOTES_STRING, "$1");
  301. }
  302. return string.trim();
  303. }