tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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;