jenkins-bot has submitted this change and it was merged. Change subject: JsApi: Add text and comment node support; bump domino dependency. ......................................................................
JsApi: Add text and comment node support; bump domino dependency. Bump domino dependency to 1.0.19 due to fixes needed for the DOM TreeWalker interface. Change-Id: I3667af918035c721d40813d32e9063bf61404f39 --- M .jsduck/categories.json M lib/jsapi.js M package.json M tests/mocha/jsapi.js 4 files changed, 178 insertions(+), 6 deletions(-) Approvals: Arlolra: Looks good to me, approved jenkins-bot: Verified diff --git a/.jsduck/categories.json b/.jsduck/categories.json index 56b89e7..7d8f247 100644 --- a/.jsduck/categories.json +++ b/.jsduck/categories.json @@ -6,6 +6,7 @@ "name": "Package Interface", "classes": [ "Parsoid", + "PComment", "PDoc", "PNodeList", "PNode", @@ -13,6 +14,7 @@ "PHeading", "PHtmlEntity", "PTemplate", + "PText", "PWikiLink" ] }, diff --git a/lib/jsapi.js b/lib/jsapi.js index ece9e90..e91d88d 100644 --- a/lib/jsapi.js +++ b/lib/jsapi.js @@ -6,13 +6,16 @@ require('../lib/core-upgrade.js'); // TO DO: -// comment/tag/text/figure +// tag/figure // PTemplate#get should return PParameter and support mutation. // PExtLink#url PWikiLink#title should handle mw:ExpandedAttrs // make separate package? var WikitextSerializer = require('../lib/mediawiki.WikitextSerializer.js').WikitextSerializer; var DU = require('../lib/mediawiki.DOMUtils.js').DOMUtils; +var DOMImpl = require('domino').impl; +var Node = DOMImpl.Node; +var NodeFilter = DOMImpl.NodeFilter; var util = require('util'); // WTS helper @@ -38,7 +41,7 @@ var noop = function() { }; // Forward declarations of Wrapper classes. -var PNode, PNodeList, PExtLink, PHeading, PHtmlEntity, PTemplate, PWikiLink; +var PNode, PNodeList, PComment, PExtLink, PHeading, PHtmlEntity, PTemplate, PText, PWikiLink; // HTML escape helper var toHtmlStr = function(node, v) { @@ -106,7 +109,52 @@ if (this.parent) { this.parent.update(); } }, }, _querySelectorAll: { value: function(selector) { - return Array.from(this.container.querySelectorAll(selector)); + var tweakedSelector = ',' + selector + ','; + if (!(/,(COMMENT|TEXT),/.test(tweakedSelector))) { + // Use fast native querySelectorAll + return Array.from(this.container.querySelectorAll(selector)); + } + // Implement comment/text node selector the hard way + /* jshint bitwise: false */ + var whatToShow = NodeFilter.SHOW_ELEMENT; // always show templates + if (/,COMMENT,/.test(tweakedSelector)) { + whatToShow = whatToShow | NodeFilter.SHOW_COMMENT; + } + if (/,TEXT,/.test(tweakedSelector)) { + whatToShow = whatToShow | NodeFilter.SHOW_TEXT; + } + var nodeFilter = function(node) { + if (node.nodeType !== Node.ELEMENT_NODE) { + return NodeFilter.FILTER_ACCEPT; + } + if (/\bmw:Transclusion\b/.test(node.getAttribute('typeof') || '')) { + return NodeFilter.FILTER_ACCEPT; + } + return NodeFilter.FILTER_SKIP; + }; + var result = []; + var includeTemplates = + /,\[typeof~="mw:Transclusion"\],/.test(tweakedSelector); + var treeWalker = this.pdoc.document.createTreeWalker( + this.container, whatToShow, nodeFilter, false + ); + while (treeWalker.nextNode()) { + var node = treeWalker.currentNode; + // We don't need the extra test for ELEMENT_NODEs yet, since + // non-template element nodes will be skipped by the nodeFilter + // above. But if we ever extend filter() to be fully generic, + // we might need the commented-out portion of this test. + if (node.nodeType === Node.ELEMENT_NODE /* && + /\bmw:Transclusion\b/.test(node.getAttribute('typeof') || '')*/ + ) { + treeWalker.lastChild(); // always skip over all children + if (!includeTemplates) { + continue; // skip template itself + } + } + result.push(node); + } + return result; }, }, _templatesForNode: { value: function(node) { // each Transclusion node could represent multiple templates. @@ -134,8 +182,8 @@ tSelector += ',' + selector; } this._querySelectorAll(tSelector).forEach(function(node) { - var ty = node.getAttribute('typeof') || ''; - var isTemplate = /\bmw:Transclusion\b/.test(ty); + var isTemplate = node.nodeType === Node.ELEMENT_NODE && + /\bmw:Transclusion\b/.test(node.getAttribute('typeof') || ''); if (isTemplate) { self._templatesForNode(node).forEach(function(t) { if (!selector) { @@ -157,6 +205,18 @@ } }); return result; + }, }, + + /** + * Return an array of {@link PComment} representing comments + * found in this {@link PNodeList}. + * @inheritdoc #_filter + * @return {PComment[]} + */ + filterComments: { value: function(opts) { + return this._filter([], 'COMMENT', function(r, parent, node, opts) { + r.push(new PComment(parent.pdoc, parent, node)); + }, opts); }, }, /** @@ -205,6 +265,17 @@ return this._filter([], null, null, opts); }, }, + /** + * Return an array of {@link PText} representing plain text + * found in this {@link PNodeList}. + * @inheritdoc #_filter + * @return {PText[]} + */ + filterText: { value: function(opts) { + return this._filter([], 'TEXT', function(r, parent, node, opts) { + r.push(new PText(parent.pdoc, parent, node)); + }, opts); + }, }, /** * Return an array of {@link PWikiLink} representing wiki links @@ -254,10 +325,12 @@ * * Useful subclasses of {@link PNode} include: * + * - {@link PComment}: comments, like `<!-- example -->` * - {@link PExtLink}: external links, like `[http://example.com Example]` * - {@link PHeading}: headings, like `== Section 1 ==` * - {@link PHtmlEntity}: html entities, like ` ` * - {@link PTemplate}: templates, like `{{foo|bar}}` + * - {@link PText}: unformatted text, like `foo` * - {@link PWikiLink}: wiki links, like `[[Foo|bar]]` */ /** @@ -530,6 +603,36 @@ }; /** + * PComment represents a hidden HTML comment, like `<!-- fobar -->`. + * @class PComment + * @extends PNode + */ +/** + * @method constructor + * @private + * @inheritdoc PNode#constructor + */ +PComment = function PComment(pdoc, parent, node, opts) { + PNode.call(this, pdoc, parent, node, opts); +}; +util.inherits(PComment, PNode); +Object.defineProperties(PComment.prototype, { + /** + * The hidden text contained between `<!--` and `-->`. + * @property {String} + */ + contents: { + get: function() { + return DU.decodeComment(this.node.data); + }, + set: function(v) { + this.node.data = DU.encodeComment(v); + this.update(); + }, + }, +}); + +/** * PExtLink represents an external link, like `[http://example.com Example]`. * @class PExtLink * @extends PNode @@ -643,6 +746,35 @@ }, }); +/** + * PText represents ordinary unformatted text with no special properties. + * @class PText + * @extends PNode + */ +/** + * @method constructor + * @private + * @inheritdoc PNode#constructor + */ +PText = function PText(pdoc, parent, node, opts) { + PNode.call(this, pdoc, parent, node, opts); +}; +util.inherits(PText, PNode); +Object.defineProperties(PText.prototype, { + /** + * The actual text itself. + * @property {String} + */ + value: { + get: function() { + return this.node.data; + }, + set: function(v) { + this.node.data = v; + this.update(); + }, + }, +}); /** * PWikiLink represents an internal wikilink, like `[[Foo|Bar]]`. diff --git a/package.json b/package.json index ba4047d..b8f73b4 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "compression": "~1.4.0", "core-js": "^0.8.1", "diff": "~1.0.7", - "domino": "~1.0.18", + "domino": "~1.0.19", "entities": "~1.1.1", "express": "~2.5.11", "gelf-stream": "~0.2.4", diff --git a/tests/mocha/jsapi.js b/tests/mocha/jsapi.js index b0c5f53..de71321 100644 --- a/tests/mocha/jsapi.js +++ b/tests/mocha/jsapi.js @@ -172,4 +172,42 @@ String(pdoc).should.equal('<{{echo|>}}\n'); }); }); + it('filters and mutates comments', function() { + var text = '<!-- foo --> {{echo|<!--bar-->}}'; + return Parsoid.parse(text, { pdoc: true }).then(function(pdoc) { + var comments = pdoc.filterComments(); + comments.length.should.equal(2); + comments[0].contents.should.equal(' foo '); + comments[1].contents.should.equal('bar'); + comments[0].contents = '<!-- ha! -->'; + comments[1].contents = '--'; + String(pdoc).should.equal('<!--<!-- ha! -->--> {{echo|<!------>}}'); + }); + }); + it('filters and mutates text', function() { + var text = 'foo {{echo|bar}}'; + return Parsoid.parse(text, { pdoc: true }).then(function(pdoc) { + var texts = pdoc.filterText({ recursive: false }); + texts.length.should.equal(1); + texts = pdoc.filterText({ recursive: true }); + texts.length.should.equal(2); + texts[0].value.should.equal('foo '); + texts[1].value.should.equal('bar'); + texts[0].value = 'FOO '; + String(pdoc).should.equal('FOO {{echo|bar}}\n'); + texts[1].value = 'BAR'; + String(pdoc).should.equal('FOO {{echo|BAR}}\n'); + }); + }); + it.skip('filters and mutates text (2)', function() { + var text = '{{{echo|{{!}}}}\n| foo\n|}'; + return Parsoid.parse(text, { pdoc: true }).then(function(pdoc) { + var texts = pdoc.filterText(); + texts.length.should.equal(1); + // XXX this doesn't work yet, see note at end of + // https://www.mediawiki.org/wiki/Parsoid/MediaWiki_DOM_spec#Transclusion_content + // for details. + texts[0].value.should.equal(' foo'); + }); + }); }); -- To view, visit https://gerrit.wikimedia.org/r/227886 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I3667af918035c721d40813d32e9063bf61404f39 Gerrit-PatchSet: 7 Gerrit-Project: mediawiki/services/parsoid Gerrit-Branch: master Gerrit-Owner: Cscott <canan...@wikimedia.org> Gerrit-Reviewer: Arlolra <abrea...@wikimedia.org> Gerrit-Reviewer: Cscott <canan...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits