ilib-loctool-ghfm-readmeio
Version:
A loctool plugin that knows how to process github-flavored markdown files with www.readme.io extensions
1,110 lines (986 loc) • 37.5 kB
JavaScript
/*
* MarkdownFile.js - plugin to extract resources from an Markdown file
*
* Copyright © 2019, Box, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var fs = require("fs");
var path = require("path");
var log4js = require("log4js");
var MessageAccumulator = require("message-accumulator");
var Node = require("ilib-tree-node");
var ilib = require("ilib");
var Locale = require("ilib/lib/Locale.js");
var isAlnum = require("ilib/lib/isAlnum.js");
var isIdeo = require("ilib/lib/isIdeo.js");
var IString = require("ilib/lib/IString.js");
var unified = require("unified");
var markdown = require("remark-parse");
var html = require("remark-html");
var remark2rehype = require('remark-rehype');
var highlight = require('remark-highlight.js');
var raw = require('rehype-raw');
var stringify = require('remark-stringify');
var he = require("he");
var unistFilter = require('unist-util-filter');
var unistMap = require('unist-util-map');
var unistVisit = require('unist-util-visit');
var u = require('unist-builder');
var logger = log4js.getLogger("loctool.lib.MarkdownFile");
// load the data for these
isAlnum._init();
isIdeo._init();
var mdparser = unified().
use(markdown, {
commonmark: true,
gfm: true
}).
use(highlight).
use(remark2rehype).
use(raw);
var mdstringify = unified().
use(stringify, {
commonmark: true,
gfm: true,
rule: '-',
ruleSpaces: false,
bullet: '*',
listItemIndent: 1
})();
function escapeQuotes(str) {
var ret = "";
if (str.length < 1) {
return '';
}
var inQuote = false;
for (var i = 0; i < str.length; i++) {
switch (str[i]) {
case '"':
if (inQuote) {
if (i+1 < str.length-1 && str[i+1] !== '\n') {
ret += '\\';
}
} else {
inQuote = true;
}
ret += '"';
break;
case '\n':
inQuote = false;
ret += '\n';
break;
case '\\':
if (i+1 < str.length-1) {
i++;
if (str[i] === '[') {
ret += str[i];
} else {
ret += '\\';
if (str[i] !== '\\') {
ret += str[i];
}
}
} else {
ret += '\\';
}
break;
default:
ret += str[i];
break;
}
}
return ret;
};
/**
* Create a new Markdown file with the given path name and within
* the given project.
*
* @param {Project} project the project object
* @param {String} pathName path to the file relative to the root
* of the project file
* @param {FileType} type the file type instance of this file
*/
var MarkdownFile = function(options) {
options = options || {};
this.project = options.project;
this.API = this.project.getAPI();
this.pathName = options.pathName;
this.type = options.type;
this.set = this.API.newTranslationSet(this.project ? this.project.sourceLocale : "zxx-XX");
// this.componentIndex = 0;
};
/**
* Unescape the string to make the same string that would be
* in memory in the target programming language. This includes
* unescaping both special and Unicode characters.
*
* @static
* @param {String} string the string to unescape
* @returns {String} the unescaped string
*/
MarkdownFile.unescapeString = function(string) {
var unescaped = string;
unescaped = he.decode(unescaped);
unescaped = unescaped.
replace(/^\\\\/g, "\\").
replace(/([^\\])\\\\/g, "$1\\").
replace(/\\(.)/g, "$1");
return unescaped;
};
/**
* Clean the string to make a source string. This means
* removing leading and trailing white space, compressing
* whitespaces, and unescaping characters. This changes
* the string from what it looks like in the source
* code, but it increases the matching between strings
* that only differ in ways that don't matter.
*
* @static
* @param {String} string the string to clean
* @returns {String} the cleaned string
*/
MarkdownFile.cleanString = function(string) {
var unescaped = MarkdownFile.unescapeString(string);
unescaped = unescaped.
replace(/[ \n\t\r\f]+/g, " ").
trim();
return unescaped;
};
/**
* Make a new key for the given string.
*
* @private
* @param {String} source the source string to make a resource
* key for
* @returns {String} a unique key for this string
*/
MarkdownFile.prototype.makeKey = function(source) {
return this.API.utils.hashKey(MarkdownFile.cleanString(source));
};
var reWholeTag = /<("(\\"|[^"])*"|'(\\'|[^'])*'|[^>])*>/g;
MarkdownFile.prototype._addTransUnit = function(text, comment) {
if (text) {
var source = this.API.utils.escapeInvalidChars(text);
this.set.add(this.API.newResource({
resType: "string",
project: this.project.getProjectId(),
key: this.makeKey(source),
sourceLocale: this.project.sourceLocale,
source: source,
autoKey: true,
pathName: this.pathName,
state: "new",
comment: comment,
datatype: "markdown",
index: this.resourceIndex++
}));
}
};
/**
* @private
* @param {Object} API
* @param {string} text
* @returns {Object} an object containing the leading whitespace, the text,
* and the trailing whitespace
*/
function trim(API, text) {
var i, pre = "", post = "", ret = {};
for (i = 0; i < text.length && API.utils.isWhite(text.charAt(i)); i++);
if (i >= text.length) {
// all white? just return it
return {
pre: text
};
}
if (i > 0) {
ret.pre = text.substring(0, i);
text = text.substring(i);
}
for (i = text.length-1; i > -1 && API.utils.isWhite(text.charAt(i)); i--);
if (i < text.length-1) {
ret.post = text.substring(i+1);
text = text.substring(0, i+1);
}
ret.text = text;
logger.trace('text: pre is "' + ret.pre + '" value is "' + ret.text + '" and post is "' + ret.post + '"');
return ret;
}
// schemes are registered in the IANA list
var reUrl = /^(https?|github|ftps?|mailto|file|data|irc):\/\/[\S]+$/;
/**
* Return true if the given string contains translatable text,
* and false otherwise. For example, a string extracted with only
* punctuation or only an URL in it is not translatable.
*
* @param {string} str the string to test
* @returns {boolean} true if the given string contains translatable text,
* and false otherwise.
*/
MarkdownFile.prototype.isTranslatable = function(str) {
if (!str || !str.length || !str.trim().length) return false;
reUrl.startIndex = 0;
var match = reUrl.exec(str);
if (match && match.length) return false;
return this.API.utils.containsActualText(str);
}
/**
* @param {boolean} escape true if you want the translated
* text to be escaped for attribute values
* @private
*/
MarkdownFile.prototype._emitText = function(escape) {
if (!this.message.getTextLength()) return;
var text = this.message.getMinimalString();
logger.trace('text using message accumulator is: ' + text);
if (this.isTranslatable(text)) {
this._addTransUnit(text, this.comment);
var prefixes = this.message.getPrefix();
var suffixes = this.message.getSuffix();
prefixes.concat(suffixes).forEach(function(end) {
if (typeof(end) === "object") {
end.localizable = false;
}
});
}
this.comment = undefined;
this.message = new MessageAccumulator();
};
var reAttrNameAndValue = /(\w+)\s*=\s*('((\\'|[^'])*)'|"((\\"|[^"])*)")/g;
/**
* @private
*/
MarkdownFile.prototype._findAttributes = function(tagName, tag) {
var match, name;
var ret = [];
// If this is a multiline HTML tag, the parser does not split it for us.
// It comes as one big ole HTML tag with the open, body, and close all as
// one. As such, we should only search the initial open tag for translatable
// attributes.
if (tag.indexOf('\n') > -1) {
reWholeTag.lastIndex = 0;
var match = reWholeTag.exec(tag);
if (match) {
tag = match[0];
}
}
reAttrNameAndValue.lastIndex = 0;
while ((match = reAttrNameAndValue.exec(tag)) !== null) {
var name = match[1],
value = (match[3] && match[3].trim()) || (match[5] && match[5].trim()) || "";
if (value && name === "title" || (this.API.utils.localizableAttributes[tagName] && this.API.utils.localizableAttributes[tagName][name])) {
this._addTransUnit(value);
}
}
return ret;
}
/**
* @private
*/
MarkdownFile.prototype._localizeAttributes = function(tagName, tag, locale, translations) {
var match, name;
var ret = "<" + tagName;
var rest = "";
var attributes = {};
// If this is a multiline HTML tag, the parser does not split it for us.
// It comes as one big ole HTML tag with the open, body, and close all as
// one. As such, we should only search the initial open tag for translatable
// attributes.
if (tag.indexOf('\n') > -1) {
reWholeTag.lastIndex = 0;
var match = reWholeTag.exec(tag);
if (match) {
rest = tag.substring(match.index + match[0].length);
tag = match[0];
}
}
reAttrNameAndValue.lastIndex = 0;
while ((match = reAttrNameAndValue.exec(tag)) !== null) {
var name = match[1],
value = (match[3] && match[3].trim()) || (match[5] && match[5].trim());
if (value && name === "title" || (this.API.utils.localizableAttributes[tagName] && this.API.utils.localizableAttributes[tagName][name])) {
var translation = this._localizeString(value, locale, translations);
attributes[name] = translation || value;
} else {
attributes[name] = value;
}
}
for (var attr in attributes) {
ret += " " + attr + '="' + attributes[attr] + '"';
}
ret += '>' + rest;
return ret;
}
var reTagName = /<(\/?)\s*(\w+)(\s|>)/;
var reL10NComment = /<!--\s*[iI]18[Nn]\s*(.*)\s*-->/;
/**
* @private
* Walk the tree looking for localizable text.
* @param {AST} node the current node of an abstract syntax tree to
* walk.
*/
MarkdownFile.prototype._walk = function(node) {
var match, valid;
switch (node.type) {
case 'text':
var parts = trim(this.API, node.value);
// only localizable if there already is some localizable text
// or if this text contains anything that is not whitespace
if (this.message.getTextLength() > 0 || parts.text) {
this.message.addText(node.value);
node.localizable = true;
}
break;
case 'delete':
case 'link':
case 'emphasis':
case 'strong':
// var thisComponent = this.componentIndex++;
// this.text += '<c' + thisComponent + '>';
node.title && this._addTransUnit(node.title);
if (node.children && node.children.length) {
this.message.push({
name: node.type,
node: node
});
valid = true;
node.children.forEach(function(child) {
if (child.type === 'linkReference') {
if (child.identifier.substring(0, 6) === "block:") {
this._emitText();
valid = false;
} else if (child.identifier.substring(0, 6) === "/block") {
valid = true;
return;
}
}
if (valid) {
this._walk(child);
}
}.bind(this));
this.message.pop();
node.localizable = node.children.every(function(child) {
return child.localizable;
});
}
break;
case 'image':
case 'imageReference':
node.title && this._addTransUnit(node.title);
node.alt && this._addTransUnit(node.alt);
// images are non-breaking, self-closing nodes
// this.text += '<c' + this.componentIndex++ + '/>';
if (this.message.getTextLength()) {
node.localizable = true;
this.message.push(node);
this.message.pop();
}
break;
case 'definition':
node.label && this._addTransUnit(node.label);
node.title && this._addTransUnit(node.title);
// definitions are breaking nodes
this._emitText();
break;
case 'footnoteDefinition':
node.label && this._addTransUnit(node.label);
// definitions are breaking nodes
this._emitText();
break;
case 'linkReference':
case 'inlineCode':
// inline code nodes and link references are non-breaking and self-closing
// Also, pass true to the push so that this node is never optimized out of a string.
if (this.message.getTextLength()) {
node.localizable = true;
this.message.push(node, true);
this.message.pop();
}
break;
case 'html':
reTagName.lastIndex = 0;
if (node.value.substring(0, 4) === '<!--') {
reL10NComment.lastIndex = 0;
match = reL10NComment.exec(node.value);
if (match) {
this.comment = match[1].trim();
}
// ignore HTML comments
break;
}
match = reTagName.exec(node.value);
if (match) {
var tagName = match[2];
if (match[1] !== '/') {
// opening tag
if (this.message.getTextLength()) {
if (this.API.utils.nonBreakingTags[tagName]) {
this.message.push({
name: tagName,
node: node
});
node.localizable = true;
} else {
// it's a breaking tag, so emit any text
// we have accumulated so far
this._emitText();
}
}
this._findAttributes(tagName, node.value);
} else {
// closing tag
if (this.message.getTextLength()) {
if (this.API.utils.nonBreakingTags[tagName] && this.message.getCurrentLevel() > 0) {
var tag = this.message.pop();
while (tag.name !== tagName && this.message.getCurrentLevel() > 0) {
tag = this.message.pop();
}
if (tag.name !== tagName) {
throw new Error("Syntax error in markdown file " + this.pathName + " line " +
node.position.start.line + " column " + node.position.start.column + ". Unbalanced HTML tags.");
}
node.localizable = true;
} else {
this._emitText();
}
}
}
} else {
throw new Error("Syntax error in markdown file " + this.pathName + " line " +
node.position.start.line + " column " + node.position.start.column + ". Bad HTML tag.");
}
break;
default:
this._emitText();
if (node.children && node.children.length) {
valid = true;
node.children.forEach(function(child, index, array) {
if (child.type === 'linkReference') {
if (child.identifier.substring(0, 6) === "block:") {
this._emitText();
valid = false;
} else if (child.identifier.substring(0, 6) === "/block") {
valid = true;
return;
} else if (child.children &&
child.children.length &&
child.children[0].type === "text") {
// Don't need to localize the text nodes inside of linkReferences
// because that text should correspond with the text in the
// references below. So just avoid it and recreate it later during
// the localization step if necessary
child.children = [];
}
} else if (child.type === 'thematicBreak') {
if (index+1 < array.length && array[index+1]) {
var p = array[index+1];
if (p.type === 'paragraph' &&
p.children &&
p.children.length > 0 &&
p.children[0].type === "text" &&
p.children[0].value.substring(0, 6) === "title:") {
valid = false;
} else {
valid = true;
}
} else {
valid = true;
}
}
if (valid) {
this._walk(child);
}
}.bind(this));
}
break;
}
};
/**
* Parse the data string looking for the localizable strings and add them to the
* project's translation set.
* @param {String} data the string to parse
*/
MarkdownFile.prototype.parse = function(data) {
logger.debug("Extracting strings from " + this.pathName);
// massage the broken headers and code blocks a bit first so that the parser
// works as expected
data = data.
replace(/\[block:/g, "```\n[block:").
replace(/\[\/block\]/g, "[/block]\n```").
replace(/(^|\n)(#+)([^#\s])/g, "\n$2 $3");
this.ast = mdparser.parse(data);
// accumulates characters in text segments
// this.text = "";
this.message = new MessageAccumulator();
this.resourceIndex = 0;
this._walk(this.ast);
// in case any is left over at the end
this._emitText();
};
/**
* Extract all the localizable strings from the md file and add them to the
* project's translation set.
*/
MarkdownFile.prototype.extract = function() {
logger.debug("Extracting strings from " + this.pathName);
if (this.pathName) {
var p = path.join(this.project.root, this.pathName);
try {
var data = fs.readFileSync(p, "utf8");
if (data) {
this.parse(data);
}
} catch (e) {
logger.warn("Could not read file: " + p);
logger.warn(e);
}
}
};
/**
* Return the set of resources found in the current Java file.
*
* @returns {TranslationSet} The set of resources found in the
* current Java file.
*/
MarkdownFile.prototype.getTranslationSet = function() {
return this.set;
};
//we don't write Markdown source files
MarkdownFile.prototype.write = function() {};
/**
* Return the location on disk where the version of this file localized
* for the given locale should be written.
* @param {String] locale the locale spec for the target locale
* @returns {String} the localized path name
*/
MarkdownFile.prototype.getLocalizedPath = function(locale) {
var pathName = this.pathName.
replace("\/" + this.project.sourceLocale + "\/", "/" + locale + "/").
replace(new RegExp("^" + this.project.sourceLocale + "\/"), locale + "/");
if (pathName !== this.pathName) return pathName;
// if we haven't replaced a source locale path component with the current locale
// then add it to the beginning of the path.
return path.join(locale, this.pathName);
};
/**
* @private
*/
MarkdownFile.prototype._localizeString = function(source, locale, translations) {
if (!source) return source;
var key = this.makeKey(this.API.utils.escapeInvalidChars(source));
var tester = this.API.newResource({
type: "string",
project: this.project.getProjectId(),
sourceLocale: this.project.getSourceLocale(),
reskey: key,
datatype: "markdown"
});
// var hashkey = ResourceString.hashKey(this.project.getProjectId(), locale, key, "markdown");
var hashkey = tester.hashKeyForTranslation(locale);
var translatedResource = translations.get(hashkey);
var translation;
if (locale === this.project.pseudoLocale && this.project.settings.nopseudo) {
translation = source;
} else if (translatedResource) {
translation = translatedResource.getTarget();
} else if (this.type) {
if (source && this.type.pseudos && this.type.pseudos[locale]) {
var sourceLocale = this.type.pseudos[locale].getPseudoSourceLocale();
if (sourceLocale !== this.project.sourceLocale) {
// translation is derived from a different locale's translation instead of from the source string
var sourceRes = translations.get(tester.hashKeyForTranslation(sourceLocale));
source = sourceRes ? sourceRes.getTarget() : source;
}
translation = this.type.pseudos[locale].getString(source);
} else {
logger.trace("New string found: " + source);
this.type.newres.add(this.API.newResource({
resType: "string",
project: this.project.getProjectId(),
key: key,
sourceLocale: this.project.sourceLocale,
source: this.API.utils.escapeInvalidChars(source),
targetLocale: locale,
target: this.API.utils.escapeInvalidChars(source),
autoKey: true,
pathName: this.pathName,
state: "new",
datatype: "markdown",
index: this.resourceIndex++
}));
translation = source;
if (this.type.missingPseudo && !this.project.settings.nopseudo) {
translation = this.type.missingPseudo.getString(source);
}
}
} else {
translation = source;
}
return translation;
};
/**
* @private
*/
MarkdownFile.prototype._localizeNode = function(node, message, locale, translations) {
var match, valid, translation;
switch (node.type) {
case 'text':
var parts = trim(this.API, node.value);
// only localizable if there already is some localizable text
// or if this text contains anything that is not whitespace
if (node.localizable || parts.text) {
message.addText(node.value);
}
break;
case 'delete':
case 'link':
case 'emphasis':
case 'strong':
if (node.title) {
node.title = this._localizeString(node.title, locale, translations);
}
if (node.localizable) {
if (node.use === "start") {
message.push(node);
} else {
message.pop();
}
}
break;
case 'image':
case 'imageReference':
if (node.title) {
node.title = this._localizeString(node.title, locale, translations);
}
if (node.alt) {
node.alt = this._localizeString(node.alt, locale, translations);
}
// images are non-breaking, self-closing nodes
if (node.localizable) {
message.push(node);
message.pop();
}
break;
case 'linkReference':
if (node.localizable) {
node.add(new Node({
type: 'text',
value: node.label
}));
message.push(node, true);
message.pop();
}
break;
case 'inlineCode':
// inline code is a non-breaking, self-closing node
if (node.localizable) {
message.push(node, true);
message.pop();
}
break;
case 'definition':
if (node.label) {
node.label = this._localizeString(node.label, locale, translations);
}
if (node.title) {
node.title = this._localizeString(node.title, locale, translations);
}
if (node.localizable) {
if (node.use === "start") {
message.push(node);
} else {
message.pop();
}
}
break;
case 'footnoteDefinition':
if (node.label) {
node.label = this._localizeString(node.label, locale, translations);
}
if (node.localizable) {
if (node.use === "start") {
message.push(node);
} else {
message.pop();
}
}
break;
case 'html':
reTagName.lastIndex = 0;
if (node.value.substring(0, 4) === '<!--') {
reL10NComment.lastIndex = 0;
match = reL10NComment.exec(node.value);
if (match) {
this.comment = match[1].trim();
}
// ignore HTML comments
break;
}
match = reTagName.exec(node.value);
if (match) {
var tagName = match[2];
if (match[1] !== '/') {
// opening tag
node.value = this._localizeAttributes(tagName, node.value, locale, translations);
if (node.localizable && this.API.utils.nonBreakingTags[tagName]) {
node.name = tagName;
message.push(node);
}
} else {
// closing tag
if (node.localizable && this.API.utils.nonBreakingTags[tagName] && message.getCurrentLevel() > 0) {
var tag = message.pop();
while (tag.name !== tagName && message.getCurrentLevel() > 0) {
tag = message.pop();
}
if (tag.name !== tagName) {
throw new Error("Syntax error in markdown file " + this.pathName + " line " +
node.position.start.line + " column " + node.position.start.column + ". Unbalanced HTML tags.");
}
}
}
} else {
throw new Error("Syntax error in markdown file " + this.pathName + " line " +
node.position.start.line + " column " + node.position.start.column + ". Bad HTML tag.");
}
break;
default:
break;
}
};
function flattenHtml(node) {
var ret = [];
if (node.type === "html") {
var children = node.children;
node.children = undefined;
ret.push(node);
if (children && children.length) {
for (var i = 0; i < children.length; i++) {
ret = ret.concat(flattenHtml(children[i]));
}
ret.push({
type: "html",
value: '</' + node.name + '>'
});
}
} else {
ret.push(node);
}
return ret;
}
function mapToAst(node) {
var children = [];
if (node.type === "linkReference") {
node.add(new Node({
type: 'text',
value: node.label
}));
}
for (var i = 0; i < node.children.length; i++) {
var child = mapToAst(node.children[i]);
if (child.type === "html") {
// flatten any HTML
children = children.concat(flattenHtml(child));
} else {
children.push(child);
}
}
if (node.type === "code" && node.value.startsWith("[block:")) {
node.type = "html";
}
if (node.extra) {
if (children.length) {
node.extra.children = node.extra.children ? node.extra.children.concat(children) : children;
}
return node.extra;
}
return u(node.type, node, children);
}
MarkdownFile.prototype._getTranslationNodes = function(locale, translations, ma) {
if (ma.getTextLength() === 0) {
// nothing to localize
return undefined;
}
var text = ma.getMinimalString();
var key = this.makeKey(this.API.utils.escapeInvalidChars(text));
var translation = this._localizeString(text, locale, translations);
if (translation) {
var transMa = MessageAccumulator.create(translation, ma);
var nodes = transMa.root.toArray();
// don't return the "root" start and end nodes
nodes = nodes.slice(1, -1);
// warn about components in the target that don't exist in the source,
// and remove them from the node array as if they didn't exist
var maxIndex = Object.keys(ma.getMapping()).length;
var mismatch = false;
for (i = 0; i < nodes.length; i++) {
if (nodes[i].type == 'component' && (!nodes[i].extra || nodes[i].index > maxIndex)) {
nodes.splice(i, 1);
mismatch = true;
}
}
if (mismatch) {
logger.warn("Warning! Translation of\n'" + text + "' (key: " + key + ")\nto locale " + locale + " is\n'" + translation + "'\nwhich has a more components in it than the source.");
}
if (this.project.settings.identify) {
var tmp = [];
tmp.push(new Node({
type: "html",
use: "start",
name: "span",
extra: u("html", {
value: '<span x-locid="' + key + '">',
name: "span"
})
}));
tmp = tmp.concat(nodes);
tmp.push(new Node({
type: "html",
use: "end",
name: "span",
extra: u("html", {
value: '</span>',
name: "span"
})
}));
nodes = tmp;
}
return nodes;
}
return undefined;
};
function mapToNodes(astNode) {
var node = new Node(astNode);
if (astNode.children) {
for (var i = 0; i < astNode.children.length; i++) {
node.add(mapToNodes(astNode.children[i]));
}
}
return node;
}
/**
* Localize the text of the current file to the given locale and return
* the results.
*
* @param {TranslationSet} translations the current set of translations
* @param {String} locale the locale to translate to
* @returns {String} the localized text of this file
*/
MarkdownFile.prototype.localizeText = function(translations, locale) {
var output = "";
this.resourceIndex = 0;
logger.debug("Localizing strings for locale " + locale);
// copy the ast for this locale so that we don't modify the original
var ast = unistFilter(this.ast, function(node) {
return true;
});
// flatten the tree into an array and then walk the array finding
// localizable segments that will get replaced with the translation
var nodeArray = mapToNodes(ast).toArray();
var valid = true, start = -1, end, ma = new MessageAccumulator();
for (var i = 0; i < nodeArray.length; i++) {
if (nodeArray[i].type === 'linkReference') {
if (nodeArray[i].identifier.substring(0, 6) === "block:") {
valid = false;
} else if (nodeArray[i].identifier.substring(0, 6) === "/block" && nodeArray[i].use === "end") {
valid = true;
}
} else if (nodeArray[i].type === 'thematicBreak') {
if (i+2 < nodeArray.length && nodeArray[i+1] && nodeArray[i+2]) {
if (nodeArray[i+1].type === 'paragraph' &&
nodeArray[i+2].type === "text" &&
nodeArray[i+2].value.substring(0, 6) === "title:") {
// glue all the subsequent contiguous text nodes together as one
var tmpStr = "";
for (var j = i+2; j < nodeArray.length && nodeArray[j].type === "text"; j++) {
tmpStr += nodeArray[j].value;
}
nodeArray[i+2].value = escapeQuotes(tmpStr);
nodeArray.splice(i+2, j-i-2, nodeArray[i+2]);
valid = false;
} else {
valid = true;
}
} else {
valid = true;
}
}
if (valid) this._localizeNode(nodeArray[i], ma, locale, translations);
if (valid && nodeArray[i].localizable) {
if (start < 0) {
start = i;
}
end = i;
} else if (start > -1) {
if (this.isTranslatable(ma.getMinimalString())) {
var nodes = this._getTranslationNodes(locale, translations, ma);
if (nodes) {
// replace the source nodes with the translation nodes
var prefix = ma.getPrefix();
var suffix = ma.getSuffix();
nodeArray = nodeArray.slice(0, start).concat(prefix).concat(nodes).concat(suffix).concat(nodeArray.slice(end+1));
// adjust for the difference in node length of the source and translation
i += nodes.length - (end - start) - 1;
} // else leave the source nodes alone and register this string as new
}
start = -1;
ma = new MessageAccumulator();
}
if (!valid && nodeArray[i] && nodeArray[i].value) nodeArray[i].value = escapeQuotes(nodeArray[i].value);
}
// in case any is left over at the end
if (start > -1 && ma.getTextLength()) {
var nodes = this._getTranslationNodes(locale, translations, ma);
if (nodes) {
// replace the last few source nodes with the translation nodes
var prefix = ma.getPrefix();
var suffix = ma.getSuffix();
nodeArray = nodeArray.slice(0, start).concat(prefix).concat(nodes).concat(suffix).concat(nodeArray.slice(end+1));
} // else leave the source nodes alone
}
// convert to a tree again
ast = mapToAst(Node.fromArray(nodeArray));
var str = mdstringify.stringify(ast);
// make sure the thematic breaks don't have blank lines after them and they
// don't erroneously escape the backslash chars
str = str.
replace(/---\n\n/g, "---\n").
replace(/\n\n---/g, "\n---");
return str;
};
/**
* Localize the contents of this Markdown file and write out the
* localized Markdown file to a different file path.
*
* @param {TranslationSet} translations the current set of
* translations
* @param {Array.<String>} locales array of locales to translate to
*/
MarkdownFile.prototype.localize = function(translations, locales) {
for (var i = 0; i < locales.length; i++) {
if (!this.project.isSourceLocale(locales[i])) {
// skip variants for now until we can handle them properly
var l = new Locale(locales[i]);
if (!l.getVariant()) {
var pathName = this.getLocalizedPath(locales[i]);
logger.debug("Writing file " + pathName);
var p = path.join(this.project.target, pathName);
var d = path.dirname(p);
this.API.utils.makeDirs(d);
fs.writeFileSync(p, this.localizeText(translations, locales[i]), "utf-8");
}
}
}
};
module.exports = MarkdownFile;