usfm-editor
Version:
A WYSIWYG editor component for USFM
206 lines (157 loc) • 6.26 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.slateToUsfm = slateToUsfm;
require("core-js/modules/es.array.reduce.js");
var _NodeTypes = _interopRequireDefault(require("../utils/NodeTypes"));
var _MyText = require("../plugins/helpers/MyText");
var _slate = require("slate");
var _UsfmMarkers = require("../utils/UsfmMarkers");
var _MarkerInfoMap = _interopRequireDefault(require("../utils/MarkerInfoMap"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function slateToUsfm(value) {
const usfm = serializeRecursive(value);
const normalized = normalizeWhitespace(usfm); // Convert the workaround for the pipe literal back to the pipe character
return normalized.replace(/&pipe;/g, "|");
}
function normalizeWhitespace(usfm) {
// Remove leading newline
usfm = usfm.replace(/^\n/, ""); // Any whitespace ending in a newline should just be a newline
usfm = usfm.replace(/(\s+?)\n/g, "\n"); // Remove space at the end of the document
usfm = usfm.replace(/(\s+)$/, "");
return usfm;
}
function isTypedElement(node) {
return _slate.Element.isElement(node) && node.type !== undefined;
}
function serializeRecursive(value) {
if (Array.isArray(value)) {
return value.map(serializeRecursive).reduce(concatUsfm);
} else if (_slate.Element.isElement(value) && isTypedElement(value)) {
// value implements the "Element" interface
if (value.type === _UsfmMarkers.UsfmMarkers.CHAPTERS_AND_VERSES.v) {
return serializeVerseNumber(value);
} else if (isChapterHeaderOrVerse(value)) {
// Structural types (header, chapter, verse) do not
// have a tag that needs to be converted to usfm
return serializeRecursive(value.children);
} else {
return serializeElement(value);
}
} else if (_slate.Text.isText(value)) {
return serializeTexts([value]);
} else {
return "";
}
}
function isChapterHeaderOrVerse(h) {
return [_NodeTypes.default.CHAPTER, _NodeTypes.default.HEADERS, _NodeTypes.default.VERSE].includes(h.type);
}
function concatUsfm(a, b) {
return a.concat(b);
}
function serializeVerseNumber(verseNumber) {
var _verseNumber$children;
const verseNumberText = (_verseNumber$children = verseNumber.children[0]) === null || _verseNumber$children === void 0 ? void 0 : _verseNumber$children.text; // Front matter must start on the next line.
// Whitespace will be normalized later, so empty front
// matter will not cause multiple newlines.
return verseNumberText === "front" ? "\n" : `\n\\v ${verseNumberText} `;
}
function serializeElement(value) {
const marker = serializeMarker(value.type);
const content = serializeTexts(value.children);
const endMarker = getEndMarker(value.type);
return marker + content + endMarker;
}
function serializeMarker(type) {
if (type === _NodeTypes.default.INLINE_CONTAINER) {
return "";
}
return `\n\\${type} `;
}
function getEndMarker(type) {
const tagInfo = _MarkerInfoMap.default.get(type);
return tagInfo && tagInfo.endMarker ? `\\${tagInfo.endMarker}` : "";
}
/**
* Processes marks to construct the corresponding usfm content
*/
function serializeTexts(children) {
let usfm = "";
let markStack = new Array();
for (const text of children) {
if (!_MyText.MyText.isText(text)) {
console.error("Unexpected non-text element in serializeTexts!");
continue;
}
const marks = _MyText.MyText.marks(text); // If this child lacks marks that were on the previous ones, close them
const removedMarks = setDiff(markStack, marks);
let result = closeMarks(usfm, markStack, removedMarks);
usfm = result.usfm;
markStack = result.stack; // If this child has marks that weren't on the previous ones, open them
const addedMarks = setDiff(marks, markStack);
const markerWithNoEndMarker = addedMarks.find(m => {
var _MarkerInfoMap$get;
return ((_MarkerInfoMap$get = _MarkerInfoMap.default.get(m)) === null || _MarkerInfoMap$get === void 0 ? void 0 : _MarkerInfoMap$get.endMarker) === undefined;
}); // Sometimes an empty text and an adjacent text will have the same marker.
// Forcing normalization would fix this, but for now we need to ensure that
// the text field is non-empty.
if (markerWithNoEndMarker && text.text != "") {
usfm += `\\${markerWithNoEndMarker} `;
}
const closeableAddedMarks = addedMarks.filter(m => m != markerWithNoEndMarker);
result = openMarks(usfm, markStack, closeableAddedMarks);
usfm = result.usfm;
markStack = result.stack;
usfm += text.text;
}
const res = closeMarks(usfm, markStack, [...markStack]);
return res.usfm;
}
function setDiff(a, b) {
const setA = new Set(a);
const setB = new Set(b);
return [...setA].filter(x => !setB.has(x));
}
/**
* Adds closing tags for marks that should be closed (e.g. \nd or \+nd)
*/
function closeMarks(usfm, markStack, toClose) {
while (toClose.length > 0) {
const mark = toClose[0];
let closing = undefined;
while (closing != mark) {
var _MarkerInfoMap$get2;
closing = markStack.pop();
if (!closing) {
console.error("Internal state error closing markers!"); // This is bad, but continue by closing what we can
closing = mark;
}
const endMarker = (_MarkerInfoMap$get2 = _MarkerInfoMap.default.get(closing)) === null || _MarkerInfoMap$get2 === void 0 ? void 0 : _MarkerInfoMap$get2.endMarker; // If there are still marks in the stack,
// this output tag should be nested, so add a "+"
const plus = markStack.length > 0 ? "+" : "";
usfm += `\\${plus}${endMarker}`;
toClose = toClose.filter(x => x != closing); // Remove from list
}
}
return {
usfm: usfm,
stack: markStack
};
}
/**
* Adds opening tags for marks that should be opened (e.g. \nd or \+nd)
*/
function openMarks(usfm, markStack, toOpen) {
for (const mark of toOpen) {
markStack.push(mark); // If there are additional marks in the stack,
// this output tag should be nested, so add a "+"
const plus = markStack.length > 1 ? "+" : "";
usfm += `\\${plus}${mark} `;
}
return {
usfm: usfm,
stack: markStack
};
}