350 lines
10 KiB
JavaScript
350 lines
10 KiB
JavaScript
|
/**
|
||
|
* @license Hyphenopoly_Loader 4.12.0 - client side hyphenation
|
||
|
* ©2021 Mathias Nater, Güttingen (mathiasnater at gmail dot com)
|
||
|
* https://github.com/mnater/Hyphenopoly
|
||
|
*
|
||
|
* Released under the MIT license
|
||
|
* http://mnater.github.io/Hyphenopoly/LICENSE
|
||
|
*/
|
||
|
/* globals Hyphenopoly:readonly */
|
||
|
((w, d, H, o) => {
|
||
|
"use strict";
|
||
|
|
||
|
/**
|
||
|
* Shortcut for new Map
|
||
|
* @param {any} init - initialiser for new Map
|
||
|
* @returns {Map}
|
||
|
*/
|
||
|
const mp = (init) => {
|
||
|
return new Map(init);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Sets default properties for an Object
|
||
|
* @param {object} obj - The object to set defaults to
|
||
|
* @param {object} defaults - The defaults to set
|
||
|
* @returns {object}
|
||
|
*/
|
||
|
const setDefaults = (obj, defaults) => {
|
||
|
if (obj) {
|
||
|
o.entries(defaults).forEach(([k, v]) => {
|
||
|
// eslint-disable-next-line security/detect-object-injection
|
||
|
obj[k] = obj[k] || v;
|
||
|
});
|
||
|
return obj;
|
||
|
}
|
||
|
return defaults;
|
||
|
};
|
||
|
|
||
|
const store = sessionStorage;
|
||
|
const scriptName = "Hyphenopoly_Loader.js";
|
||
|
const lcRequire = mp();
|
||
|
|
||
|
const shortcuts = {
|
||
|
"ac": "appendChild",
|
||
|
"ce": "createElement",
|
||
|
"ct": "createTextNode"
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Set H.cf (Hyphenopoly.clientFeatures) either by reading out previously
|
||
|
* computed settings from sessionStorage or creating a template object.
|
||
|
* This is in an iife to keep complexity low.
|
||
|
*/
|
||
|
(() => {
|
||
|
if (H.cacheFeatureTests && store.getItem(scriptName)) {
|
||
|
H.cf = JSON.parse(store.getItem(scriptName));
|
||
|
H.cf.langs = mp(H.cf.langs);
|
||
|
} else {
|
||
|
H.cf = {
|
||
|
"langs": mp(),
|
||
|
"pf": false
|
||
|
};
|
||
|
}
|
||
|
})();
|
||
|
|
||
|
/**
|
||
|
* Set H.paths and some H.s (setup) fields to defaults or
|
||
|
* overwrite with user settings.
|
||
|
* These is an iife to keep complexity low.
|
||
|
*/
|
||
|
(() => {
|
||
|
const thisScript = d.currentScript.src;
|
||
|
const maindir = thisScript.slice(0, (thisScript.lastIndexOf("/") + 1));
|
||
|
const patterndir = maindir + "patterns/";
|
||
|
H.paths = setDefaults(H.paths, {
|
||
|
maindir,
|
||
|
patterndir
|
||
|
});
|
||
|
|
||
|
H.s = setDefaults(H.setup, {
|
||
|
"CORScredentials": "include",
|
||
|
"hide": "all",
|
||
|
"selectors": {".hyphenate": {}},
|
||
|
"timeout": 1000
|
||
|
});
|
||
|
// Change mode string to mode int
|
||
|
H.s.hide = ["all", "element", "text"].indexOf(H.s.hide);
|
||
|
})();
|
||
|
|
||
|
/**
|
||
|
* Copy required languages to local lcRequire (lowercaseRequire) and
|
||
|
* eventually fallbacks to local lcFallbacks (lowercaseFallbacks).
|
||
|
* This is in an iife to keep complexity low.
|
||
|
*/
|
||
|
(() => {
|
||
|
const fallbacks = mp(o.entries(H.fallbacks || {}));
|
||
|
o.entries(H.require).forEach(([lang, wo]) => {
|
||
|
lcRequire.set(lang.toLowerCase(), {
|
||
|
"fn": fallbacks.get(lang) || lang,
|
||
|
wo
|
||
|
});
|
||
|
});
|
||
|
})();
|
||
|
|
||
|
/**
|
||
|
* Create deferred Promise
|
||
|
*
|
||
|
* Kudos to http://lea.verou.me/2016/12/resolve-promises-externally-with-
|
||
|
* this-one-weird-trick/
|
||
|
* @return {promise}
|
||
|
*/
|
||
|
const defProm = () => {
|
||
|
let res = null;
|
||
|
let rej = null;
|
||
|
const promise = new Promise((resolve, reject) => {
|
||
|
res = resolve;
|
||
|
rej = reject;
|
||
|
});
|
||
|
promise.resolve = res;
|
||
|
promise.reject = rej;
|
||
|
|
||
|
return promise;
|
||
|
};
|
||
|
|
||
|
let stylesNode = null;
|
||
|
|
||
|
/**
|
||
|
* Define function H.hide.
|
||
|
* This function hides (state = 1) or unhides (state = 0)
|
||
|
* the whole document (mode == 0) or
|
||
|
* each selected element (mode == 1) or
|
||
|
* text of each selected element (mode == 2) or
|
||
|
* nothing (mode == -1)
|
||
|
* @param {integer} state - State
|
||
|
* @param {integer} mode - Mode
|
||
|
*/
|
||
|
H.hide = (state, mode) => {
|
||
|
if (state === 0) {
|
||
|
if (stylesNode) {
|
||
|
stylesNode.remove();
|
||
|
}
|
||
|
} else {
|
||
|
const vis = "{visibility:hidden!important}";
|
||
|
stylesNode = d[shortcuts.ce]("style");
|
||
|
let myStyle = "";
|
||
|
if (mode === 0) {
|
||
|
myStyle = "html" + vis;
|
||
|
} else {
|
||
|
o.keys(H.s.selectors).forEach((sel) => {
|
||
|
if (mode === 1) {
|
||
|
myStyle += sel + vis;
|
||
|
} else {
|
||
|
myStyle += sel + "{color:transparent!important}";
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
stylesNode[shortcuts.ac](d[shortcuts.ct](myStyle));
|
||
|
d.head[shortcuts.ac](stylesNode);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
const tester = (() => {
|
||
|
let fakeBody = null;
|
||
|
return {
|
||
|
|
||
|
/**
|
||
|
* Append fakeBody with tests to document
|
||
|
* @returns {Object|null} The body element or null, if no tests
|
||
|
*/
|
||
|
"ap": () => {
|
||
|
if (fakeBody) {
|
||
|
d.documentElement[shortcuts.ac](fakeBody);
|
||
|
return fakeBody;
|
||
|
}
|
||
|
return null;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Remove fakeBody
|
||
|
* @returns {undefined}
|
||
|
*/
|
||
|
"cl": () => {
|
||
|
if (fakeBody) {
|
||
|
fakeBody.remove();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Create and append div with CSS-hyphenated word
|
||
|
* @param {string} lang Language
|
||
|
* @returns {undefined}
|
||
|
*/
|
||
|
"cr": (lang) => {
|
||
|
if (H.cf.langs.has(lang)) {
|
||
|
return;
|
||
|
}
|
||
|
fakeBody = fakeBody || d[shortcuts.ce]("body");
|
||
|
const testDiv = d[shortcuts.ce]("div");
|
||
|
const ha = "hyphens:auto";
|
||
|
testDiv.lang = lang;
|
||
|
testDiv.style.cssText = `visibility:hidden;-webkit-${ha};-ms-${ha};${ha};width:48px;font-size:12px;line-height:12px;border:none;padding:0;word-wrap:normal`;
|
||
|
testDiv[shortcuts.ac](
|
||
|
d[shortcuts.ct](lcRequire.get(lang).wo.toLowerCase())
|
||
|
);
|
||
|
fakeBody[shortcuts.ac](testDiv);
|
||
|
}
|
||
|
};
|
||
|
})();
|
||
|
|
||
|
/**
|
||
|
* Checks if hyphens (ev.prefixed) is set to auto for the element.
|
||
|
* @param {Object} elm - the element
|
||
|
* @returns {Boolean} result of the check
|
||
|
*/
|
||
|
const checkCSSHyphensSupport = (elmStyle) => {
|
||
|
const h = elmStyle.hyphens ||
|
||
|
elmStyle.webkitHyphens ||
|
||
|
elmStyle.msHyphens;
|
||
|
return (h === "auto");
|
||
|
};
|
||
|
|
||
|
H.res = {
|
||
|
"he": mp()
|
||
|
};
|
||
|
const fw = mp();
|
||
|
|
||
|
/**
|
||
|
* Load hyphenEngines
|
||
|
*
|
||
|
* Make sure each .wasm is loaded exactly once, even for fallbacks
|
||
|
* fw: fetched wasm (maps filename to language)
|
||
|
* he: hyphenEngines (maps lang to wasm and counter)
|
||
|
* c (counter) is needed in Hyphenopoly.js to decide
|
||
|
* if wasm needs to be cloned
|
||
|
* @param {string} lang The language
|
||
|
* @returns {undefined}
|
||
|
*/
|
||
|
const loadhyphenEngine = (lang) => {
|
||
|
const filename = lcRequire.get(lang).fn + ".wasm";
|
||
|
H.cf.pf = true;
|
||
|
H.cf.langs.set(lang, "H9Y");
|
||
|
if (fw.has(filename)) {
|
||
|
const hyphenEngineWrapper = H.res.he.get(fw.get(filename));
|
||
|
hyphenEngineWrapper.c = true;
|
||
|
H.res.he.set(lang, hyphenEngineWrapper);
|
||
|
} else {
|
||
|
H.res.he.set(
|
||
|
lang,
|
||
|
{
|
||
|
"w": w.fetch(H.paths.patterndir + filename, {"credentials": H.s.CORScredentials})
|
||
|
}
|
||
|
);
|
||
|
fw.set(filename, lang);
|
||
|
}
|
||
|
};
|
||
|
lcRequire.forEach((value, lang) => {
|
||
|
if (value.wo === "FORCEHYPHENOPOLY" || H.cf.langs.get(lang) === "H9Y") {
|
||
|
loadhyphenEngine(lang);
|
||
|
} else {
|
||
|
tester.cr(lang);
|
||
|
}
|
||
|
});
|
||
|
const testContainer = tester.ap();
|
||
|
if (testContainer) {
|
||
|
testContainer.querySelectorAll("div").forEach((n) => {
|
||
|
if (checkCSSHyphensSupport(n.style) && n.offsetHeight > 12) {
|
||
|
H.cf.langs.set(n.lang, "CSS");
|
||
|
} else {
|
||
|
loadhyphenEngine(n.lang);
|
||
|
}
|
||
|
});
|
||
|
tester.cl();
|
||
|
}
|
||
|
const he = H.handleEvent;
|
||
|
if (H.cf.pf) {
|
||
|
H.res.DOM = new Promise((res) => {
|
||
|
if (d.readyState === "loading") {
|
||
|
d.addEventListener(
|
||
|
"DOMContentLoaded",
|
||
|
res,
|
||
|
{
|
||
|
"once": true,
|
||
|
"passive": true
|
||
|
}
|
||
|
);
|
||
|
} else {
|
||
|
res();
|
||
|
}
|
||
|
});
|
||
|
const hide = H.s.hide;
|
||
|
if (hide === 0) {
|
||
|
H.hide(1, 0);
|
||
|
}
|
||
|
if (hide !== -1) {
|
||
|
H.timeOutHandler = w.setTimeout(() => {
|
||
|
H.hide(0, null);
|
||
|
// eslint-disable-next-line no-console
|
||
|
console.info(scriptName + " timed out.");
|
||
|
}, H.s.timeout);
|
||
|
}
|
||
|
H.res.DOM.then(() => {
|
||
|
if (hide > 0) {
|
||
|
H.hide(1, hide);
|
||
|
}
|
||
|
});
|
||
|
// Load main script
|
||
|
const script = d[shortcuts.ce]("script");
|
||
|
script.src = H.paths.maindir + "Hyphenopoly.js";
|
||
|
d.head[shortcuts.ac](script);
|
||
|
H.hy6ors = mp();
|
||
|
H.cf.langs.forEach((langDef, lang) => {
|
||
|
if (langDef === "H9Y") {
|
||
|
H.hy6ors.set(lang, defProm());
|
||
|
}
|
||
|
});
|
||
|
H.hy6ors.set("HTML", defProm());
|
||
|
H.hyphenators = new Proxy(H.hy6ors, {
|
||
|
"get": (target, key) => {
|
||
|
return target.get(key);
|
||
|
},
|
||
|
"set": () => {
|
||
|
// Inhibit setting of hyphenators
|
||
|
return true;
|
||
|
}
|
||
|
});
|
||
|
(() => {
|
||
|
if (he && he.polyfill) {
|
||
|
he.polyfill();
|
||
|
}
|
||
|
})();
|
||
|
} else {
|
||
|
(() => {
|
||
|
if (he && he.tearDown) {
|
||
|
he.tearDown();
|
||
|
}
|
||
|
w.Hyphenopoly = null;
|
||
|
})();
|
||
|
}
|
||
|
(() => {
|
||
|
if (H.cacheFeatureTests) {
|
||
|
store.setItem(scriptName, JSON.stringify(
|
||
|
{
|
||
|
"langs": [...H.cf.langs.entries()],
|
||
|
"pf": H.cf.pf
|
||
|
}
|
||
|
));
|
||
|
}
|
||
|
})();
|
||
|
})(window, document, Hyphenopoly, Object);
|