UNPKG

docutils-ts

Version:

Port of the Python Docutils library to TypeScript

1,396 lines (1,344 loc) 74.2 kB
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', '&#37;&#52;&#48;'); encoded = encoded.replace('.', '&#46;'); } 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