docutils-ts
Version:
Port of the Python Docutils library to TypeScript
1,396 lines (1,344 loc) • 74.2 kB
JavaScript
import { compile } from 'ejs';
import * as constants from '../constants.js';
import BaseWriter from '../writer.js';
import * as nodes from '../nodes.js';
import * as utils from '../utils.js';
import { basename } from '../utils/paths.js';
import { UnimplementedError } from '../exceptions.js';
import { getLanguage } from "../languages/index.js";
import { logger as baseLogger } from '../logger.js';
const logger = baseLogger.child({ 'class': 'HtmlBase' });
const __version__ = '';
const defaultTemplate = `<%- headPrefix %>
<%- head %>
<%- stylesheet %>
<%- bodyPrefix %>
<%- bodyPreDocinfo %>
<%- docinfo %>
<%- body %>
<%- bodySuffix %>
`;
const template = compile(defaultTemplate, {});
/**
* Raise `nodes.NodeFound` if non-simple list item is encountered.
*
* Here "simple" means a list item containing nothing other than a single
* paragraph, a simple list, or a paragraph followed by a simple list.
*
* This version also checks for simple field lists and docinfo.
*/
class SimpleListChecker extends nodes.GenericNodeVisitor {
default_visit(node) {
super.default_visit(node);
}
default_departure(node) {
super.default_departure(node);
}
}
// def default_visit(self, node):
// raise nodes.NodeFound
//
// def visit_list_item(self, node):
// # print "visiting list item", node.__class__
// children = [child for child in node.children
// if not isinstance(child, nodes.Invisible)]
// # print "has %s visible children" % len(children)
// if (children and isinstance(children[0], nodes.paragraph)
// and (isinstance(children[-1], nodes.bullet_list) or
// isinstance(children[-1], nodes.enumerated_list) or
// isinstance(children[-1], nodes.field_list))):
// children.pop()
// # print "%s children remain" % len(children)
// if len(children) <= 1:
// return
// else:
// # print "found", child.__class__, "in", node.__class__
// raise nodes.NodeFound
//
// def pass_node(self, node):
// pass
//
// def ignore_node(self, node):
// # ignore nodes that are never complex (can contain only inline nodes)
// raise nodes.SkipNode
//
// # Paragraphs and text
// visit_Text = ignore_node
// visit_paragraph = ignore_node
//
// # Lists
// visit_bullet_list = pass_node
// visit_enumerated_list = pass_node
// visit_docinfo = pass_node
//
// # Docinfo nodes:
// visit_author = ignore_node
// visit_authors = visit_list_item
// visit_address = visit_list_item
// visit_contact = pass_node
// visit_copyright = ignore_node
// visit_date = ignore_node
// visit_organization = ignore_node
// visit_status = ignore_node
// visit_version = visit_list_item
//
// # Definition list:
// visit_definition_list = pass_node
// visit_definition_list_item = pass_node
// visit_term = ignore_node
// visit_classifier = pass_node
// visit_definition = visit_list_item
//
// # Field list:
// visit_field_list = pass_node
// visit_field = pass_node
// # the field body corresponds to a list item
// visit_field_body = visit_list_item
// visit_field_name = ignore_node
//
// # Invisible nodes should be ignored.
// visit_comment = ignore_node
// visit_substitution_definition = ignore_node
// visit_target = ignore_node
/**
* HTMLTranslator class
*/
class HTMLTranslator extends nodes.NodeVisitor {
constructor(document) {
super(document);
this.xmlDeclaration = compile('<?xml version="1.0" encoding="<%=encoding%>" ?>\n');
this.doctype = '<!DOCTYPE html>\n';
this.doctypeMathML = this.doctype;
this.headPrefixTemplate = compile('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="<%=lang%>" lang="<%=lang%>">\n<head>\n');
this.contentType = compile('<meta charset="<%=charset%>"/>\n');
this.generator = compile('<meta name="generator" content="Docutils <%=version%>: http://docutils.sourceforge.net/" />\n');
this.context = [];
this.initialHeaderLevel = -1;
this.langAttribute = 'lang';
this.mathOutput = '';
this.mathOutputOptions = [];
this.tableStyle = '';
this.attribution = '';
this.cloakEmailAddresses = true;
this.settings = document.settings;
const settings = this.settings;
const langCode = settings.languageCode;
if (langCode !== undefined) {
this.language = getLanguage(langCode, document.reporter);
}
else {
this.language = getLanguage('en', document.reporter);
}
this.meta = [this.generator({ version: constants.__version__ })];
this.headPrefix = [];
this.htmlProlog = [];
let myConfig = settings;
if (myConfig !== undefined) {
this.cloakEmailAddresses = myConfig.cloakEmailAddresses || true;
if (myConfig.attribution !== undefined) {
this.attribution = myConfig.attribution;
}
if (myConfig.tableStyle !== undefined) {
this.tableStyle = myConfig.tableStyle;
}
}
if (myConfig && myConfig.xmlDeclaration) {
this.headPrefix.push(this.xmlDeclaration({ encoding: settings.outputEncoding }));
}
this.head = this.meta.slice();
this.stylesheet = [];
/* fixme
this.stylesheet = utils.getStylesheetList(settings).
map(this.stylesheetCall.bind(this));
*/
this.bodyPrefix = ['</head>\n<body>\n'];
this.bodyPreDocinfo = [];
this.docinfo = [];
this.body = [];
this.fragment = [];
this.bodySuffix = ['</body>\n</html>\n'];
this.sectionLevel = 0;
if (myConfig) {
if (myConfig.initialHeaderLevel !== undefined) {
this.initialHeaderLevel = myConfig.initialHeaderLevel;
}
const mathOutput = utils.pySplit(myConfig.mathOutput || '');
this.mathOutputOptions = mathOutput.slice(1, this.mathOutput.length - 1);
this.mathOutput = mathOutput[0].toLowerCase();
}
this.context = [];
this.topicClasses = [];
this.colspecs = [];
this.compactParagraph = true;
this.compactSimple = false;
this.compactFieldList = false;
this.inDocinfo = false;
this.inSidebar = false;
this.inFootnoteList = false;
this.title = [];
this.subtitle = [];
this.header = [];
this.footer = [];
this.htmlHead = [ /*this.contentType*/];
this.htmlTitle = [];
this.htmlSubtitle = [];
this.htmlBody = [];
this.inDocumentTitle = 0;
this.inMailto = false;
this.authorInAuthors = false;
this.mathHeader = [];
}
astext() {
return [this.headPrefix, this.head, this.stylesheet, this.bodyPrefix].map((a) => a.join('')).join('');
}
encode(text) {
return text; // fixme
}
cloakMailto(href) {
return href;
}
cloakEmail(encoded) {
return encoded;
}
/**
* Cleanse, HTML encode, and return attribute value text.
* @param {String} text - text to cleanse
* @param {RegExp} whitespace - regexp for matching whitespace.
*/
attVal(text, whitespace = /[\n\r\t\v\f]/g) {
if (!text) {
text = '';
}
let encoded = this.encode(text.replace(whitespace, ' '));
if (this.inMailto && this.cloakEmailAddresses) {
// Cloak at-signs ("%40") and periods with HTML entities.
encoded = encoded.replace('%40', '%40');
encoded = encoded.replace('.', '.');
}
return encoded;
}
stylesheetCall(path) {
return '';
}
/*
* Construct and return a start tag given a node (id & class attributes
* are extracted), tag name, and optional attributes.
*/
starttag(node, tagname = '', suffix = '\n', empty = false, attributes = {}) {
if (typeof suffix !== 'string') {
throw new Error('suffix should be a string!!');
}
const myTagname = tagname.toLowerCase();
const prefix = [];
const atts = {};
const ids = [];
Object.entries(attributes).forEach(([name, value]) => {
atts[name.toLowerCase()] = value;
});
const classes = [];
const languages = [];
// unify class arguments and move language specification
const c = (node.attributes && node.attributes.classes) || [];
c.splice(c.length - 1, 0, ...utils.pySplit(atts.class || ''));
c.forEach((cls) => {
if (cls.substring(0, 8) === 'language-') {
languages.splice(0, 0, cls.substring(9));
}
else if (cls.trim() && classes.indexOf(cls) === -1) {
classes.push(cls);
}
});
if (languages.length) {
// attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
atts[this.langAttribute] = languages[0];
}
if (classes.length) {
atts.class = classes.join(' ');
}
// assert 'id' not in atts
ids.push(...(node.attributes.ids || []));
/* if 'ids' in atts:
ids.extend(atts['ids'])
del atts['ids']
if ids:
atts['id'] = ids[0]
for id in ids[1:]:
// Add empty "span" elements for additional IDs. Note
// that we cannot use empty "a" elements because there
// may be targets inside of references, but nested "a"
// elements aren't allowed in XHTML (even if they do
// not all have a "href" attribute).
if empty or isinstance(node,
(nodes.bullet_list, nodes.docinfo,
nodes.definition_list, nodes.enumerated_list,
nodes.field_list, nodes.option_list,
nodes.table)):
// Insert target right in front of element.
prefix.push('<span id="%s"></span>' % id)
else:
// Non-empty tag. Place the auxiliary <span> tag
// *inside* the element, as the first child.
suffix += '<span id="%s"></span>' % id
*/
const attlist = { ...atts };
// attlist.sort()
const parts = [myTagname];
Object.keys(attlist).forEach((name) => {
const value = attlist[name];
// value=None was used for boolean attributes without
// value, but this isn't supported by XHTML.
// assert value is not None
if (Array.isArray(value)) {
parts.push(`${name.toLowerCase()}="${this.attVal(value.join(' '))}"`);
}
else {
parts.push(`${name.toLowerCase()}="${this.attVal(value.toString())}"`);
}
});
const infix = empty ? ' /' : '';
// return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
const result = `${prefix.join('')}<${parts.join(' ')}${infix}>${suffix}`;
const badStr = '[object Object]';
if (result.indexOf(badStr) !== -1) {
let invalidVar = '';
if (prefix.join('').indexOf('[object Object]') !== -1) {
invalidVar = 'prefix';
}
else if (parts.join(' ').indexOf(badStr) !== -1) {
// implement fixme
}
else if (infix.indexOf(badStr) !== -1) {
// implement fixme
}
else if (suffix.indexOf(badStr) !== -1) {
invalidVar = 'suffix';
}
throw new Error(`invalid object in ${invalidVar} thing ${prefix} ${parts} ${infix}`);
}
return result;
}
/**
* Construct and return an XML-compatible empty tag.
*/
emptytag(node, tagname, suffix = '\n', attributes = {}) {
return this.starttag(node, tagname, suffix, true, attributes);
}
setClassOnChild(node, class_, index = 0) {
}
visit_Text(node) {
const text = node.astext();
let encoded = this.encode(text);
if (this.inMailto && this.cloakEmailAddresses) {
encoded = this.cloakEmail(encoded);
}
this.body.push(encoded);
}
depart_Text(node) {
}
visit_section(node) {
this.sectionLevel += 1;
this.body.push(this.starttag(node, 'div', '\n', false, { CLASS: 'section' }));
}
depart_section(node) {
this.sectionLevel -= 1;
this.body.push('</div>\n');
}
visit_abbreviation(node) {
this.body.push(this.starttag(node, 'abbr', ''));
}
depart_abbreviation(node) {
this.body.push('</abbr>');
}
visit_acronym(node) {
this.body.push(this.starttag(node, 'acronym', ''));
}
depart_acronym(node) {
this.body.push('</acronym>');
}
/*
visit_address(node: NodeInterface): void {
this.visitDocinfoItem(node, 'address', meta = false);
this.body.push(this.starttag(node, 'pre',
suffix = '', false, { CLASS: 'address' }));
}
depart_address(node: NodeInterface): void {
this.body.push('\n</pre>\n');
this.departDocinfoItem();
} */
visit_admonition(node) {
node.attributes.classes.splice(0, 0, 'admonition');
this.body.push(this.starttag(node, 'div'));
}
depart_admonition(node) {
this.body.push('</div>\n');
const attributionFormats = {
dash: ['\u2014', ''],
parentheses: ['(', ')'],
parens: ['(', ')'],
none: ['', ''],
};
}
visit_attribution(node) {
const [prefix, suffix] = this.attributionFormats[this.attribution];
this.context.push(suffix);
this.body.push(this.starttag(node, 'p', prefix, false, { CLASS: 'attribution' }));
}
depart_attribution(node) {
this.body.push(`${this.context.pop()}</p>\n`);
}
visit_author(node) {
if (!(node.parent instanceof nodes.authors)) {
this.visitDocinfoItem(node, 'author');
}
this.body.push('<p>');
}
depart_author(node) {
this.body.push('</p>');
if (!(node.parent instanceof nodes.authors)) {
this.body.push('\n');
}
else {
this.departDocinfoItem();
}
}
visit_authors(node) {
this.visitDocinfoItem(node, 'authors');
}
depart_authors(node) {
this.departDocinfoItem();
}
visit_block_quote(node) {
this.body.push(this.starttag(node, 'blockquote'));
}
depart_block_quote(node) {
this.body.push('</blockquote>\n');
}
/*
* Check for a simple list that can be rendered compactly.
*/
/*
checkSimpleList(node: NodeInterface): void {
const visitor = new SimpleListChecker(this.document);
try {
node.walk(visitor);
} catch (error) {
if (error instanceof nodes.NodeFound) {
return false;
}
throw error;
}
return true;
// Compact lists
// ------------
// Include definition lists and field lists (in addition to ordered
// and unordered lists) in the test if a list is "simple" (cf. the
}
// html4css1.HTMLTranslator docstring and the SimpleListChecker class at
// the end of this file).
*/
isCompactable(node) {
// print "isCompactable %s ?" % node.__class__,
// explicite class arguments have precedence
if ('compact' in node.attributes.classes) {
return true;
}
if ('open' in node.attributes.classes) {
return false;
}
// check config setting:
if ((node instanceof nodes.field_list
|| node instanceof nodes.definition_list)
&& !this.compactFieldLists) {
// print "`compact-field-lists` is false"
return false;
}
if ((node instanceof nodes.enumerated_list
|| node instanceof nodes.bullet_list)
&& !this.compactLists) {
// print "`compact-lists` is false"
return false;
}
// more special cases:
if ((this.topicClasses.length === 1
&& this.topicClasses[0] === 'contents')) { // TODO: this.in_contents
return true;
}
// check the list items:
return this.checkSimpleList(node);
}
visit_caption(node) {
this.body.push(this.starttag(node, 'p', '', false, { CLASS: 'caption' }));
}
depart_caption(node) {
this.body.push('</p>\n');
}
// citations
// ---------
// Use definition list instead of table for bibliographic references.
// Join adjacent citation entries.
visit_citation(node) {
if (!this.inFootnoteList) {
this.body.push('<dl class="citation">\n');
}
this.inFootnoteList = true;
}
depart_citation(node) {
this.body.push('</dd>\n');
if (!(node.nextNode({ descend: false, siblings: true }) instanceof nodes.citation)) {
this.body.push('</dl>\n');
this.inFootnoteList = false;
}
}
visit_citation_reference(node) {
let href = '#';
if ('refid' in node.attributes) {
href += node.attributes.refid;
}
else if ('refame' in node.attributes) {
href += this.document.nameIds[node.attributes.refname];
}
// else: // TODO system message (or already in the transform)?
// 'Citation reference missing.'
this.body.push(this.starttag(node, 'a', '[', false, { CLASS: 'citation-reference', href }));
}
depart_citation_reference(node) {
this.body.push(']</a>');
// classifier
// ----------
// don't insert classifier-delimiter here (done by CSS)
}
visit_classifier(node) {
this.body.push(this.starttag(node, 'span', '', false, { CLASS: 'classifier' }));
}
depart_classifier(node) {
this.body.push('</span>');
}
visit_colspec(node) {
this.colspecs.push(node);
// "stubs" list is an attribute of the tgroup element:
//node.parent!.stubs.push(node.attributes.stub); fixme
}
depart_colspec(node) {
// write out <colgroup> when all colspecs are processed
if (node.nextNode({ descend: false, siblings: true }) instanceof nodes.colspec) {
return;
}
if ('colwidths-auto' in node.parent.parent.attributes.classes ||
(this.settings.tableStyle &&
this.settings.tableStyle.includes('colwidths-auto') &&
(!('colwidths-given' in node.parent.parent.attributes.classes)))) {
return;
}
const totalWidth = this.colspecs.map((subNode) => parseInt(subNode.attributes.colwidth, 10))
.reduce((a, c) => a + c);
this.body.push(this.starttag(node, 'colgroup'));
this.colspecs.forEach((subNode) => {
const colWidth = parseInt(subNode.attributes.colwidth, 10) * 100.0 / totalWidth + 0.5;
this.body.push(this.emptytag(subNode, 'col', '', { style: `width: ${colWidth}` }));
});
this.body.push('</colgroup>\n');
}
/*
visit_comment(self, node,
sub=re.compile('-(?=-)').sub):
// Escape double-dashes in comment text.
this.body.push('<!-- %s -->\n' % sub('- ', node.astext()))
// Content already processed:
raise nodes.SkipNode
}
visit_compound(node: NodeInterface): void {
this.body.push(this.starttag(node, 'div', { CLASS: 'compound' }))
if len(node: NodeInterface) > 1:
node.attributes[0]['classes'].push('compound-first')
node.attributes[-1]['classes'].push('compound-last')
for child in node.attributes[1:-1]:
child['classes'].push('compound-middle')
}
depart_compound(node: NodeInterface): void {
this.body.push('</div>\n')
}
*/
visit_container(node) {
this.body.push(this.starttag(node, 'div', '\n', false, { CLASS: 'docutils container' }));
}
depart_container(node) {
this.body.push('</div>\n');
}
visit_contact(node) {
this.visitDocinfoItem(node, 'contact', false);
}
depart_contact(node) {
this.departDocinfoItem();
}
visit_copyright(node) {
this.visitDocinfoItem(node, 'copyright');
}
depart_copyright(node) {
this.departDocinfoItem();
}
visit_date(node) {
this.visitDocinfoItem(node, 'date');
}
depart_date(node) {
this.departDocinfoItem();
}
visit_decoration(node) {
}
depart_decoration(node) {
}
visit_definition(node) {
this.body.push('</dt>\n');
this.body.push(this.starttag(node, 'dd', ''));
}
depart_definition(node) {
this.body.push('</dd>\n');
}
visit_definition_list(node) {
if (node.attributes.classes == null) {
node.attributes.classes = [];
}
const classes = node.attributes.classes;
if (this.isCompactable(node)) {
classes.push('simple');
}
this.body.push(this.starttag(node, 'dl'));
}
depart_definition_list(node) {
this.body.push('</dl>\n');
}
visit_definition_list_item(node) {
// pass class arguments, ids and names to definition term:
node.getChild(0).attributes.classes.splice(0, 0, ...(node.attributes.classes || []));
node.getChild(0).attributes.ids.splice(0, 0, ...(node.attributes.ids || []));
node.getChild(0).attributes.names.splice(0, 0, ...(node.attributes.names || []));
}
depart_definition_list_item(node) {
}
visit_description(node) {
this.body.push(this.starttag(node, 'dd', ''));
}
depart_description(node) {
this.body.push('</dd>\n');
}
visit_docinfo(node) {
this.context.push(this.body.length.toString());
let classes = 'docinfo';
if (this.isCompactable(node)) {
classes += ' simple';
}
this.body.push(this.starttag(node, 'dl', '\n', false, { CLASS: classes }));
}
depart_docinfo(node) {
this.body.push('</dl>\n');
const start = this.context.pop();
this.docinfo = this.body.slice(parseInt(start, 10));
this.body = [];
}
visitDocinfoItem(node, name, meta = true) {
if (meta) {
const metaTag = `<meta name="${name}" content="${this.attVal(node.astext())}" />\n`;
this.addMeta(metaTag);
}
this.body.push(`<dt class="${name}">${this.language.labels[name]}</dt>\n`);
this.body.push(this.starttag(node, 'dd', '', false, { CLASS: name }));
}
departDocinfoItem() {
this.body.push('</dd>\n');
}
visit_doctest_block(node) {
this.body.push(this.starttag(node, 'pre', '', false, { CLASS: 'code javascript doctest' }));
}
depart_doctest_block(node) {
this.body.push('\n</pre>\n');
}
visit_document(node) {
const title = ((node.attributes.title || '') || basename(node.attributes.source) || 'docutils document without title');
this.head.push(`<title>${this.encode(title)}</title>\n`);
}
depart_document(node) {
this.headPrefix.push(this.doctype, this.headPrefixTemplate({ lang: this.settings.languageCode }));
this.meta.splice(0, 0, this.contentType({ charset: this.settings.outputEncoding }));
this.head.splice(0, 0, this.contentType({ charset: this.settings.outputEncoding }));
this.htmlHead.splice(this.htmlHead.length, 0, ...this.head.slice(1));
this.bodyPrefix.push(this.starttag(node, 'div', '\n', false, { CLASS: 'document' }));
this.fragment.push(...this.body);
this.htmlBody.push(...this.bodyPrefix.slice(1), ...this.bodyPreDocinfo, ...this.docinfo, ...this.body, ...this.bodySuffix.slice(0, this.bodySuffix.length - 1));
}
/*
this.headPrefixTemplate %
{'lang': this.settings.language_code}])
this.html_prolog.push(this.doctype)
this.meta.splice(0, 0, this.content_type % this.settings.output_encoding)
this.head.splice(0, 0, this.content_type % this.settings.output_encoding)
if 'name="dcterms.' in ''.join(this.meta):
this.head.push(
'<link rel="schema.dcterms" href="http://purl.org/dc/terms/">')
if this.math_header:
if this.math_output == 'mathjax':
this.head.extend(this.math_header)
else:
this.stylesheet.extend(this.math_header)
// skip content-type meta tag with interpolated charset value:
this.htmlHead.extend(this.head[1:])
this.bodyPrefix.push(this.starttag(node, 'div', { CLASS: 'document' }))
this.bodySuffix.insert(0, '</div>\n')
this.fragment.extend(this.body) // this.fragment is the "naked" body
this.htmlBody.extend(this.bodyPrefix[1:] + this.body_pre_docinfo
+ this.docinfo + this.body
+ this.body_suffix[:-1])
// assert not this.context, 'len(context) = %s' % len(this.context)
}
*/
visit_emphasis(node) {
this.body.push(this.starttag(node, 'em', ''));
}
depart_emphasis(node) {
this.body.push('</em>');
}
visit_entry(node) {
const atts = { class: [] };
if (node.parent.parent instanceof nodes.thead) {
atts.class.push('head');
}
const ggParent = node.parent.parent.parent;
// let stubs: any[] = ggParent.getCustomAttr('stubs');
/* if(stubs[node.parent!.getCustomAttr('column')]) {
// "stubs" list is an attribute of the tgroup element
atts.class.push('stub');
}*/
let tagname;
if (atts.class.length) {
tagname = 'th';
atts.class = (atts.class.join(' '));
}
else {
tagname = 'td';
delete atts.class;
}
node.parent.column += 1;
if ('morerows' in node.attributes) {
atts.rowspan = node.attributes.morerows + 1;
}
if ('morecols' in node.attributes) {
atts.colspan = node.attributes.morecols + 1;
node.parent.column += node.attributes.morecols;
}
this.body.push(this.starttag(node, tagname, '', false, atts));
this.context.push(`</${tagname.toLowerCase()}>\n`);
// TODO: why does the html4css1 writer insert an NBSP into empty cells?
// if len(node: NodeInterface) == 0: // empty cell
// this.body.push('&//0160;') // no-break space
}
depart_entry(node) {
this.body.push(this.context.pop());
}
/*
visit_enumerated_list(node: NodeInterface): void {
atts = {}
if 'start' in node:
atts['start'] = node.attributes['start']
if 'enumtype' in node:
atts['class'] = node.attributes['enumtype']
if this.isCompactable(node: NodeInterface):
atts['class'] = (atts.get('class', '') + ' simple').strip()
this.body.push(this.starttag(node, 'ol', **atts))
}
depart_enumerated_list(node: NodeInterface): void {
this.body.push('</ol>\n')
}
*/
visit_field_list(node) {
// Keep simple paragraphs in the field_body to enable CSS
// rule to start body on new line if the label is too long
let classes = 'field-list';
if (this.isCompactable(node)) {
classes += ' simple';
}
this.body.push(this.starttag(node, 'dl', '\n', false, { CLASS: classes }));
}
depart_field_list(node) {
this.body.push('</dl>\n');
}
visit_field(node) {
}
depart_field(node) {
}
// as field is ignored, pass class arguments to field-name and field-body:
visit_field_name(node) {
this.body.push(this.starttag(node, 'dt', '', false, { CLASS: node.parent.attributes.classes.join(' ') }));
}
depart_field_name(node) {
this.body.push('</dt>\n');
}
visit_field_body(node) {
this.body.push(this.starttag(node, 'dd', '', false, { CLASS: node.parent.attributes.classes.join(' ') }));
// prevent misalignment of following content if the field is empty:
if (!node.hasChildren()) {
this.body.push('<p></p>');
}
}
depart_field_body(node) {
this.body.push('</dd>\n');
}
/*
visit_figure(node: NodeInterface): void {
atts = {'class': 'figure'}
if node.get('width'):
atts['style'] = 'width: %s' % node.attributes['width']
if node.get('align'):
atts['class'] += " align-" + node.attributes['align']
this.body.push(this.starttag(node, 'div', **atts))
}
depart_figure(node: NodeInterface): void {
this.body.push('</div>\n')
}
// use HTML 5 <footer> element?
visit_footer(node: NodeInterface): void {
this.context.push(len(this.body))
}
depart_footer(node: NodeInterface): void {
start = this.context.pop()
footer = [this.starttag(node, 'div', { CLASS: 'footer' }),
'<hr class="footer" />\n']
footer.extend(this.body[start:])
footer.push('\n</div>\n')
this.footer.extend(footer)
this.body_suffix[:0] = footer
del this.body[start:]
// footnotes
// ---------
// use definition list instead of table for footnote text
// TODO: use the new HTML5 element <aside>? (Also for footnote text)
}
*/
visit_footer(node) {
this.context.push(this.body.length.toString());
}
depart_footer(node) {
const start = this.context.pop();
const footer = [
this.starttag(node, 'div', undefined, undefined, { CLASS: 'footer' }),
'<hr class="footer" />\n',
];
footer.push(...this.body.slice(parseInt(start, 10)));
footer.push('\n</div>\n');
this.footer.push(...footer);
this.bodySuffix.splice(0, 0, ...footer);
this.body.splice(parseInt(start, 10), this.body.length - parseInt(start, 10));
}
visit_footnote(node) {
if (!this.inFootnoteList) {
const classes = `footnote ${this.settings.footnoteReferences}`;
this.body.push(`<dl class="${classes}">\n`);
this.inFootnoteList = true;
}
}
depart_footnote(node) {
this.body.push('</dd>\n');
if (!(node.nextNode({ descend: false, siblings: true }) instanceof nodes.footnote)) {
this.body.push('</dl>\n');
this.inFootnoteList = false;
}
}
/* whoops this requires references transform!! */
visit_footnote_reference(node) {
if (!node.attributes.refid) {
console.warn('warning, no refid ( implement transforms )');
}
const href = `#${node.attributes.refid || ''}`;
const classes = `footnote-reference ${this.settings.footnoteReferences}`;
this.body.push(this.starttag(node, 'a', '', // suffix,
false, { CLASS: classes, href }));
}
depart_footnote_reference(node) {
this.body.push('</a>');
}
// Docutils-generated text: put section numbers in a span for CSS styling:
visit_generated(node) {
/* generating error */
}
depart_generated(node) {
}
/*
visit_header(node: NodeInterface): void {
this.context.push(len(this.body))
}
depart_header(node: NodeInterface): void {
start = this.context.pop()
header = [this.starttag(node, 'div', { CLASS: 'header' })]
header.extend(this.body[start:])
header.push('\n<hr class="header"/>\n</div>\n')
this.bodyPrefix.extend(header)
this.header.extend(header)
}
del this.body[start:]
// Image types to place in an <object> element
object_image_types = {'.swf': 'application/x-shockwave-flash'}
visit_image(node: NodeInterface): void {
atts = {}
uri = node.attributes['uri']
ext = os.path.splitext(uri)[1].lower()
if ext in this.object_image_types:
atts['data'] = uri
atts['type'] = this.object_image_types[ext]
else:
atts['src'] = uri
atts['alt'] = node.get('alt', uri)
// image size
if 'width' in node:
atts['width'] = node.attributes['width']
if 'height' in node:
atts['height'] = node.attributes['height']
if 'scale' in node:
if (PIL and not ('width' in node and 'height' in node)
and this.settings.file_insertion_enabled):
imagepath = urllib.url2pathname(uri)
try:
img = PIL.Image.open(
imagepath.encode(sys.getfilesystemencoding()))
except (IOError, UnicodeEncodeError):
pass // TODO: warn?
else:
this.settings.record_dependencies.add(
imagepath.replace('\\', '/'))
if 'width' not in atts:
atts['width'] = '%dpx' % img.size[0]
if 'height' not in atts:
atts['height'] = '%dpx' % img.size[1]
del img
for att_name in 'width', 'height':
if att_name in atts:
match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
assert match
atts[att_name] = '%s%s' % (
float(match.group(1)) * (float(node.attributes['scale']) / 100),
match.group(2))
style = []
for att_name in 'width', 'height':
if att_name in atts:
if re.match(r'^[0-9.]+$', atts[att_name]):
// Interpret unitless values as pixels.
atts[att_name] += 'px'
style.push('%s: %s;' % (att_name, atts[att_name]))
del atts[att_name]
if style:
atts['style'] = ' '.join(style)
if (isinstance(node.parent, nodes.TextElement) or
(isinstance(node.parent, nodes.reference) and
not isinstance(node.parent.parent, nodes.TextElement))):
// Inline context or surrounded by <a>...</a>.
suffix = ''
else:
suffix = '\n'
if 'align' in node:
atts['class'] = 'align-%s' % node.attributes['align']
if ext in this.object_image_types:
// do NOT use an empty tag: incorrect rendering in browsers
this.body.push(this.starttag(node, 'object', suffix, **atts) +
node.get('alt', uri) + '</object>' + suffix)
else:
this.body.push(this.emptytag(node, 'img', suffix, **atts))
}
depart_image(node: NodeInterface): void {
// this.body.push(this.context.pop())
}
pass
visit_inline(node: NodeInterface): void {
this.body.push(this.starttag(node, 'span', ''))
}
depart_inline(node: NodeInterface): void {
this.body.push('</span>')
}
*/
// footnote and citation labels:
visit_label(node) {
let classes;
if (node.parent instanceof nodes.footnote) {
classes = this.settings.footnoteReferences;
}
else {
classes = 'brackets';
}
// pass parent node to get id into starttag:
this.body.push(this.starttag(node.parent, 'dt', '', false, { CLASS: 'label' }));
this.body.push(this.starttag(node, 'span', '', false, { CLASS: classes }));
// footnote/citation backrefs:
if (this.settings.footnoteBacklinks) {
const backrefs = node.parent.attributes.backrefs;
if (backrefs.length === 1) {
this.body.push(`<a class="fn-backref" href="//${backrefs[0]}">`);
}
}
}
depart_label(node) {
let backrefs = [];
if (this.settings.footnoteBacklinks) {
backrefs = node.parent.attributes.backrefs;
if (backrefs.length === 1) {
this.body.push('</a>');
}
}
this.body.push('</span>');
if (this.settings.footnoteBacklinks && backrefs.length > 1) {
const backlinks = backrefs.map((ref, i) => `<a href="//${ref}">${i + 1}</a>`);
this.body.push(`<span class="fn-backref">(${backlinks.join(',')})</span>`);
}
this.body.push('</dt>\n<dd>');
}
/*
visit_legend(node: NodeInterface): void {
this.body.push(this.starttag(node, 'div', { CLASS: 'legend' }))
}
depart_legend(node: NodeInterface): void {
this.body.push('</div>\n')
}
visit_line(node: NodeInterface): void {
this.body.push(this.starttag(node, 'div', suffix='', { CLASS: 'line' }))
if not len(node: NodeInterface):
this.body.push('<br />')
}
depart_line(node: NodeInterface): void {
this.body.push('</div>\n')
}
visit_line_block(node: NodeInterface): void {
this.body.push(this.starttag(node, 'div', { CLASS: 'line-block' }))
}
depart_line_block(node: NodeInterface): void {
this.body.push('</div>\n')
}
visit_list_item(node: NodeInterface): void {
this.body.push(this.starttag(node, 'li', ''))
}
depart_list_item(node: NodeInterface): void {
this.body.push('</li>\n')
}
*/
// inline literal
visit_literal(node) {
// special case: "code" role
const classes = node.attributes.classes || [];
if (classes.indexOf('code') !== -1) {
// filter 'code' from class arguments
// fixme //node.attributes['classes'] = [cls for cls in classes if cls != 'code']
this.body.push(this.starttag(node, 'span', '', false, { CLASS: 'docutils literal' }));
return;
}
let text = node.astext();
// remove hard line breaks (except if in a parsed-literal block)
if (!(node.parent instanceof nodes.literal_block)) {
text = text.replace('\n', ' ');
}
// Protect text like ``--an-option`` and the regular expression
// ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
/* fixme
this.wordsAndSpaces.findall(text).forEach((token): void => {
if (token.trim() && this.inWordWrapPoint.search(token)) {
this.body.push(`<span class="pre">${this.encode(token)}</span>`);
} else {
this.body.push(this.encode(token));
}
});
*/
this.body.push('</span>');
// Content already processed:
throw new nodes.SkipNode();
}
depart_literal(node) {
// skipped unless literal element is from "code" role:
this.body.push('</code>');
}
visit_literal_block(node) {
this.body.push(this.starttag(node, 'pre', '', false, { CLASS: 'literal-block' }));
if ((node.attributes.classes || []).indexOf('code') !== -1) {
this.body.push('<code>');
}
}
depart_literal_block(node) {
if ((node.attributes.classes || []).indexOf('code') !== -1) {
this.body.push('</code>');
}
this.body.push('</pre>\n');
// Mathematics:
// As there is no native HTML math support, we provide alternatives
// for the math-output: LaTeX and MathJax simply wrap the content,
// HTML and MathML also convert the math_code.
// HTML container
const mathTags = {
mathml: ['div', '', ''],
html: ['div', 'span', 'formula'],
mathjax: ['div', 'span', 'math'],
latex: ['pre', 'tt', 'math'],
};
}
/*
visit_math(node, math_env='') {
// If the method is called from visit_math_block(), math_env != ''.
if this.math_output not in this.math_tags:
this.document.reporter.error(
'math-output format "%s" not supported '
'falling back to "latex"'% this.math_output)
this.math_output = 'latex'
tag = this.math_tags[this.math_output][math_env == '']
clsarg = this.math_tags[this.math_output][2]
// LaTeX container
wrappers = {// math_mode: (inline, block)
'mathml': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
'html': ('$%s$', u'\\begin{%s}\n%s\n\\end{%s}'),
'mathjax': (r'\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
'latex': (None, None),
}
wrapper = wrappers[this.math_output][math_env != '']
if this.math_output == 'mathml' and (not this.math_output_options or
this.math_output_options[0] == 'blahtexml'):
wrapper = None
// get and wrap content
math_code = node.astext().translate(unichar2tex.uni2tex_table)
if wrapper:
try: // wrapper with three "%s"
math_code = wrapper % (math_env, math_code, math_env)
except TypeError: // wrapper with one "%s"
math_code = wrapper % math_code
// settings and conversion
if this.math_output in ('latex', 'mathjax'):
math_code = this.encode(math_code)
if this.math_output == 'mathjax' and not this.math_header:
try:
this.mathjax_url = this.math_output_options[0]
except IndexError:
this.document.reporter.warning('No MathJax URL specified, '
'using local fallback (see config.html)')
// append configuration, if not already present in the URL:
// input LaTeX with AMS, output common HTML
if '?' not in this.mathjax_url:
this.mathjax_url += '?config=TeX-AMS_CHTML'
this.math_header = [this.mathjax_script % this.mathjax_url]
elif this.math_output == 'html':
if this.math_output_options and not this.math_header:
this.math_header = [this.stylesheet_call(
utils.find_file_in_dirs(s, this.settings.stylesheet_dirs))
for s in this.math_output_options[0].split(',')]
// TODO: fix display mode in matrices and fractions
math2html.DocumentParameters.displaymode = (math_env != '')
math_code = math2html.math2html(math_code)
elif this.math_output == 'mathml':
if 'XHTML 1' in this.doctype:
this.doctype = this.doctype_mathml
this.content_type = this.content_type_mathml
converter = ' '.join(this.math_output_options).lower()
try:
if converter == 'latexml':
math_code = tex2mathml_extern.latexml(math_code,
this.document.reporter)
elif converter == 'ttm':
math_code = tex2mathml_extern.ttm(math_code,
this.document.reporter)
elif converter == 'blahtexml':
math_code = tex2mathml_extern.blahtexml(math_code,
inline=not(math_env),
reporter=this.document.reporter)
elif not converter:
math_code = latex2mathml.tex2mathml(math_code,
inline=not(math_env))
else:
this.document.reporter.error('option "%s" not supported '
'with math-output "MathML"')
except OSError:
raise OSError('is "latexmlmath" in your PATH?')
except SyntaxError, err:
err_node = this.document.reporter.error(err, base_node=node)
this.visit_system_message(err_node)
this.body.push(this.starttag(node, 'p'))
this.body.push(u','.join(err.args))
this.body.push('</p>\n')
this.body.push(this.starttag(node, 'pre',
{ CLASS: 'literal-block' }))
this.body.push(this.encode(math_code))
this.body.push('\n</pre>\n')
this.depart_system_message(err_node)
raise nodes.SkipNode
// append to document body
if tag:
this.body.push(this.starttag(node, tag,
suffix='\n'*bool(math_env),
CLASS=clsarg))
this.body.push(math_code)
if math_env: // block mode (equation, display)
this.body.push('\n')
if tag:
this.body.push('</%s>' % tag)
if math_env:
this.body.push('\n')
}
// Content already processed:
raise nodes.SkipNode
depart_math(node: NodeInterface): void {
}
pass // never reached
visit_math_block(node: NodeInterface): void {
// print node.astext().encode('utf8')
math_env = pick_math_environment(node.astext())
this.visit_math(node, math_env=math_env)
}
depart_math_block(node: NodeInterface): void {
}
pass // never reached
// Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
// HTML5/polyglot recommends using both
visit_meta(node: NodeInterface): void {
meta = this.emptytag(node, 'meta', **node.non_default_attributes())
this.add_meta(meta)
}
depart_meta(node: NodeInterface): void {
}
pass
add_meta( tag) {
this.meta.push(tag)
this.head.push(tag)
}
visit_option(node: NodeInterface): void {
this.body.push(this.starttag(node, 'span', '', { CLASS: 'option' }))
}
depart_option(node: NodeInterface): void {
this.body.push('</span>')
if isinstance(node.next_node(descend=false, siblings=true),
nodes.option):
this.body.push(', ')
}
visit_option_argument(node: NodeInterface): void {
this.body.push(node.get('delimiter', ' '))
this.body.push(this.starttag(node, 'var', ''))
}
depart_option_argument(node: NodeInterface): void {
this.body.push('</var>')
}
visit_option_group(node: NodeInterface): void {
this.body.push(this.starttag(node, 'dt', ''))
this.body.push('<kbd>')
}
depart_option_group(node: NodeInterface): void {
this.body.push('</kbd></dt>\n')
}
visit_option_list(node: NodeInterface): void {
this.body.push(
this.starttag(node, 'dl', { CLASS: 'option-list' }))
}
depart_option_list(node: NodeInterface): void {
this.body.push('</dl>\n')
}
visit_option_list_item(node: NodeInterface): void {
}
depart_option_list_item(node: NodeInterface): void {
}
visit_option_string(node: NodeInterface): void {
}
depart_option_string(node: NodeInterface): void {
}
visit_organization(node: NodeInterface): void {
this.visitDocinfoItem(node, 'organization')
}
depart_organization(node: NodeInterface): void {
this.departDocinfoItem()
// Do not omit <p> tags
// --------------------
//
// The HTML4CSS1 writer does this to "produce
// visually compact lists (less vertical whitespace)". This writer
}
// relies on CSS rules for"visual compactness".
//
// * In XHTML 1.1, e.g. a <blockquote> element may not contain
// character data, so you cannot drop the <p> tags.
// * Keeping simple paragraphs in the field_body enables a CSS
// rule to start the field-body on a new line if the label is too long
// * it makes the code simpler.
//
// TODO: omit paragraph tags in simple table cells?
visit_paragraph(node: NodeInterface): void {
this.body.push(this.starttag(node, 'p', ''))
}
depart_paragraph(node: NodeInterface): void {
this.body.push('</p>')
if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
(len(node.parent) == 1)):
this.body.push('\n')
}
*/
visit_problematic(node) {
if (typeof node.attributes.refid !== 'undefined') {
this.body.push(`<a href="//${node.attributes.refid}">`);
this.context.push('</a>');
}
else {
this.context.push('');
this.body.push(this.starttag(node, 'span', '', false, { CLASS: 'problematic' }));
}
}
depart_problematic(node) {
this.body.push('</span>');
this.body.push(this.context.pop());
}
/*
visit_raw(node: NodeInterface): void {
if 'html' in node.get('format', '').split():
t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
if node.attributes['classes']:
this.body.push(this.starttag(node, t, suffix=''))
this.body.push(node.astext())
if node.attributes['classes']:
this.body.push('</%s>' % t)
// Keep non-HTML raw text out of output:
raise nodes.SkipNode
}
*/
visit_reference(node) {
const atts = { class: 'reference' };
if ('refuri' in node.attributes) {
atts.href = node.attributes.refuri;
if (this.settings.cloakEmailAddresses
&& atts.href.substring(0, 6) === 'mailto:') {
atts.href = this.cloakMailto(atts.href);
this.inMailto = true;
}
atts.class += ' external';
}
else {
// assert 'refid' in node, \ 'References must have "refuri" or "refid" attribute.'
atts.href = `#${node.attributes.refid}`;
atts.class += ' internal';
}
if (!(node.parent instanceof nodes.TextElement)) {
// assert len(node: NodeInterface) == 1 and isinstance(node.attributes[0], nodes.image)
atts.class += ' image-reference';
}
this.body.push(this.starttag(node, 'a', '', false, atts));
}
depart_reference(node) {
this.body.push('</a>');
if (!(node.parent instanceof nodes.TextElement)) {
this.body.push('\n');
}
this.inMailto = false;
}
/*
visit_revision(node: NodeInterface): void {
this.visitDocinfoItem(node, 'revision', meta=false)
}
depart_revision(node: NodeInterface): void {
this.departDocinfoItem()
}
*/
visit_row(node) {
this.body.push(this.starttag(node, 'tr', ''));
node.column = 0;
}
depart_row(node) {
this.body.push('</tr>\n');
}
/*
visit_rubric(node: NodeInterface): void {
this.body.push(this.starttag(node, 'p', '', { CLASS: 'rubric' }))
}
depart_rubric(node: NodeInterface): void {
this.body.push('</p>\n')
}
// TODO: use the new HTML