Cscott has uploaded a new change for review.
https://gerrit.wikimedia.org/r/228006
Change subject: Add Tag and Image node support to JsApi; implement get/indexOf.
......................................................................
Add Tag and Image node support to JsApi; implement get/indexOf.
Begin to implement accessor methods on PNodeList, starting with
`get` and `indexOf`.
Add PTag and PImage nodes, and allow mutation of image captions.
Change-Id: I8a8a6899782ecbf0c0d011b5a0c490fa7d3b256d
---
M .jsduck/categories.json
M guides/jsapi/README.md
M lib/jsapi.js
M tests/mocha/jsapi.js
4 files changed, 559 insertions(+), 174 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/services/parsoid
refs/changes/06/228006/1
diff --git a/.jsduck/categories.json b/.jsduck/categories.json
index f5c45f0..1938360 100644
--- a/.jsduck/categories.json
+++ b/.jsduck/categories.json
@@ -13,6 +13,8 @@
"PExtLink",
"PHeading",
"PHtmlEntity",
+ "PImage",
+ "PTag",
"PTemplate",
"PText",
"PWikiLink"
diff --git a/guides/jsapi/README.md b/guides/jsapi/README.md
index f3224ef..45620f7 100644
--- a/guides/jsapi/README.md
+++ b/guides/jsapi/README.md
@@ -102,18 +102,18 @@
```
Templates can be easily modified to add, remove, or alter params.
-Templates also have a [`matches()`](#!/api/PTemplate-method-matches) method
-for comparing template names, which takes care of capitalization and
+Templates also have a [`nameMatches()`](#!/api/PTemplate-method-nameMatches)
+method for comparing template names, which takes care of capitalization and
white space:
```
> var text = "{{cleanup}} '''Foo''' is a [[bar]]. {{uncategorized}}";
> var pdoc = yield Parsoid.parse(text, { pdoc: true });
> pdoc.filterTemplates().forEach(function(template) {
-... if (template.matches('Cleanup') && !template.has('date')) {
+... if (template.nameMatches('Cleanup') && !template.has('date')) {
... template.add('date', 'July 2012');
... }
-... if (template.matches('uncategorized')) {
+... if (template.nameMatches('uncategorized')) {
... template.name = 'bar-stub';
... }
... });
diff --git a/lib/jsapi.js b/lib/jsapi.js
index e91d88d..22a01a5 100644
--- a/lib/jsapi.js
+++ b/lib/jsapi.js
@@ -6,7 +6,7 @@
require('../lib/core-upgrade.js');
// TO DO:
-// tag/figure
+// extension
// PTemplate#get should return PParameter and support mutation.
// PExtLink#url PWikiLink#title should handle mw:ExpandedAttrs
// make separate package?
@@ -41,7 +41,7 @@
var noop = function() { };
// Forward declarations of Wrapper classes.
-var PNode, PNodeList, PComment, PExtLink, PHeading, PHtmlEntity, PTemplate,
PText, PWikiLink;
+var PNode, PNodeList, PComment, PExtLink, PHeading, PHtmlEntity, PImage, PTag,
PTemplate, PText, PWikiLink;
// HTML escape helper
var toHtmlStr = function(node, v) {
@@ -84,6 +84,7 @@
this.parent = parent;
this.container = container;
this._update = (opts && opts.update);
+ this._cachedPNodes = null;
};
Object.defineProperties(PNodeList.prototype, {
/**
@@ -105,6 +106,7 @@
* @method
*/
update: { value: function() {
+ this._cachedPNodes = null;
if (this._update) { this._update(); }
if (this.parent) { this.parent.update(); }
}, },
@@ -127,7 +129,7 @@
if (node.nodeType !== Node.ELEMENT_NODE) {
return NodeFilter.FILTER_ACCEPT;
}
- if
(/\bmw:Transclusion\b/.test(node.getAttribute('typeof') || '')) {
+ if (node.matches(PTemplate._selector)) {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
@@ -145,7 +147,7 @@
// 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') || '')*/
+ node.matches(PTemplate._selector) */
) {
treeWalker.lastChild(); // always skip over all
children
if (!includeTemplates) {
@@ -177,13 +179,13 @@
_filter: { value: function(result, selector, func, opts) {
var self = this;
var recursive = (opts && opts.recursive) !== false;
- var tSelector = '[typeof~="mw:Transclusion"]';
+ var tSelector = PTemplate._selector;
if (selector) {
tSelector += ',' + selector;
}
this._querySelectorAll(tSelector).forEach(function(node) {
var isTemplate = node.nodeType === Node.ELEMENT_NODE &&
-
/\bmw:Transclusion\b/.test(node.getAttribute('typeof') || '');
+ node.matches(PTemplate._selector);
if (isTemplate) {
self._templatesForNode(node).forEach(function(t) {
if (!selector) {
@@ -214,7 +216,7 @@
* @return {PComment[]}
*/
filterComments: { value: function(opts) {
- return this._filter([], 'COMMENT', function(r, parent, node,
opts) {
+ return this._filter([], PComment._selector, function(r, parent,
node, opts) {
r.push(new PComment(parent.pdoc, parent, node));
}, opts);
}, },
@@ -226,7 +228,7 @@
* @return {PExtLink[]}
*/
filterExtLinks: { value: function(opts) {
- return this._filter([], 'a[rel="mw:ExtLink"]', function(r,
parent, node, opts) {
+ return this._filter([], PExtLink._selector, function(r, parent,
node, opts) {
r.push(new PExtLink(parent.pdoc, parent, node));
}, opts);
}, },
@@ -238,7 +240,7 @@
* @return {PHeading[]}
*/
filterHeadings: { value: function(opts) {
- return this._filter([], 'h1,h2,h3,h4,h5,h6', function(r,
parent, node, opts) {
+ return this._filter([], PHeading._selector, function(r, parent,
node, opts) {
r.push(new PHeading(parent.pdoc, parent, node));
}, opts);
}, },
@@ -250,8 +252,20 @@
* @return {PHtmlEntity[]}
*/
filterHtmlEntities: { value: function(opts) {
- return this._filter([], '[typeof="mw:Entity"]', function(r,
parent, node, opts) {
+ return this._filter([], PHtmlEntity._selector, function(r,
parent, node, opts) {
r.push(new PHtmlEntity(parent.pdoc, parent, node));
+ }, opts);
+ }, },
+
+ /**
+ * Return an array of {@link PImage} representing images
+ * found in this {@link PNodeList}.
+ * @inheritdoc #_filter
+ * @return {PImage[]}
+ */
+ filterImages: { value: function(opts) {
+ return this._filter([], PImage._selector, function(r, parent,
node, opts) {
+ r.push(new PImage(parent.pdoc, parent, node));
}, opts);
}, },
@@ -272,7 +286,7 @@
* @return {PText[]}
*/
filterText: { value: function(opts) {
- return this._filter([], 'TEXT', function(r, parent, node, opts)
{
+ return this._filter([], PText._selector, function(r, parent,
node, opts) {
r.push(new PText(parent.pdoc, parent, node));
}, opts);
}, },
@@ -284,9 +298,115 @@
* @return {PWikiLink[]}
*/
filterWikiLinks: { value: function(opts) {
- return this._filter([], 'a[rel="mw:WikiLink"]', function(r,
parent, node, opts) {
+ return this._filter([], PWikiLink._selector, function(r,
parent, node, opts) {
r.push(new PWikiLink(parent.pdoc, parent, node));
}, opts);
+ }, },
+
+ /**
+ * Internal list of PNodes in this list.
+ * @property {PNode[]}
+ * @private
+ */
+ pnodes: { get: function() {
+ if (this._cachedPNodes !== null) {
+ return this._cachedPNodes;
+ }
+ var templates = new Set();
+ var result = [];
+ OUTER: for (var i = 0; i < this.container.childNodes.length;
i++) {
+ var node = this.container.childNodes.item(i);
+ if (node.nodeType === Node.TEXT_NODE) {
+ result.push(new PText(this.pdoc, this, node));
+ continue;
+ }
+ if (node.nodeType === Node.COMMENT_NODE) {
+ result.push(new PComment(this.pdoc, this,
node));
+ continue;
+ }
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ // Note: multiple PTemplates per Node, and
possibly
+ // multiple Nodes per PTemplate.
+ if (node.matches(PTemplate._selector)) {
+
templates.add(node.getAttribute('about'));
+
this._templatesForNode(node).forEach(function(t) {
+ result.push(t);
+ });
+ continue;
+ } else if
(templates.has(node.getAttribute('about'))) {
+ continue;
+ }
+ // PTag is the catch-all; it should always be
last.
+ var which = [
+ PExtLink, PHeading, PHtmlEntity,
PImage, PWikiLink,
+ PTag
+ ];
+ for (var j = 0; j < which.length; j++) {
+ var Ty = which[j];
+ if (node.matches(Ty._selector)) {
+ result.push(new Ty(this.pdoc,
this, node));
+ continue OUTER;
+ }
+ }
+ }
+ // Unknown type.
+ result.push(new PNode(this.pdoc, this, node));
+ }
+ return (this._cachedPNodes = result);
+ }, },
+
+ /**
+ * The number of nodes within the node list.
+ * @property {Number}
+ */
+ length: { get: function() { return this.pnodes.length; }, },
+
+ /**
+ * Return the `index`th node within the node list.
+ * @param {Number} index
+ * @return {PNode}
+ */
+ get: { value: function(index) { return this.pnodes[index]; }, },
+
+ /**
+ * Return the index of `target` in the list of nodes, or `-1` if
+ * the target was not found.
+ *
+ * If `recursive` is true, we will look in all nodes of ours and
+ * their descendants, and return the index of our direct descendant
+ * node which contains the target. Otherwise, the search is done
+ * only on direct descendants.
+ *
+ * If `fromIndex` is provided, it is the index to start the search
+ * at.
+ * @param {PNode|Node} target
+ * @param {Object} [options]
+ * @param {Boolean} [options.recursive=false]
+ * @param {Number} [options.fromIndex=0]
+ */
+ indexOf: { value: function(target, options) {
+ var recursive = Boolean(options && options.recursive);
+ var fromIndex = Number(options && options.fromIndex) || 0;
+ var child, children;
+ var i, j;
+ if (target instanceof PNode) {
+ target = target.node;
+ }
+ for (i = fromIndex; i < this.length; i++) {
+ child = this.get(i);
+ if (child.matches(target)) {
+ return i;
+ }
+ if (recursive) {
+ children = child._children();
+ for (j = 0; j < children.length; j++) {
+ if (children[j].indexOf(target,
options) !== -1) {
+ return i;
+ }
+ }
+ }
+ }
+ return -1;
}, },
/**
@@ -365,6 +485,12 @@
set: function(v) { DU.storeDataMw(this.node, v); this.update();
},
},
/**
+ * Internal helper: enumerate all PNodeLists contained within this node.
+ * @private
+ * @return {PNodeList[]}
+ */
+ _children: { value: function() { return []; }, },
+ /**
* Call {@link #update} after manually mutating the DOM {@link Node}
* associated with this {@link PNode} in order to ensure that any
* containing templates are refreshed with their updated contents.
@@ -376,6 +502,18 @@
update: { value: function() {
if (this._update) { this._update(); }
if (this.parent) { this.parent.update(); }
+ }, },
+ /**
+ * Returns true if the `target` matches this node. By default a
+ * node matches only if its #node is strictly equal to the target
+ * or the target's #node. Subclasses can override this to provide
+ * more flexible matching: for example see {@link PText#matches}.
+ * @param {Node|PNode} target
+ * @return {Boolean} true if the target matches this node, false
otherwise.
+ */
+ matches: { value: function(target) {
+ return (target === this) || (target === this.node) ||
+ (target instanceof PNode && target.node === this.node);
}, },
/**
* @inheritdoc PNodeList#toHtml
@@ -394,6 +532,321 @@
return wts(this.pdoc.env, nodes);
}, },
});
+
+// Helper: getter and setter for the inner contents of a node.
+var innerAccessor = {
+ get: function() {
+ return new PNodeList(this.pdoc, this, this.node);
+ },
+ set: function(v) {
+ this.node.innerHTML = toHtmlStr(this.node, v);
+ this.update();
+ },
+};
+
+/**
+ * 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();
+ },
+ },
+});
+/** @private @static @ignore */
+PComment._selector = 'COMMENT'; // non-standard selector
+
+/**
+ * PExtLink represents an external link, like `[http://example.com Example]`.
+ * @class PExtLink
+ * @extends PNode
+ */
+/**
+ * @method constructor
+ * @private
+ * @inheritdoc PNode#constructor
+ */
+PExtLink = function PExtLink(pdoc, parent, node, opts) {
+ PNode.call(this, pdoc, parent, node, opts);
+};
+util.inherits(PExtLink, PNode);
+Object.defineProperties(PExtLink.prototype, {
+ /**
+ * The URL of the link target.
+ * @property {String}
+ */
+ url: {
+ // XXX url should be a PNodeList, but that requires handling
+ // typeof="mw:ExpandedAttrs"
+ get: function() {
+ return this.node.getAttribute('href');
+ },
+ set: function(v) {
+ this.node.setAttribute('href', v);
+ },
+ },
+ /**
+ * The link title, as a {@link PNodeList}.
+ * You can assign a String, Node, or PNodeList to mutate the title.
+ * @property {PNodeList}
+ */
+ title: innerAccessor,
+ // XXX include this.url, once it is a PNodeList
+ _children: { value: function() { return [this.title]; }, },
+});
+/** @private @static @ignore */
+PExtLink._selector = 'a[rel="mw:ExtLink"]';
+
+/**
+ * PHeading represents a section heading in wikitext, like `== Foo ==`.
+ * @class PHeading
+ * @extends PNode
+ */
+/**
+ * @method constructor
+ * @private
+ * @inheritdoc PNode#constructor
+ */
+PHeading = function PHeading(pdoc, parent, node, opts) {
+ PNode.call(this, pdoc, parent, node, opts);
+};
+util.inherits(PHeading, PNode);
+Object.defineProperties(PHeading.prototype, {
+ /**
+ * The heading level, as an integer between 1 and 6 inclusive.
+ * @property {Number}
+ */
+ level: {
+ get: function() {
+ return +this.node.nodeName.slice(1);
+ },
+ set: function(v) {
+ v = +v;
+ if (v === this.level) {
+ return;
+ } else if (v >= 1 && v <= 6) {
+ var nh = this.ownerDocument.createElement('h' +
v);
+ while (this.node.firstChild !== null) {
+ nh.appendChild(this.node.firstChild);
+ }
+ this.node.parentNode.replaceChild(nh,
this.node);
+ this.node = nh;
+ this.update();
+ } else {
+ throw new Error("Level must be between 1 and 6,
inclusive.");
+ }
+ },
+ },
+ /**
+ * The title of the heading, as a {@link PNodeList}.
+ * You can assign a String, Node, or PNodeList to mutate the title.
+ * @property {PNodeList}
+ */
+ title: innerAccessor,
+
+ _children: { value: function() { return [this.title]; }, },
+});
+/** @private @static @ignore */
+PHeading._selector = 'h1,h2,h3,h4,h5,h6';
+
+/**
+ * PHtmlEntity represents an HTML entity, like ` `.
+ * @class PHtmlEntity
+ * @extends PNode
+ */
+/**
+ * @method constructor
+ * @private
+ * @inheritdoc PNode#constructor
+ */
+PHtmlEntity = function PHtmlEntity(pdoc, parent, node, opts) {
+ PNode.call(this, pdoc, parent, node, opts);
+};
+util.inherits(PHtmlEntity, PNode);
+Object.defineProperties(PHtmlEntity.prototype, {
+ /**
+ * The character represented by the HTML entity.
+ * @property {String}
+ */
+ normalized: {
+ get: function() { return this.node.textContent; },
+ set: function(v) {
+ this.node.textContent = v;
+ this.node.removeAttribute('data-parsoid');
+ this.update();
+ },
+ },
+ /**
+ * Extends {@link PNode#matches} to allow a target string to match
+ * if it matches this node's #normalized character.
+ * @method
+ * @inheritdoc PNode#matches
+ * @param {Node|PNode|String} target
+ */
+ matches: { value: function(target) {
+ return PNode.prototype.matches.call(this, target) ||
+ this.normalized === target;
+ }, },
+});
+/** @private @static @ignore */
+PHtmlEntity._selector = '[typeof="mw:Entity"]';
+
+/**
+ * PImage represents an image in wikitext, like `[[File:Foobar.jpg|caption]]`.
+ * @class PImage
+ * @extends PNode
+ */
+/**
+ * @method constructor
+ * @private
+ * @inheritdoc PNode#constructor
+ */
+PImage = function PImage(pdoc, parent, node, opts) {
+ PNode.call(this, pdoc, parent, node, opts);
+};
+util.inherits(PImage, PNode);
+Object.defineProperties(PImage.prototype, {
+ // Internal helper: is the outer element a <figure> or a <span>?
+ _isBlock: { get: function() { return this.node.tagName === 'FIGURE'; },
},
+ // Internal helper: get at the 'caption' property in the dataMw
+ _caption: {
+ get: function() {
+ var c = this.dataMw.caption;
+ return c === undefined ? null : c;
+ },
+ set: function(v) {
+ var dmw = this.dataMw;
+ if (v===undefined || v===null) {
+ delete dmw.caption;
+ } else {
+ dmw.caption = v;
+ }
+ this.dataMw = dmw;
+ },
+ },
+
+ /**
+ * The caption of the image, or `null` if not present.
+ * You can assign `null`, a String, Node, or PNodeList to mutate the
+ * contents.
+ * @property {PNodeList|null}
+ */
+ caption: {
+ get: function() {
+ var c, captionDiv;
+ // Note that _cachedNodeList is null if caption is
missing.
+ if (this._cachedNodeList === undefined) {
+ if (this._isBlock) {
+ c = this.node.firstChild.nextSibling;
+ this._cachedNodeList =
+ c ? new PNodeList(this.pdoc,
this, c) : null;
+ } else {
+ c = this._caption;
+ if (c === null) {
+ this._cachedNodeList = null;
+ } else {
+ captionDiv =
this.ownerDocument.createElement('div');
+ captionDiv.innerHTML = c;
+ this._cachedNodeList = new
PNodeList(
+ this.pdoc, this,
captionDiv, {
+ update:
function() {
+
this.parent._caption = this.container.innerHTML;
+ },
+ });
+ }
+ }
+ }
+ return this._cachedNodeList;
+ },
+ set: function(v) {
+ this._cachedNodeList = undefined;
+ if (this._isBlock) {
+ var c = this.node.firstChild.nextSibling;
+ if (v===null || v===undefined) {
+ if (c) {
+ this.node.removeChild(c);
+ this.update();
+ }
+ } else {
+ if (!c) {
+ c =
this.ownerDocument.createElement('figcaption');
+ this.node.appendChild(c);
+ }
+ c.innerHTML = toHtmlStr(c, v);
+ this.update();
+ }
+ } else {
+ this._caption = (v===null || v===undefined) ? v
:
+ toHtmlStr(this.node, v);
+ this.update();
+ }
+ },
+ },
+
+ _children: { value: function() {
+ var c = this.caption;
+ return c ? [ c ] : [];
+ }, },
+});
+/** @private @static @ignore */
+PImage._selector = 'figure,[typeof~="mw:Image"]';
+
+
+/**
+ * PTag represents any otherwise-unmatched tag. This includes
+ * HTML-style tags in wikicode, like `<span>`, as well as some
+ * "invisible" tags like `<p>`.
+ * @class PTag
+ * @extends PNode
+ */
+/**
+ * @method constructor
+ * @private
+ * @inheritdoc PNode#constructor
+ */
+PTag = function PTag(pdoc, parent, node, opts) {
+ PNode.call(this, pdoc, parent, node, opts);
+};
+util.inherits(PTag, PNode);
+Object.defineProperties(PTag.prototype, {
+ /**
+ * The name of the tag, in lowercase.
+ */
+ tagName: {
+ get: function() { return this.node.tagName.toLowerCase(); },
+ },
+
+ /**
+ * The contents of the tag, as a {@PNodeList} object.
+ * You can assign a String, Node, or PNodeList to mutate the contents.
+ * @property {PNodeList}
+ */
+ contents: innerAccessor,
+
+ _children: { value: function() { return [this.contents]; }, },
+});
+/** @private @static @ignore */
+PTag._selector = '*'; // any otherwise-unmatched element
/**
* PTemplate represents a wikitext template, like `{{foo}}`.
@@ -459,7 +912,7 @@
* @param {String} name The template name to test against.
* @return {Boolean}
*/
- matches: {
+ nameMatches: {
value: function(name) {
var href = './' +
this.pdoc.env.normalizeTitle('Template:' + name);
return this._template.template.target.href === href;
@@ -589,162 +1042,19 @@
return this._cachedHtml[k];
},
},
-});
-// Helper: getter and setter for the inner contents of a node.
-var innerAccessor = {
- get: function() {
- return new PNodeList(this.pdoc, this, this.node);
- },
- set: function(v) {
- this.node.innerHTML = toHtmlStr(this.node, v);
- this.update();
- },
-};
-
-/**
- * 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();
- },
- },
+ _children: { value: function() {
+ var result = [];
+ this.params.forEach(function(k) {
+ var p = this.get(k);
+ if (p.key) { result.push(p.key); }
+ result.push(p.value);
+ }.bind(this));
+ return result;
+ }, },
});
-
-/**
- * PExtLink represents an external link, like `[http://example.com Example]`.
- * @class PExtLink
- * @extends PNode
- */
-/**
- * @method constructor
- * @private
- * @inheritdoc PNode#constructor
- */
-PExtLink = function PExtLink(pdoc, parent, node, opts) {
- PNode.call(this, pdoc, parent, node, opts);
-};
-util.inherits(PExtLink, PNode);
-Object.defineProperties(PExtLink.prototype, {
- /**
- * The URL of the link target.
- * @property {String}
- */
- url: {
- // XXX url should be a PNodeList, but that requires handling
- // typeof="mw:ExpandedAttrs"
- get: function() {
- return this.node.getAttribute('href');
- },
- set: function(v) {
- this.node.setAttribute('href', v);
- },
- },
- /**
- * The link title, as a {@link PNodeList}.
- * You can assign a String, Node, or PNodeList to mutate the title.
- * @property {PNodeList}
- */
- title: innerAccessor,
-});
-
-/**
- * PHeading represents a section heading in wikitext, like `== Foo ==`.
- * @class PHeading
- * @extends PNode
- */
-/**
- * @method constructor
- * @private
- * @inheritdoc PNode#constructor
- */
-PHeading = function PHeading(pdoc, parent, node, opts) {
- PNode.call(this, pdoc, parent, node, opts);
-};
-util.inherits(PHeading, PNode);
-Object.defineProperties(PHeading.prototype, {
- /**
- * The heading level, as an integer between 1 and 6 inclusive.
- * @property {Number}
- */
- level: {
- get: function() {
- return +this.node.nodeName.slice(1);
- },
- set: function(v) {
- v = +v;
- if (v === this.level) {
- return;
- } else if (v >= 1 && v <= 6) {
- var nh = this.ownerDocument.createElement('h' +
v);
- while (this.node.firstChild !== null) {
- nh.appendChild(this.node.firstChild);
- }
- this.node.parentNode.replaceChild(nh,
this.node);
- this.node = nh;
- this.update();
- } else {
- throw new Error("Level must be between 1 and 6,
inclusive.");
- }
- },
- },
- /**
- * The title of the heading, as a {@link PNodeList}.
- * You can assign a String, Node, or PNodeList to mutate the title.
- * @property {PNodeList}
- */
- title: innerAccessor,
-});
-
-/**
- * PHtmlEntity represents an HTML entity, like ` `.
- * @class PHtmlEntity
- * @extends PNode
- */
-/**
- * @method constructor
- * @private
- * @inheritdoc PNode#constructor
- */
-PHtmlEntity = function PHtmlEntity(pdoc, parent, node, opts) {
- PNode.call(this, pdoc, parent, node, opts);
-};
-util.inherits(PHtmlEntity, PNode);
-Object.defineProperties(PHtmlEntity.prototype, {
- /**
- * The character represented by the HTML entity.
- * @property {String}
- */
- normalized: {
- get: function() { return this.node.textContent; },
- set: function(v) {
- this.node.textContent = v;
- this.node.removeAttribute('data-parsoid');
- this.update();
- },
- },
-});
+/** @private @static @ignore */
+PTemplate._selector = '[typeof~="mw:Transclusion"]';
/**
* PText represents ordinary unformatted text with no special properties.
@@ -774,7 +1084,20 @@
this.update();
},
},
+ /**
+ * Extends {@link PNode#matches} to allow a target string to match
+ * if it matches this node's #value.
+ * @method
+ * @inheritdoc PNode#matches
+ * @param {Node|PNode|String} target
+ */
+ matches: { value: function(target) {
+ return PNode.prototype.matches.call(this, target) ||
+ this.value === target;
+ }, },
});
+/** @private @static @ignore */
+PText._selector = 'TEXT'; // non-standard selector
/**
* PWikiLink represents an internal wikilink, like `[[Foo|Bar]]`.
@@ -813,7 +1136,11 @@
* @property {PNodeList}
*/
text: innerAccessor,
+
+ _children: { value: function() { return [this.text]; }, },
});
+/** @private @static @ignore */
+PWikiLink._selector = 'a[rel="mw:WikiLink"]';
/**
* A PDoc object wraps an entire Parsoid document. Since it is an
@@ -860,4 +1187,14 @@
module.exports = {
PDoc: PDoc,
PNodeList: PNodeList,
+ PNode: PNode,
+ PComment: PComment,
+ PExtLink: PExtLink,
+ PHeading: PHeading,
+ PHtmlEntity: PHtmlEntity,
+ PImage: PImage,
+ PTag: PTag,
+ PTemplate: PTemplate,
+ PText: PText,
+ PWikiLink: PWikiLink,
};
diff --git a/tests/mocha/jsapi.js b/tests/mocha/jsapi.js
index 0f3b16d..af0cd2d 100644
--- a/tests/mocha/jsapi.js
+++ b/tests/mocha/jsapi.js
@@ -65,10 +65,10 @@
var text = "{{cleanup}} '''Foo''' is a [[bar]].
{{uncategorized}}";
return Parsoid.parse(text, { pdoc: true }).then(function(pdoc) {
pdoc.filterTemplates().forEach(function(template) {
- if (template.matches('Cleanup') &&
!template.has('date')) {
+ if (template.nameMatches('Cleanup') &&
!template.has('date')) {
template.add('date', 'July 2012');
}
- if (template.matches('uncategorized')) {
+ if (template.nameMatches('uncategorized')) {
template.name = 'bar-stub';
}
});
@@ -83,7 +83,7 @@
var text = "{{echo|{{cleanup}} '''Foo''' is a [[bar]].}}
{{uncategorized}}";
return Parsoid.parse(text, { pdoc: true }).then(function(pdoc) {
pdoc.filterTemplates().forEach(function(template) {
- if (template.matches('Cleanup') &&
!template.has('date')) {
+ if (template.nameMatches('Cleanup') &&
!template.has('date')) {
template.add('date', 'July 2012');
// Works even when there are special
characters
template.add('test1',
'{{foo}}&bar|bat<p>');
@@ -184,6 +184,22 @@
String(pdoc).should.equal('<!--<!-- ha! -->-->
{{echo|<!------>}}');
});
});
+ it('filters and mutates images', function() {
+ var text = '[[File:SomeFile1.jpg]]
[[File:SomeFile2.jpg|thumb|caption]]';
+ return Parsoid.parse(text, { pdoc: true }).then(function(pdoc) {
+ var images = pdoc.filterImages();
+ images.length.should.equal(2);
+ images[0].should.have.property('caption', null);
+ String(images[1].caption).should.equal('caption');
+ images[0].caption = '|';
+ images[1].caption = null;
+ // XXX Bug T107435
+
//String(pdoc).should.equal('[[File:SomeFile1.jpg|<nowiki>|</nowiki>]]
[[File:SomeFile2.jpg|thumb]]');
+ images[0].caption = null;
+ images[1].caption = '|';
+ String(pdoc).should.equal('[[File:SomeFile1.jpg]]
[[File:SomeFile2.jpg|thumb|<nowiki>|</nowiki>]]');
+ });
+ });
it('filters and mutates text', function() {
var text = 'foo {{echo|bar}}';
return Parsoid.parse(text, { pdoc: true }).then(function(pdoc) {
@@ -221,4 +237,34 @@
});
});
});
+ it('allows iteration using length and get()', function() {
+ var text = '== 1 ==\n[http://example.com 2]<!-- 3
--> {{echo|4}} 5 [[Foo|6]]';
+ return Parsoid.parse(text, { pdoc: true }).then(function(pdoc) {
+ pdoc.length.should.equal(3);
+ pdoc.get(0).should.be.instanceof(Parsoid.PHeading);
+ pdoc.get(1).should.be.instanceof(Parsoid.PText);
+ pdoc.get(2).should.be.instanceof(Parsoid.PTag);
+ pdoc.get(2).tagName.should.be.equal('p');
+ var paragraph = pdoc.get(2).contents;
+ paragraph.length.should.equal(6);
+ paragraph.get(0).should.be.instanceof(Parsoid.PExtLink);
+ paragraph.get(1).should.be.instanceof(Parsoid.PComment);
+
paragraph.get(2).should.be.instanceof(Parsoid.PHtmlEntity);
+
paragraph.get(3).should.be.instanceof(Parsoid.PTemplate);
+ paragraph.get(4).should.be.instanceof(Parsoid.PText);
+
paragraph.get(5).should.be.instanceof(Parsoid.PWikiLink);
+ // Test indexOf with PNodes and Nodes
+ for (var i = 0; i < paragraph.length; i++) {
+
paragraph.indexOf(paragraph.get(i)).should.equal(i);
+
paragraph.indexOf(paragraph.get(i).node).should.equal(i);
+ pdoc.indexOf(paragraph.get(i), { recursive:
true }).should.equal(2);
+ pdoc.indexOf(paragraph.get(i).node, {
recursive: true }).should.equal(2);
+ }
+ // Test indexOf with strings
+ pdoc.indexOf(' 5 ').should.equal(-1);
+ pdoc.indexOf(' 5 ', { recursive: true
}).should.equal(2);
+ paragraph.indexOf(' 5 ').should.equal(4);
+ paragraph.indexOf('\u00A0').should.equal(2);
+ });
+ });
});
--
To view, visit https://gerrit.wikimedia.org/r/228006
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I8a8a6899782ecbf0c0d011b5a0c490fa7d3b256d
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/services/parsoid
Gerrit-Branch: master
Gerrit-Owner: Cscott <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits