Skip to content

Commit

Permalink
improve logic for parsing font family strings
Browse files Browse the repository at this point in the history
  • Loading branch information
allansson committed Jan 13, 2021
1 parent 8204a5c commit 5e5cf96
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 4 deletions.
106 changes: 106 additions & 0 deletions src/libs/fontFace.js
Expand Up @@ -223,6 +223,15 @@ var defaultGenericFontFamilies = {
serif: "times"
};

var systemFonts = {
caption: "times",
icon: "times",
menu: "times",
"message-box": "times",
"small-caption": "times",
"status-bar": "times"
};

function ruleToString(rule) {
return [rule.stretch, rule.style, rule.weight, rule.family].join(" ");
}
Expand Down Expand Up @@ -290,3 +299,100 @@ export function resolveFontFace(fontFaceMap, rules, opts) {
export function toStyleName(font) {
return [font.weight, font.style, font.stretch].join(" ");
}

function eatWhiteSpace(input) {
return input.trimLeft();
}

function parseQuotedFontFamily(input, quote) {
var index = 0;

while (index < input.length) {
var current = input.charAt(index);

switch (current) {
case quote:
return [input.substring(0, index), input.substring(index + 1)];

// Mismatching quote
case ",":
return null;
}

index += 1;
}

// Unexpected end of input
return null;
}

function parseNonQuotedFontFamily(input) {
// It implements part of the identifier parser here: https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
//
// NOTE: This parser pretty much ignores escaped identifiers and that there is a thing called unicode.
//
// Breakdown of regexp:
// -[a-z_] - when identifier starts with a hyphen, you're not allowed to have another hyphen or a digit
// [a-z_] - allow a-z and underscore at beginning of input
// [a-z0-9_-]* - after that, anything goes
var match = input.match(/^(-[a-z_]|[a-z_])[a-z0-9_-]*/i);

// non quoted value contains illegal characters
if (match === null) {
return null;
}

return [match[0], input.substring(match[0].length)];
}

var defaultFont = ["times"];

export function parseFontFamily(input) {
var result = [];
var ch, parsed;
var remaining = input.trim();

if (remaining === "") {
return defaultFont;
}

if (remaining in systemFonts) {
return [systemFonts[remaining]];
}

while (remaining !== "") {
parsed = null;
remaining = eatWhiteSpace(remaining);
ch = remaining.charAt(0);

switch (ch) {
case '"':
case "'":
parsed = parseQuotedFontFamily(remaining.substring(1), ch);
break;

default:
parsed = parseNonQuotedFontFamily(remaining);
break;
}

if (parsed === null) {
return defaultFont;
}

result.push(parsed[0]);

remaining = eatWhiteSpace(parsed[1]);

// We expect end of input or a comma separator here
if (remaining !== "" && remaining.charAt(0) !== ",") {
return defaultFont;
}

remaining = remaining.replace(/^,/, "");
}

return result.map(function(f) {
return f.toLowerCase();
});
}
9 changes: 6 additions & 3 deletions src/modules/context2d.js
Expand Up @@ -10,7 +10,11 @@
import { jsPDF } from "../jspdf.js";
import { RGBColor } from "../libs/rgbcolor.js";
import { console } from "../libs/console.js";
import { buildFontFaceMap, resolveFontFace } from "../libs/fontFace.js";
import {
buildFontFaceMap,
parseFontFamily,
resolveFontFace
} from "../libs/fontFace.js";

/**
* This plugin mimics the HTML5 CanvasRenderingContext2D.
Expand Down Expand Up @@ -524,8 +528,7 @@ import { buildFontFaceMap, resolveFontFace } from "../libs/fontFace.js";
}

this.pdf.setFontSize(fontSize);

var parts = fontFamily.replace(/"|'/g, "").split(/\s*,\s*/);
var parts = parseFontFamily(fontFamily);

if (this.fontFaces) {
var fontFaceMap = getFontFaceMap(this.pdf, this.fontFaces);
Expand Down
71 changes: 70 additions & 1 deletion test/specs/fontfaces.spec.mjs
@@ -1,7 +1,8 @@
import {
resolveFontFace,
buildFontFaceMap,
normalizeFontFace
normalizeFontFace,
parseFontFamily
} from "../../src/libs/fontFace.js";

function fontFace(opts) {
Expand Down Expand Up @@ -373,4 +374,72 @@ describe("font-face", () => {
});
});
});

describe("font family parser", () => {
const defaultFont = ["times"];

it("should return default font family on empty input", () => {
const result = parseFontFamily(" ");

expect(result).toEqual(defaultFont);
});

it("should return default font family on non-sensical input", () => {
const result = parseFontFamily("@£$∞$§∞|$§∞©£@•$");

expect(result).toEqual(defaultFont);
});

it("should return default font family when font family contains illegal characters", () => {
const invalidStrs = [
"--no-double-hyphen",
"-3no-digit-after-hypen",
"0digits",
"#£no-illegal-characters"
];

const result = invalidStrs.map(parseFontFamily);

expect(result).toEqual(invalidStrs.map(() => defaultFont));
});

// If the user has specified a system font, then it's up to the user-agent to pick one.
it("should return default font if it is a system font", () => {
const systemFonts = [
"caption",
"icon",
"menu",
"message-box",
"small-caption",
"status-bar"
];

const result = systemFonts.map(parseFontFamily);

expect(result).toEqual(systemFonts.map(() => defaultFont));
});

it("should return all font families", () => {
var result = parseFontFamily(
" 'roboto sans' , \"SourceCode Pro\", Co-mP_l3x , arial, sans-serif "
);

expect(result).toEqual([
"roboto sans",
"sourcecode pro",
"co-mp_l3x",
"arial",
"sans-serif"
]);
});

it("should return default on mismatching quotes", () => {
var result = [
parseFontFamily("'I am not closed"),
parseFontFamily('"I am not closed either')
];

expect(result).toEqual([defaultFont, defaultFont]);
});
});
});

0 comments on commit 5e5cf96

Please sign in to comment.