style-utils.js (7362B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const XHTML_NS = "http://www.w3.org/1999/xhtml"; 8 const FONT_PREVIEW_TEXT = "Abc"; 9 const FONT_PREVIEW_FONT_SIZE = 40; 10 const FONT_PREVIEW_FILLSTYLE = "black"; 11 const FONT_PREVIEW_FONT_FALLBACK = "serif"; 12 // Offset (in px) to avoid cutting off text edges of italic fonts. 13 const FONT_PREVIEW_OFFSET = 4; 14 // Factor used to resize the canvas in order to get better text quality. 15 const FONT_PREVIEW_OVERSAMPLING_FACTOR = 2; 16 const FONT_NEED_WRAPPING_QUOTES_REGEX = /^[^'"].* /; 17 18 /** 19 * Helper function for getting an image preview of the given font. 20 * 21 * @param font {string} 22 * Name of font to preview 23 * @param doc {Document} 24 * Document to use to render font 25 * @param options {object} 26 * Object with options 'previewText' and 'previewFontSize' 27 * 28 * @return {object} An object with the following properties: 29 * - dataUrl {string}: The data URI of the font preview image 30 * - size {Number}: The optimal width of preview image 31 * - ctx {CanvasRenderingContext2D}: The canvas context (returned for tests) 32 */ 33 function getFontPreviewData(font, doc, options) { 34 options = options || {}; 35 const previewText = options.previewText || FONT_PREVIEW_TEXT; 36 const previewTextLines = previewText.split("\n"); 37 const previewFontSize = options.previewFontSize || FONT_PREVIEW_FONT_SIZE; 38 const fillStyle = options.fillStyle || FONT_PREVIEW_FILLSTYLE; 39 const fontStyle = options.fontStyle || ""; 40 const fontWeight = options.fontWeight || ""; 41 42 const canvas = doc.createElementNS(XHTML_NS, "canvas"); 43 const ctx = canvas.getContext("2d"); 44 45 // We want to wrap some font in quotes so font family like `Font Awesome 5 Brands` are 46 // properly applied, but we don't want to wrap all fonts, otherwise generic family names 47 // (e.g. `monospace`) wouldn't work. 48 // It should be safe to only add the quotes when the font has some spaces (generic family 49 // names don't have spaces, https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-family#generic-name) 50 // We also don't want to add quotes if there are already some 51 // `font` is the declaration value, so it can have multiple parts, 52 // e.g: `"Menlo", MonoLisa, monospace` 53 const fontParts = []; 54 // We could use the parser to properly handle complex values, for example css variable, 55 // but ideally this function would only receive computed values (see Bug 1952821). 56 // If we'd get `var(--x)` here, we'd have to resolve it somehow, so it'd be simpler to 57 // get the computed value directly. 58 for (let f of font.split(",")) { 59 if (FONT_NEED_WRAPPING_QUOTES_REGEX.test(f.trim())) { 60 f = `"${f}"`; 61 } 62 fontParts.push(f); 63 } 64 // Add a fallback value 65 fontParts.push(FONT_PREVIEW_FONT_FALLBACK); 66 67 // Apply individual font properties to the canvas element so we can easily compute the 68 // canvas context font 69 // First, we need to start with a default shorthand to make it work 70 canvas.style.font = `${FONT_PREVIEW_FONT_SIZE}px ${FONT_PREVIEW_FONT_FALLBACK}`; 71 // Then we can set the different properties 72 canvas.style.fontFamily = fontParts.join(", "); 73 canvas.style.fontSize = `${previewFontSize}px`; 74 if (fontWeight) { 75 canvas.style.fontWeight = fontWeight; 76 } 77 if (fontStyle) { 78 canvas.style.fontStyle = fontStyle; 79 } 80 81 const fontValue = canvas.style.font; 82 83 // Get the correct preview text measurements and set the canvas dimensions 84 ctx.font = fontValue; 85 ctx.fillStyle = fillStyle; 86 const previewTextLinesWidths = previewTextLines.map( 87 previewTextLine => ctx.measureText(previewTextLine).width 88 ); 89 const textWidth = Math.round(Math.max(...previewTextLinesWidths)); 90 91 // The canvas width is calculated as the width of the longest line plus 92 // an offset at the left and right of it. 93 // The canvas height is calculated as the font size multiplied by the 94 // number of lines plus an offset at the top and bottom. 95 // 96 // In order to get better text quality, we oversample the canvas. 97 // That means, after the width and height are calculated, we increase 98 // both sizes by some factor. 99 const simpleCanvasWidth = textWidth + FONT_PREVIEW_OFFSET * 2; 100 canvas.width = simpleCanvasWidth * FONT_PREVIEW_OVERSAMPLING_FACTOR; 101 canvas.height = 102 (previewFontSize * previewTextLines.length + FONT_PREVIEW_OFFSET * 2) * 103 FONT_PREVIEW_OVERSAMPLING_FACTOR; 104 105 // we have to reset these after changing the canvas size 106 ctx.font = fontValue; 107 ctx.fillStyle = fillStyle; 108 109 // Oversample the canvas for better text quality 110 ctx.scale(FONT_PREVIEW_OVERSAMPLING_FACTOR, FONT_PREVIEW_OVERSAMPLING_FACTOR); 111 112 ctx.textBaseline = "top"; 113 ctx.textAlign = "center"; 114 const horizontalTextPosition = simpleCanvasWidth / 2; 115 let verticalTextPosition = FONT_PREVIEW_OFFSET; 116 for (let i = 0; i < previewTextLines.length; i++) { 117 ctx.fillText( 118 previewTextLines[i], 119 horizontalTextPosition, 120 verticalTextPosition 121 ); 122 123 // Move vertical text position one line down 124 verticalTextPosition += previewFontSize; 125 } 126 127 const dataURL = canvas.toDataURL("image/png"); 128 129 return { 130 dataURL, 131 size: textWidth + FONT_PREVIEW_OFFSET * 2, 132 ctx, 133 }; 134 } 135 136 exports.getFontPreviewData = getFontPreviewData; 137 138 /** 139 * Get the text content of a rule given some CSS text, a line and a column 140 * Consider the following example: 141 * 142 * ```css 143 * body { 144 * color: red; 145 * } 146 * p { 147 * line-height: 2em; 148 * color: blue; 149 * } 150 * ``` 151 * 152 * Calling the function with the whole text above and `line=4` and `column=1` would 153 * return `line-height: 2em; color: blue;` 154 * 155 * @param {string} initialText 156 * @param {number} line (1-indexed) 157 * @param {number} column (1-indexed) 158 * @return {object} An object of the form {offset: number, text: string} 159 * The offset is the index into the input string where 160 * the rule text started. The text is the content of 161 * the rule. 162 */ 163 function getRuleText(initialText, line, column) { 164 if (typeof line === "undefined" || typeof column === "undefined") { 165 throw new Error("Location information is missing"); 166 } 167 168 const { text } = getTextAtLineColumn(initialText, line, column); 169 const res = InspectorUtils.getRuleBodyText(text); 170 if (res === null || typeof res === "undefined") { 171 throw new Error("Couldn't find rule"); 172 } 173 return res; 174 } 175 176 exports.getRuleText = getRuleText; 177 178 /** 179 * Return the offset and substring of |text| that starts at the given 180 * line and column. 181 * 182 * @param {string} text 183 * @param {number} line (1-indexed) 184 * @param {number} column (1-indexed) 185 * @return {object} An object of the form {offset: number, text: string}, 186 * where the offset is the offset into the input string 187 * where the text starts, and where text is the text. 188 */ 189 function getTextAtLineColumn(text, line, column) { 190 let offset; 191 if (line > 1) { 192 const rx = new RegExp( 193 "(?:[^\\r\\n\\f]*(?:\\r\\n|\\n|\\r|\\f)){" + (line - 1) + "}" 194 ); 195 offset = rx.exec(text)[0].length; 196 } else { 197 offset = 0; 198 } 199 offset += column - 1; 200 return { offset, text: text.substr(offset) }; 201 } 202 203 exports.getTextAtLineColumn = getTextAtLineColumn;