infusion
Version:
Infusion is an application framework for developing flexible stuff with JavaScript
510 lines (470 loc) • 21.5 kB
JavaScript
/*
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/main/AUTHORS.md.
Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.
You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt
*/
"use strict";
/*******************************************************************************
* syllabification
*
* An enactor that is capable of breaking words down into syllables
*******************************************************************************/
/*
* `fluid.prefs.enactor.syllabification` makes use of the "hypher" library to split up words into their phonetic
* parts. Because different localizations may have different means of splitting up words, pattern files for the
* supported languages are used. The language patterns are pulled in dynamically based on the language codes
* encountered in the content. The language patterns available are configured through the patterns option,
* populated by the `fluid.prefs.enactor.syllabification.patterns` grade.
*/
fluid.defaults("fluid.prefs.enactor.syllabification", {
gradeNames: ["fluid.prefs.enactor", "fluid.prefs.enactor.syllabification.patterns", "fluid.viewComponent"],
preferenceMap: {
"fluid.prefs.syllabification": {
"model.enabled": "value"
}
},
selectors: {
separator: ".flc-syllabification-separator",
softHyphenPlaceholder: ".flc-syllabification-softHyphenPlaceholder"
},
strings: {
languageUnavailable: "Syllabification not available for %lang",
patternLoadError: "The pattern file %src could not be loaded. %errorMsg"
},
markup: {
separator: "<span class=\"flc-syllabification-separator fl-syllabification-separator\"></span>",
softHyphenPlaceholder: "<span class=\"flc-syllabification-softHyphenPlaceholder fl-syllabification-softHyphenPlaceholder\"></span>"
},
model: {
enabled: false
},
events: {
afterParse: null,
afterSyllabification: null,
onParsedTextNode: null,
onNodeAdded: null,
onError: null
},
listeners: {
"afterParse.waitForHyphenators": {
listener: "fluid.prefs.enactor.syllabification.waitForHyphenators",
args: ["{that}"]
},
"onParsedTextNode.syllabify": {
listener: "{that}.apply",
args: ["{arguments}.0.node", "{arguments}.0.lang"]
},
"onNodeAdded.syllabify": {
listener: "{that}.parse",
args: ["{arguments}.0", "{that}.model.enabled"]
}
},
components: {
parser: {
type: "fluid.textNodeParser",
options: {
listeners: {
"afterParse.boil": "{syllabification}.events.afterParse",
"onParsedTextNode.boil": "{syllabification}.events.onParsedTextNode"
}
}
},
observer: {
type: "fluid.mutationObserver",
container: "{that}.container",
options: {
defaultObserveConfig: {
attributes: false
},
modelListeners: {
"{syllabification}.model.enabled": {
funcName: "fluid.prefs.enactor.syllabification.disconnectObserver",
priority: "before:setPresentation",
args: ["{that}", "{change}.value"],
namespace: "disconnectObserver"
}
},
listeners: {
"onNodeAdded.boil": "{syllabification}.events.onNodeAdded",
"{syllabification}.events.afterSyllabification": {
listener: "{that}.observe",
namespace: "enableObserver"
}
}
}
}
},
distributeOptions: {
ignoreSelectorForEnactor: {
source: "{that}.options.ignoreSelectorForEnactor.forEnactor",
target: "{that > parser}.options.ignoredSelectors.forEnactor"
}
},
members: {
// `hyphenators` is a mapping of strings, representing the source paths of pattern files, to Promises
// linked to the resolutions of loading and initially applying those syllabification patterns.
hyphenators: {}
},
modelListeners: {
"enabled": {
listener: "{that}.setPresentation",
args: ["{that}.container", "{change}.value"],
namespace: "setPresentation"
}
},
invokers: {
apply: {
funcName: "fluid.prefs.enactor.syllabification.syllabify",
args: ["{that}", "{arguments}.0", "{arguments}.1"]
},
remove: {
funcName: "fluid.prefs.enactor.syllabification.removeSyllabification",
args: ["{that}", "{that}.options.ignoreSelectorForEnactor"]
},
setPresentation: {
funcName: "fluid.prefs.enactor.syllabification.setPresentation",
args: ["{that}", "{arguments}.0", "{arguments}.1"]
},
parse: {
funcName: "fluid.prefs.enactor.syllabification.parse",
args: ["{that}", "{arguments}.0"]
},
createHyphenator: {
funcName: "fluid.prefs.enactor.syllabification.createHyphenator",
args: ["{that}", "{arguments}.0", "{arguments}.1"]
},
getHyphenator: {
funcName: "fluid.prefs.enactor.syllabification.getHyphenator",
args: ["{that}", "{arguments}.0"]
},
getPattern: "fluid.prefs.enactor.syllabification.getPattern",
hyphenateNode: {
funcName: "fluid.prefs.enactor.syllabification.hyphenateNode",
args: [
"{arguments}.0",
"{arguments}.1",
"{that}.options.markup.separator",
"{that}.options.markup.softHyphenPlaceholder"
]
},
injectScript: {
this: "$",
method: "ajax",
args: [{
url: "{arguments}.0",
dataType: "script",
cache: true
}]
}
}
});
/**
* Only disconnect the observer if the state is set to false.
* This corresponds to the syllabification's `enabled` model path being set to false.
*
* @param {fluid.mutationObserver} that - an instance of `fluid.mutationObserver`
* @param {Boolean} state - if `false` disconnect, otherwise do nothing
*/
fluid.prefs.enactor.syllabification.disconnectObserver = function (that, state) {
if (!state) {
that.disconnect();
}
};
/**
* Wait for all hyphenators to be resolved. After they are resolved the `afterSyllabification` event is fired.
* If any of the hyphenator promises is rejected, the `onError` event is fired instead.
*
* @param {fluid.prefs.enactor.syllabification} that - an instance of `fluid.prefs.enactor.syllabification`
*
* @return {Promise} - returns the sequence promise; which is constructed from the hyphenator promises.
*/
fluid.prefs.enactor.syllabification.waitForHyphenators = function (that) {
var hyphenatorPromises = fluid.values(that.hyphenators);
var promise = fluid.promise.sequence(hyphenatorPromises);
promise.then(function () {
that.events.afterSyllabification.fire();
}, that.events.onError.fire);
return promise;
};
fluid.prefs.enactor.syllabification.parse = function (that, elm) {
elm = fluid.unwrap(elm);
elm = elm.nodeType === Node.ELEMENT_NODE ? $(elm) : $(elm.parentNode);
that.parser.parse(elm);
};
/**
* Creates a hyphenator instance making use of the pattern supplied by the path; which is injected into the Document
* if it hasn't already been loaded. If the pattern file cannot be loaded, the onError event is fired.
*
* @param {fluid.prefs.enactor.syllabification} that - an instance of `fluid.prefs.enactor.syllabification`
* @param {Object} pattern - the `file path` to the pattern file. The path may include a string template token to
* resolve a portion of its path from. The token will be resolved from the component's
* `terms` option. (e.g. "%patternPrefix/en-us.js");
* @param {String} lang - a valid BCP 47 language code. (NOTE: supported lang codes are defined in the
* `patterns`) option.
*
* @return {Promise} - If a hyphenator is successfully created, the promise is resolved with it. Otherwise it is
* resolved with undefined and the `onError` event fired.
*/
fluid.prefs.enactor.syllabification.createHyphenator = function (that, pattern, lang) {
var promise = fluid.promise();
var globalPath = ["Hypher", "languages", lang];
var hyphenator = fluid.getGlobalValue(globalPath);
// If the pattern file has already been loaded, return the hyphenator.
// This could happen if the pattern file is statically linked to the page.
if (hyphenator) {
promise.resolve(hyphenator);
return promise;
}
var src = fluid.stringTemplate(pattern, that.options.terms);
var injectPromise = that.injectScript(src);
injectPromise.then(function () {
hyphenator = fluid.getGlobalValue(globalPath);
promise.resolve(hyphenator);
}, function (error) {
var errorInfo = {
src: src,
errorMsg: typeof(error) === "string" ? error : ""
};
var errorMessage = fluid.stringTemplate(that.options.strings.patternLoadError, errorInfo);
fluid.log(fluid.logLevel.WARN, errorMessage, error);
that.events.onError.fire(errorMessage, error);
//TODO: A promise rejection would be more appropriate. However, we need to know when all of the hyphenators
// have attempted to load and apply syllabification. The current promise utility,
// fluid.promise.sequence, will reject the whole sequence if a promise is rejected, and prevent us from
// knowing if all of the hyphenators have been attempted. We should be able to improve this
// implementation once https://issues.fluidproject.org/browse/FLUID-5938 has been resolved.
//
// If the pattern file could not be loaded, resolve the promise without a hyphenator (undefined).
promise.resolve();
});
return promise;
};
/**
* Information about a pattern, including the resolved language code and the file path to the pattern file.
*
* @typedef {Object} PatternInfo
* @property {String} lang - The resolved language code
* @property {String|undefined} src - The file path to the pattern file for the resolved language. If no pattern
* file is available, the value should be `undefined`.
*/
/**
* Assembles an Object containing the information for locating the pattern file. If a pattern for the specific
* requested language code cannot be located, it will attempt to locate a fall back, by looking for a pattern
* supporting the generic language code. If no pattern can be found, `undefined` is returned as the `src` value.
*
*
* @param {String} lang - a valid BCP 47 language code. (NOTE: supported lang codes are defined in the
* `patterns`) option.
* @param {Object.<String, String>} patterns - an object mapping language codes to file paths for the pattern files. For example:
* {"en": "./patterns/en-us.js"}
*
* @return {PatternInfo} - returns a PatternInfo Object for the resolved language code. If a pattern file is not
* available for the language, the `src` property will be `undefined`.
*/
fluid.prefs.enactor.syllabification.getPattern = function (lang, patterns) {
var src = patterns[lang];
if (!src) {
lang = lang.split("-")[0];
src = patterns[lang];
}
return {
lang: lang,
src: src
};
};
/**
* Retrieves a promise for the appropriate hyphenator. If a hyphenator has not already been created, it will attempt
* to create one and assign the related promise to the `hyphenators` member for future retrieval.
*
* When creating a hyphenator, it first checks if there is configuration for the specified `lang`. If that fails,
* it attempts to fall back to a less specific localization.
*
* @param {fluid.prefs.enactor.syllabification} that - an instance of `fluid.prefs.enactor.syllabification`
* @param {String} lang - a valid BCP 47 language code. (NOTE: supported lang codes are defined in the
* `patterns`) option.
*
* @return {Promise} - returns a promise. If a hyphenator is successfully created, it is resolved with it.
* Otherwise, it resolves with undefined.
*/
fluid.prefs.enactor.syllabification.getHyphenator = function (that, lang) {
//TODO: For all of the instances where an empty promise is resolved, a promise rejection would be more
// appropriate. However, we need to know when all of the hyphenators have attempted to load and apply
// syllabification. The current promise utility, fluid.promise.sequence, will reject the whole sequence if
// a promise is rejected, and prevent us from knowing if all of the hyphenators have been attempted. We
// should be able to improve this implementation once https://issues.fluidproject.org/browse/FLUID-5938 has
// been resolved.
var promise = fluid.promise();
var hyphenatorPromise;
if (!lang) {
promise.resolve();
return promise;
}
var pattern = that.getPattern(lang.toLowerCase(), that.options.patterns);
if (!pattern.src) {
hyphenatorPromise = promise;
promise.resolve();
return promise;
}
if (that.hyphenators[pattern.src]) {
return that.hyphenators[pattern.src];
}
hyphenatorPromise = that.createHyphenator(pattern.src, pattern.lang);
fluid.promise.follow(hyphenatorPromise, promise);
that.hyphenators[pattern.src] = hyphenatorPromise;
return promise;
};
fluid.prefs.enactor.syllabification.syllabify = function (that, node, lang) {
var hyphenatorPromise = that.getHyphenator(lang);
hyphenatorPromise.then(function (hyphenator) {
that.hyphenateNode(hyphenator, node);
});
};
/**
* Splits a text node at the specified `position` into two text nodes and inserts the specified `toInsert` content
* between them.
*
* @param {DomNode} node - The text node to be split by the inserted content
* @param {DomNode|DomElement} toInsert - The node/element to insert into the original content from the `node`
* @param {Number} position - The position to split `node`. At this point the `node` will be split into two new text
* nodes and the `toInsert` node placed between them. The position is the 0 based index
* where the content would be inserted into the original text node. That is, the portion
* before `position` will be included before the inserted node, and those including and
* after `position` will be included after the inserted node.
*
* @return {DomNode} - The newly created text node after executing the split. That is, the second portion of the
* severed text node.
*/
fluid.prefs.enactor.syllabification.insertIntoTextNode = function (node, toInsert, position) {
node = node.splitText(position);
node.parentNode.insertBefore(toInsert, node);
return node;
};
/**
* Insert separators, indicating the hyphenated positions, into a text node. This will inject the specified
* markup as the separators and split the original textnode at the hyphenation points. If there are no places to
* inject a hyphenation point, the node will be left unchanged. If the original textnode inclues soft hyphens (i.e.
* ­) characters, these will be used as a hyphenation point with the original location in the text node
* replaced with the `softHyphenPlaceholderMarkup` to allow it to be restored when syllabification is removed.
*
* @param {HypherHyphenator} hyphenator - an instance of a Hypher Hyphenator
* @param {DomNode} node - a DOM node containing text to be hyphenated
* @param {String} separatorMarkup - the markup to be injected and used to indicate the separators/hyphens
* @param {String} softHyphenPlaceholderMarkup - the markup to be injected and used to indicate the original
* location of any soft hyphens included in `node`
*/
fluid.prefs.enactor.syllabification.hyphenateNode = function (hyphenator, node, separatorMarkup, softHyphenPlaceholderMarkup) {
if (!hyphenator || !node.textContent) {
return;
}
var softHyphen = "\u00AD";
var hyphenated = hyphenator.hyphenateText(node.textContent);
// split words on soft hyphens
var segs = hyphenated.split(softHyphen);
for (var i = 0; i < segs.length; i++) {
// If the text content starts with a soft hyphen (\u00AD, ­) replace it with an empty placeholder; which
// is used to put the soft hyphen back if syllabification is disabled.
// See: https://issues.fluidproject.org/browse/FLUID-6554
if (node.textContent.startsWith(softHyphen)) {
// converts the softHyphenPlaceholderMarkup markup string into a Dom Node
var placeholder = $(softHyphenPlaceholderMarkup)[0];
var newNode = fluid.prefs.enactor.syllabification.insertIntoTextNode(node, placeholder, 1);
node.remove();
node = newNode;
}
// skip the last segment as we only need to place separators in between the parts of the words
if (i < segs.length - 1) {
var separator = $(separatorMarkup)[0]; // converts the separatorMarkup markup string into a Dom Node
node = fluid.prefs.enactor.syllabification.insertIntoTextNode(node, separator, segs[i].length);
}
}
};
fluid.prefs.enactor.syllabification.removeSyllabification = function (that, ignoreSelectorForEnactor) {
// remove separators
that.locate("separator").each(function (index, elm) {
// skip elements whose parent selector should be ignored
if (ignoreSelectorForEnactor) {
var selectors = Object.values(ignoreSelectorForEnactor).filter(sel => sel);
if (selectors.find(selector => elm.closest(selector))) {
return;
}
}
var parent = elm.parentNode;
$(elm).remove();
parent.normalize();
});
// restore soft hyphens
that.locate("softHyphenPlaceholder").each(function (index, elm) {
var parent = elm.parentNode;
elm.replaceWith(document.createTextNode("\u00AD"));
parent.normalize();
});
};
fluid.prefs.enactor.syllabification.setPresentation = function (that, elm, state) {
if (state) {
that.parse(elm);
} else {
that.remove();
}
};
/**********************************************************************
* Language Pattern File Configuration
*
*
* Supplies the source paths for the language pattern files used to
* separate words into their phonetic parts.
**********************************************************************/
fluid.defaults("fluid.prefs.enactor.syllabification.patterns", {
terms: {
patternPrefix: "../../../lib/hypher/patterns"
},
patterns: {
be: "%patternPrefix/bg.js",
bn: "%patternPrefix/bn.js",
ca: "%patternPrefix/ca.js",
cs: "%patternPrefix/cs.js",
da: "%patternPrefix/da.js",
de: "%patternPrefix/de.js",
el: "%patternPrefix/el-monoton.js",
"el-monoton": "%patternPrefix/el-monoton.js",
"el-polyton": "%patternPrefix/el-polyton.js",
en: "%patternPrefix/en-us.js",
"en-gb": "%patternPrefix/en-gb.js",
"en-us": "%patternPrefix/en-us.js",
es: "%patternPrefix/es.js",
fi: "%patternPrefix/fi.js",
fr: "%patternPrefix/fr.js",
grc: "%patternPrefix/grc.js",
gu: "%patternPrefix/gu.js",
hi: "%patternPrefix/hi.js",
hu: "%patternPrefix/hu.js",
hy: "%patternPrefix/hy.js",
is: "%patternPrefix/is.js",
it: "%patternPrefix/it.js",
kn: "%patternPrefix/kn.js",
la: "%patternPrefix/la.js",
lt: "%patternPrefix/lt.js",
lv: "%patternPrefix/lv.js",
ml: "%patternPrefix/ml.js",
nb: "%patternPrefix/nb-no.js",
"nb-no": "%patternPrefix/nb-no.js",
no: "%patternPrefix/nb-no.js",
nl: "%patternPrefix/nl.js",
or: "%patternPrefix/or.js",
pa: "%patternPrefix/pa.js",
pl: "%patternPrefix/pl.js",
pt: "%patternPrefix/pt.js",
ru: "%patternPrefix/ru.js",
sk: "%patternPrefix/sk.js",
sl: "%patternPrefix/sl.js",
sv: "%patternPrefix/sv.js",
ta: "%patternPrefix/ta.js",
te: "%patternPrefix/te.js",
tr: "%patternPrefix/tr.js",
uk: "%patternPrefix/uk.js"
}
});