From 433284fa10891295e2da376c5330e0d1d0e526a6 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 31 May 2016 17:46:00 +0900 Subject: [PATCH 01/23] Support `target-counter()`/`target-counters()` for counters on elements - `target-counter()`/`target-counters()` support for counters on elements, i.e., defined by `counter-reset`/`counter-increment` specified on elements. - Page-based counters are not yet taken into account in this implementation. - `attr()` function cannot be used inside `target-counter()`/`target-counters()` yet. - If target element is in a source document other than the current one (on which `target-counter()`/`target-counters()` is specified), the target counter cannot be retrieved and fallback to 0. --- resources/validation.txt | 2 + src/adapt/csscasc.js | 126 +++++++++++++++++++++++++-- src/adapt/cssstyler.js | 30 ++++++- src/adapt/epub.js | 7 +- src/adapt/ops.js | 21 +++-- src/adapt/pm.js | 2 +- src/adapt/toc.js | 7 +- src/source-list.js | 1 + src/vivliostyle/counters.js | 153 +++++++++++++++++++++++++++++++++ test/files/index.html | 1 + test/files/target-counter.html | 47 ++++++++++ 11 files changed, 375 insertions(+), 22 deletions(-) create mode 100644 src/vivliostyle/counters.js create mode 100644 test/files/target-counter.html diff --git a/resources/validation.txt b/resources/validation.txt index 0777ed2c4..0be717e70 100644 --- a/resources/validation.txt +++ b/resources/validation.txt @@ -108,6 +108,8 @@ ATTR = attr(SPACE(IDENT TYPE_OR_UNIT_IN_ATTR?) [ STRING | IDENT | COLOR | INT | CONTENT = normal | none | [ STRING | URI | counter(IDENT LIST_STYLE_TYPE?) | counters(IDENT STRING LIST_STYLE_TYPE?) | ATTR | + target-counter([ STRING | URI ] IDENT LIST_STYLE_TYPE?) | + target-counters([ STRING | URI ] IDENT STRING LIST_STYLE_TYPE?) | open-quote | close-quote | no-open-quote | no-close-quote ]+; content = CONTENT; COUNTER = [ IDENT INT? ]+ | none; diff --git a/src/adapt/csscasc.js b/src/adapt/csscasc.js index ef74b1f98..55e479391 100644 --- a/src/adapt/csscasc.js +++ b/src/adapt/csscasc.js @@ -1732,6 +1732,38 @@ adapt.csscasc.QuotesScopeItem.prototype.pop = function(cascade, depth) { return false; }; +/** + * @interface + */ +adapt.csscasc.CounterListener = function() {}; + +/** + * @param {string} id + * @param {!Object>} counters + */ +adapt.csscasc.CounterListener.prototype.countersOfId = function(id, counters) {}; + +/** + * @interface + */ +adapt.csscasc.CounterResolver = function() {}; + +/** + * @param {string} url + * @param {string} name + * @param {function(?number):string} format + * @returns {!adapt.expr.Val} + */ +adapt.csscasc.CounterResolver.prototype.getTargetCounterVal = function(url, name, format) {}; + +/** + * @param {string} url + * @param {string} name + * @param {function(!Array):string} format + * @returns {!adapt.expr.Val} + */ +adapt.csscasc.CounterResolver.prototype.getTargetCountersVal = function(url, name, format) {}; + /** * Interface representing an object which can resolve a page-based counter by its name. * @interface @@ -1819,12 +1851,14 @@ adapt.csscasc.AttrValueFilterVisitor.prototype.visitFunc = function(func) { * @constructor * @param {adapt.csscasc.CascadeInstance} cascade * @param {Element} element + * @param {!adapt.csscasc.CounterResolver} counterResolver * @extends {adapt.css.FilterVisitor} */ -adapt.csscasc.ContentPropVisitor = function(cascade, element) { +adapt.csscasc.ContentPropVisitor = function(cascade, element, counterResolver) { adapt.css.FilterVisitor.call(this); this.cascade = cascade; this.element = element; + /** @const */ this.counterResolver = counterResolver; }; goog.inherits(adapt.csscasc.ContentPropVisitor, adapt.css.FilterVisitor); @@ -2152,6 +2186,58 @@ adapt.csscasc.ContentPropVisitor.prototype.visitFuncCounters = function(values) return new adapt.css.SpaceList([c]); }; +/** + * @param {Array.} values + * @return {adapt.css.Val} + */ +adapt.csscasc.ContentPropVisitor.prototype.visitFuncTargetCounter = function(values) { + var targetUrl = values[0]; + var targetUrlStr; + if (targetUrl instanceof adapt.css.URL) { + targetUrlStr = targetUrl.url; + } else { + targetUrlStr = targetUrl.stringValue(); + } + var counterName = values[1].toString(); + var type = values.length > 2 ? values[2].stringValue() : "decimal"; + + var self = this; + var c = new adapt.css.Expr(this.counterResolver.getTargetCounterVal(targetUrlStr, counterName, function(numval) { + return self.format(numval || 0, type); + })); + return new adapt.css.SpaceList([c]); +}; + +/** + * @param {Array} values + * @returns {adapt.css.Val} + */ +adapt.csscasc.ContentPropVisitor.prototype.visitFuncTargetCounters = function(values) { + var targetUrl = values[0]; + var targetUrlStr; + if (targetUrl instanceof adapt.css.URL) { + targetUrlStr = targetUrl.url; + } else { + targetUrlStr = targetUrl.stringValue(); + } + var counterName = values[1].toString(); + var separator = values[2].stringValue(); + var type = values.length > 3 ? values[3].stringValue() : "decimal"; + + var self = this; + var c = new adapt.css.Expr(this.counterResolver.getTargetCountersVal(targetUrlStr, counterName, function(numvals) { + var parts = numvals.map(function(numval) { + return self.format(numval, type); + }); + if (parts.length) { + return parts.join(separator); + } else { + return self.format(0, type); + } + })); + return new adapt.css.SpaceList([c]); +}; + /** * @override */ @@ -2167,6 +2253,16 @@ adapt.csscasc.ContentPropVisitor.prototype.visitFunc = function(func) { return this.visitFuncCounters(func.values); } break; + case "target-counter": + if (func.values.length <= 3) { + return this.visitFuncTargetCounter(func.values); + } + break; + case "target-counters": + if (func.values.length <= 4) { + return this.visitFuncTargetCounters(func.values); + } + break; } vivliostyle.logging.logger.warn("E_CSS_CONTENT_PROP:", func.toString()); return new adapt.css.Str(""); @@ -2245,11 +2341,13 @@ adapt.csscasc.Cascade.prototype.insertInTable = function(table, key, action) { /** * @param {adapt.expr.Context} context - * @param {adapt.csscasc.PageCounterResolver} pageCounterResolver + * @param {!adapt.csscasc.CounterListener} counterListener + * @param {!adapt.csscasc.CounterResolver} counterResolver + * @param {!adapt.csscasc.PageCounterResolver} pageCounterResolver * @return {adapt.csscasc.CascadeInstance} */ -adapt.csscasc.Cascade.prototype.createInstance = function(context, pageCounterResolver, lang) { - return new adapt.csscasc.CascadeInstance(this, context, pageCounterResolver, lang); +adapt.csscasc.Cascade.prototype.createInstance = function(context, counterListener, counterResolver, pageCounterResolver, lang) { + return new adapt.csscasc.CascadeInstance(this, context, counterListener, counterResolver, pageCounterResolver, lang); }; /** @@ -2263,13 +2361,17 @@ adapt.csscasc.Cascade.prototype.nextOrder = function() { /** * @param {adapt.csscasc.Cascade} cascade * @param {adapt.expr.Context} context - * @param {adapt.csscasc.PageCounterResolver} pageCounterResolver + * @param {!adapt.csscasc.CounterListener} counterListener + * @param {!adapt.csscasc.CounterResolver} counterResolver + * @param {!adapt.csscasc.PageCounterResolver} pageCounterResolver * @param {string} lang * @constructor */ -adapt.csscasc.CascadeInstance = function(cascade, context, pageCounterResolver, lang) { +adapt.csscasc.CascadeInstance = function(cascade, context, counterListener, counterResolver, pageCounterResolver, lang) { /** @const */ this.code = cascade; /** @const */ this.context = context; + /** @const */ this.counterListener = counterListener; + /** @const */ this.counterResolver = counterResolver; /** @const */ this.pageCounterResolver = pageCounterResolver; /** @const */ this.stack = /** @type {Array.>} */ ([[],[]]); /** @const */ this.conditions = /** @type {Object.} */ ({}); @@ -2285,7 +2387,7 @@ adapt.csscasc.CascadeInstance = function(cascade, context, pageCounterResolver, /** @type {?string} */ this.currentPageType = null; /** @type {boolean} */ this.isFirst = true; /** @type {boolean} */ this.isRoot = true; - /** @type {Object.>} */ this.counters = {}; + /** @type {!Object.>} */ this.counters = {}; /** @type {Array.>} */ this.counterScoping = [{}]; /** @type {Array.} */ this.quotes = [new adapt.css.Str("\u201C"), new adapt.css.Str("\u201D"), @@ -2469,7 +2571,7 @@ adapt.csscasc.CascadeInstance.prototype.processPseudoelementProps = function(pse this.pushCounters(pseudoprops); if (pseudoprops["content"]) { pseudoprops["content"] = pseudoprops["content"].filterValue( - new adapt.csscasc.ContentPropVisitor(this, element)); + new adapt.csscasc.ContentPropVisitor(this, element, this.counterResolver)); } this.popCounters(); }; @@ -2577,6 +2679,14 @@ adapt.csscasc.CascadeInstance.prototype.pushElement = function(element, baseStyl } } this.pushCounters(this.currentStyle); + var id = this.currentId || this.currentXmlId; + if (id) { + /** @type {!Object>} */ var counters = {}; + Object.keys(this.counters).forEach(function(name) { + counters[name] = Array.from(this.counters[name]); + }, this); + this.counterListener.countersOfId(id, counters); + } var pseudos = adapt.csscasc.getStyleMap(this.currentStyle, "_pseudos"); if (pseudos) { var before = true; diff --git a/src/adapt/cssstyler.js b/src/adapt/cssstyler.js index 760b1e972..4dda49114 100644 --- a/src/adapt/cssstyler.js +++ b/src/adapt/cssstyler.js @@ -423,11 +423,13 @@ adapt.cssstyler.BoxStack.prototype.nearestBlockStartOffset = function(box) { * @param {adapt.expr.Context} context * @param {Object.} primaryFlows * @param {adapt.cssvalid.ValidatorSet} validatorSet - * @param {adapt.csscasc.PageCounterResolver} pageCounterResolver + * @param {!adapt.csscasc.CounterListener} counterListener + * @param {!adapt.csscasc.CounterResolver} counterResolver + * @param {!adapt.csscasc.PageCounterResolver} pageCounterResolver * @constructor * @implements {adapt.cssstyler.AbstractStyler} */ -adapt.cssstyler.Styler = function(xmldoc, cascade, scope, context, primaryFlows, validatorSet, pageCounterResolver) { +adapt.cssstyler.Styler = function(xmldoc, cascade, scope, context, primaryFlows, validatorSet, counterListener, counterResolver, pageCounterResolver) { /** @const */ this.xmldoc = xmldoc; /** @const */ this.root = xmldoc.root; /** @const */ this.cascadeHolder = cascade; @@ -441,7 +443,8 @@ adapt.cssstyler.Styler = function(xmldoc, cascade, scope, context, primaryFlows, /** @const */ this.flowChunks = /** @type {Array.} */ ([]); /** @type {adapt.cssstyler.FlowListener} */ this.flowListener = null; /** @type {?string} */ this.flowToReach = null; - /** @const */ this.cascade = cascade.createInstance(context, pageCounterResolver, xmldoc.lang); + /** @type {?string} */ this.idToReach = null; + /** @const */ this.cascade = cascade.createInstance(context, counterListener, counterResolver, pageCounterResolver, xmldoc.lang); /** @const */ this.offsetMap = new adapt.cssstyler.SlipMap(); /** @type {boolean} */ this.primary = true; /** @const */ this.primaryStack = /** @type {Array.} */ ([]); @@ -695,6 +698,23 @@ adapt.cssstyler.Styler.prototype.styleUntilFlowIsReached = function(flowName) { } }; +/** + * @param {string} id + */ +adapt.cssstyler.Styler.prototype.styleUntilIdIsReached = function(id) { + if (!id) return; + this.idToReach = id; + var offset = 0; + while (true) { + if (!this.idToReach) + break; + offset += 5000; + if (this.styleUntil(offset, 0) === Number.POSITIVE_INFINITY) + break; + } + this.idToReach = null; +}; + /** * @private * @param {string} flowName @@ -829,6 +849,10 @@ adapt.cssstyler.Styler.prototype.styleUntil = function(startOffset, lookup) { var style = this.getAttrStyle(elem); this.primaryStack.push(this.primary); this.cascade.pushElement(elem, style); + var id = elem.getAttribute("id") || elem.getAttributeNS(adapt.base.NS.XML, "id"); + if (id && id === this.idToReach) { + this.idToReach = null; + } if (!this.bodyReached && elem.localName == "body" && elem.parentNode == this.root) { this.postprocessTopStyle(style, true); this.bodyReached = true; diff --git a/src/adapt/epub.js b/src/adapt/epub.js index 2ff5f55f3..a70a63d7a 100644 --- a/src/adapt/epub.js +++ b/src/adapt/epub.js @@ -11,6 +11,7 @@ goog.require('adapt.net'); goog.require('adapt.xmldoc'); goog.require('adapt.csscasc'); goog.require('adapt.font'); +goog.require('vivliostyle.counters'); goog.require('adapt.ops'); goog.require('adapt.cfi'); goog.require('adapt.sha1'); @@ -968,6 +969,7 @@ adapt.epub.OPFView = function(opf, viewport, fontMapper, pref, pageSheetSizeRepo /** @type {number} */ this.offsetInItem = 0; /** @const */ this.pref = adapt.expr.clonePreferences(pref); /** @const */ this.clientLayout = new adapt.vgen.DefaultClientLayout(viewport.window); + /** @const */ this.counterStore = new vivliostyle.counters.CounterStore(opf.documentURLTransformer); }; /** @@ -1668,7 +1670,7 @@ adapt.epub.OPFView.prototype.getPageViewItem = function() { var instance = new adapt.ops.StyleInstance(style, xmldoc, self.opf.lang, viewport, self.clientLayout, self.fontMapper, customRenderer, self.opf.fallbackMap, pageNumberOffset, - self.opf.documentURLTransformer); + self.opf.documentURLTransformer, self.counterStore); if (previousViewItem) { instance.pageCounterStore.copyFrom(previousViewItem.instance.pageCounterStore); @@ -1758,7 +1760,8 @@ adapt.epub.OPFView.prototype.showTOC = function(autohide) { = adapt.task.newFrame("showTOC"); if (!this.tocView) { this.tocView = new adapt.toc.TOCView(opf.store, toc.src, opf.lang, - this.clientLayout, this.fontMapper, this.pref, this, opf.fallbackMap, opf.documentURLTransformer); + this.clientLayout, this.fontMapper, this.pref, this, opf.fallbackMap, opf.documentURLTransformer, + this.counterStore); } var viewport = this.viewport; var tocWidth = Math.min(350, Math.round(0.67 * viewport.width) - 16); diff --git a/src/adapt/ops.js b/src/adapt/ops.js index c8a629850..a71b4085d 100644 --- a/src/adapt/ops.js +++ b/src/adapt/ops.js @@ -18,6 +18,7 @@ goog.require('adapt.cssvalid'); goog.require('adapt.csscasc'); goog.require('adapt.cssstyler'); goog.require('adapt.pm'); +goog.require('vivliostyle.counters'); goog.require('adapt.vtree'); goog.require('vivliostyle.pagefloat'); goog.require('adapt.layout'); @@ -149,6 +150,7 @@ adapt.ops.Style.prototype.sizeViewport = function(viewportWidth, viewportHeight, * @param {Object.} fallbackMap * @param {number} pageNumberOffset * @param {!adapt.base.DocumentURLTransformer} documentURLTransformer + * @param {!vivliostyle.counters.CounterStore} counterStore * @constructor * @extends {adapt.expr.Context} * @implements {adapt.cssstyler.FlowListener} @@ -156,7 +158,7 @@ adapt.ops.Style.prototype.sizeViewport = function(viewportWidth, viewportHeight, * @implements {adapt.vgen.StylerProducer} */ adapt.ops.StyleInstance = function(style, xmldoc, defaultLang, viewport, clientLayout, - fontMapper, customRenderer, fallbackMap, pageNumberOffset, documentURLTransformer) { + fontMapper, customRenderer, fallbackMap, pageNumberOffset, documentURLTransformer, counterStore) { adapt.expr.Context.call(this, style.rootScope, viewport.width, viewport.height, viewport.fontSize); /** @const */ this.style = style; /** @const */ this.xmldoc = xmldoc; @@ -174,6 +176,7 @@ adapt.ops.StyleInstance = function(style, xmldoc, defaultLang, viewport, clientL /** @const */ this.faces = new adapt.font.DocumentFaces(this.style.fontDeobfuscator); /** @type {Object.} */ this.pageBoxInstances = {}; /** @type {vivliostyle.page.PageManager} */ this.pageManager = null; + /** @const */ this.counterStore = counterStore; /** @const @type {!vivliostyle.page.PageCounterStore} */ this.pageCounterStore = new vivliostyle.page.PageCounterStore(style.pageScope); /** @type {boolean} */ this.regionBreak = false; /** @type {!Object.} */ this.pageBreaks = {}; @@ -206,8 +209,12 @@ adapt.ops.StyleInstance.prototype.init = function() { var self = this; /** @type {!adapt.task.Frame.} */ var frame = adapt.task.newFrame("StyleInstance.init"); - self.styler = new adapt.cssstyler.Styler(self.xmldoc, self.style.cascade, - self.style.rootScope, self, this.primaryFlows, self.style.validatorSet, this.pageCounterStore); + var counterListener = self.counterStore.createCounterListener(self.xmldoc.url); + var counterResolver = self.counterStore.createCounterResolver(self.xmldoc.url, self.style.rootScope); + self.styler = new adapt.cssstyler.Styler(self.xmldoc, self.style.cascade, + self.style.rootScope, self, this.primaryFlows, self.style.validatorSet, counterListener, counterResolver, + this.pageCounterStore); + counterResolver.setStyler(self.styler); self.styler.resetFlowChunkStream(self); self.stylerMap = {}; self.stylerMap[self.xmldoc.url] = self.styler; @@ -215,7 +222,7 @@ adapt.ops.StyleInstance.prototype.init = function() { self.pageProgression = vivliostyle.page.resolvePageProgression(docElementStyle); var rootBox = this.style.rootBox; this.rootPageBoxInstance = new adapt.pm.RootPageBoxInstance(rootBox); - var cascadeInstance = this.style.cascade.createInstance(self, this.pageCounterStore, this.lang); + var cascadeInstance = this.style.cascade.createInstance(self, counterListener, counterResolver, this.pageCounterStore, this.lang); this.rootPageBoxInstance.applyCascadeAndInit(cascadeInstance, docElementStyle); this.rootPageBoxInstance.resolveAutoSizing(self); this.pageManager = new vivliostyle.page.PageManager(cascadeInstance, this.style.pageScope, this.rootPageBoxInstance, self, docElementStyle); @@ -253,8 +260,10 @@ adapt.ops.StyleInstance.prototype.getStylerForDoc = function(xmldoc) { var style = this.style.store.getStyleForDoc(xmldoc); // We need a separate content, so that variables can get potentially different values. var context = new adapt.expr.Context(style.rootScope, this.pageWidth(), this.pageHeight(), this.initialFontSize); - styler = new adapt.cssstyler.Styler(xmldoc, style.cascade, - style.rootScope, context, this.primaryFlows, style.validatorSet, this.pageCounterStore); + var counterListener = this.counterStore.createCounterListener(xmldoc.url); + var counterResolver = this.counterStore.createCounterResolver(xmldoc.url, style.rootScope); + styler = new adapt.cssstyler.Styler(xmldoc, style.cascade, + style.rootScope, context, this.primaryFlows, style.validatorSet, counterListener, counterResolver, this.pageCounterStore); this.stylerMap[xmldoc.url] = styler; } return styler; diff --git a/src/adapt/pm.js b/src/adapt/pm.js index 2b5b71706..78f3a9f5f 100644 --- a/src/adapt/pm.js +++ b/src/adapt/pm.js @@ -1359,7 +1359,7 @@ adapt.pm.PageBoxInstance.prototype.applyCascadeAndInit = function(cascade, docEl cascade.pushRule(this.pageBox.classes, null, style); if (style["content"]) { style["content"] = style["content"].filterValue( - new adapt.csscasc.ContentPropVisitor(cascade, null)); + new adapt.csscasc.ContentPropVisitor(cascade, null, cascade.counterResolver)); } this.init(cascade.context); for (var i = 0; i < this.pageBox.children.length ; i++) { diff --git a/src/adapt/toc.js b/src/adapt/toc.js index 61e76857c..f04abb28c 100644 --- a/src/adapt/toc.js +++ b/src/adapt/toc.js @@ -9,6 +9,7 @@ goog.require('adapt.vgen'); goog.require('adapt.ops'); goog.require('adapt.font'); goog.require('adapt.expr'); +goog.require('vivliostyle.counters'); // closed: 25B8 // open: 25BE @@ -31,11 +32,12 @@ adapt.toc.bulletEmpty = "\u25B9"; * @param {adapt.vgen.CustomRendererFactory} rendererFactory * @param {Object.} fallbackMap * @param {!adapt.base.DocumentURLTransformer} documentURLTransformer + * @param {!vivliostyle.counters.CounterStore} counterStore * @constructor * @implements {adapt.vgen.CustomRendererFactory} */ adapt.toc.TOCView = function(store, url, lang, clientLayout, fontMapper, pref, - rendererFactory, fallbackMap, documentURLTransformer) { + rendererFactory, fallbackMap, documentURLTransformer, counterStore) { /** @const */ this.store = store; /** @const */ this.url = url; /** @const */ this.lang = lang; @@ -45,6 +47,7 @@ adapt.toc.TOCView = function(store, url, lang, clientLayout, fontMapper, pref, /** @const */ this.rendererFactory = rendererFactory; /** @const */ this.fallbackMap = fallbackMap; /** @const */ this.documentURLTransformer = documentURLTransformer; + /** @const */ this.counterStore = counterStore; /** @type {adapt.vtree.Page} */ this.page = null; /** @type {adapt.ops.StyleInstance} */ this.instance = null; }; @@ -177,7 +180,7 @@ adapt.toc.TOCView.prototype.showTOC = function(elem, viewport, width, height, fo var customRenderer = self.makeCustomRenderer(xmldoc); var instance = new adapt.ops.StyleInstance(style, xmldoc, self.lang, viewport, self.clientLayout, self.fontMapper, customRenderer, self.fallbackMap, 0, - self.documentURLTransformer); + self.documentURLTransformer, self.counterStore); self.instance = instance; instance.pref = self.pref; instance.init().then(function() { diff --git a/src/source-list.js b/src/source-list.js index d068f34d8..a5e869133 100644 --- a/src/source-list.js +++ b/src/source-list.js @@ -45,6 +45,7 @@ "adapt/vgen.js", "adapt/layout.js", "vivliostyle/page.js", + "vivliostyle/counters.js", "adapt/ops.js", "adapt/cfi.js", "adapt/toc.js", diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js new file mode 100644 index 000000000..beb981e37 --- /dev/null +++ b/src/vivliostyle/counters.js @@ -0,0 +1,153 @@ +/** + * Copyright 2016 Vivliostyle Inc. + * @fileoverview Counters + */ + +goog.provide("vivliostyle.counters"); + +goog.require("adapt.base"); +goog.require("adapt.expr"); +goog.require("adapt.csscasc"); +goog.require("adapt.cssstyler"); + +goog.scope(function() { + + /** + * + * @param {!adapt.base.DocumentURLTransformer} documentURLTransformer + * @param {!Object>>} countersById + * @param {string} baseURL + * @constructor + * @implements {adapt.csscasc.CounterListener} + */ + function CounterListener(documentURLTransformer, countersById, baseURL) { + /** @const */ this.documentURLTransformer = documentURLTransformer; + /** @const */ this.countersById = countersById; + /** @const */ this.baseURL = baseURL; + } + + /** + * @override + */ + CounterListener.prototype.countersOfId = function(id, counters) { + id = this.documentURLTransformer.transformFragment(id, this.baseURL); + this.countersById[id] = counters; + }; + + /** + * + * @param {!adapt.base.DocumentURLTransformer} documentURLTransformer + * @param {!Object>>} countersById + * @param {string} baseURL + * @param {adapt.expr.LexicalScope} rootScope + * @constructor + * @implements {adapt.csscasc.CounterResolver} + */ + function CounterResolver(documentURLTransformer, countersById, baseURL, rootScope) { + /** @const */ this.documentURLTransformer = documentURLTransformer; + /** @const */ this.countersById = countersById; + /** @const */ this.baseURL = baseURL; + /** @const */ this.rootScope = rootScope; + /** @type {?adapt.cssstyler.Styler} */ this.styler = null; + } + + /** + * + * @param {!adapt.cssstyler.Styler} styler + */ + CounterResolver.prototype.setStyler = function(styler) { + this.styler = styler; + }; + + /** + * @private + * @param {string} url + * @returns {?string} + */ + CounterResolver.prototype.getFragment = function(url) { + var r = url.match(/^[^#]*#(.*)$/); + return r ? r[1] : null; + }; + + /** + * @private + * @param {string} url + * @returns {string} + */ + CounterResolver.prototype.getTransformedId = function(url) { + var transformedId = this.documentURLTransformer.transformURL(url, this.baseURL); + if (transformedId.charAt(0) === "#") { + transformedId = transformedId.substring(1); + } + return transformedId; + }; + + /** + * @override + */ + CounterResolver.prototype.getTargetCounterVal = function(url, name, format) { + var id = this.getFragment(url); + var transformedId = this.getTransformedId(url); + var self = this; + var scope = this.rootScope; + return new adapt.expr.Native(scope, function() { + var targetCounters = self.countersById[transformedId]; + if (!targetCounters && id) { + self.styler.styleUntilIdIsReached(id); + targetCounters = self.countersById[transformedId]; + } + var arr = targetCounters && targetCounters[name]; + var numval = (arr && arr.length && arr[arr.length - 1]) || null; + return format(numval); + }, "target-counter-" + name + "-of-#" + transformedId); + }; + + /** + * @override + */ + CounterResolver.prototype.getTargetCountersVal = function(url, name, format) { + var id = this.getFragment(url); + var transformedId = this.getTransformedId(url); + var self = this; + var scope = this.rootScope; + return new adapt.expr.Native(scope, function() { + var targetCounters = self.countersById[transformedId]; + if (!targetCounters && id) { + self.styler.styleUntilIdIsReached(id); + targetCounters = self.countersById[transformedId]; + } + var arr = targetCounters && targetCounters[name]; + return format(arr || []); + }, "target-counters-" + name + "-of-#" + transformedId); + }; + + /** + * + * @param {!adapt.base.DocumentURLTransformer} documentURLTransformer + * @constructor + */ + vivliostyle.counters.CounterStore = function(documentURLTransformer) { + /** @const */ this.documentURLTransformer = documentURLTransformer; + /** @const {!Object.>>} */ this.countersById = {}; + }; + + /** + * + * @param {string} baseURL + * @returns {!adapt.csscasc.CounterListener} + */ + vivliostyle.counters.CounterStore.prototype.createCounterListener = function(baseURL) { + return new CounterListener(this.documentURLTransformer, this.countersById, baseURL); + }; + + /** + * + * @param {string} baseURL + * @param {adapt.expr.LexicalScope} rootScope + * @returns {!adapt.csscasc.CounterResolver} + */ + vivliostyle.counters.CounterStore.prototype.createCounterResolver = function(baseURL, rootScope) { + return new CounterResolver(this.documentURLTransformer, this.countersById, baseURL, rootScope); + }; + +}); diff --git a/test/files/index.html b/test/files/index.html index 48a5d3abb..a3bc19ffd 100644 --- a/test/files/index.html +++ b/test/files/index.html @@ -42,6 +42,7 @@
  • Float text offset bug [dev|prod]
  • Content attr() [dev|prod]
  • Floats with position: relative [dev|prod]
  • +
  • target-counter [dev|prod]
  • Page breaks

    diff --git a/test/files/target-counter.html b/test/files/target-counter.html new file mode 100644 index 000000000..7842d1546 --- /dev/null +++ b/test/files/target-counter.html @@ -0,0 +1,47 @@ + + + + + target-counter + + + +

    'foo' value of #foo-target = =

    +

    'foo' values of #foofoo-target = =

    +

    foo=

    +

    foo=

    +

    (#foo-target) foo=

    +

    foos=, foos=, foo=

    +

    foo=

    +

    'foo' value of #foo-target = =

    +

    'foo' values of #foofoo-target = =

    + + \ No newline at end of file From eb480492eac4aca8c732cca69b40883e60418d94 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 31 May 2016 19:29:55 +0900 Subject: [PATCH 02/23] Support `attr()` function in the first argument of `target-counter()`/`target-counters()` function --- resources/validation.txt | 2 ++ test/files/target-counter.html | 39 +++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/resources/validation.txt b/resources/validation.txt index 0be717e70..c279b3bdf 100644 --- a/resources/validation.txt +++ b/resources/validation.txt @@ -109,7 +109,9 @@ CONTENT = normal | none | [ STRING | URI | counter(IDENT LIST_STYLE_TYPE?) | counters(IDENT STRING LIST_STYLE_TYPE?) | ATTR | target-counter([ STRING | URI ] IDENT LIST_STYLE_TYPE?) | + target-counter(ATTR IDENT LIST_STYLE_TYPE?) | target-counters([ STRING | URI ] IDENT STRING LIST_STYLE_TYPE?) | + target-counters(ATTR IDENT STRING LIST_STYLE_TYPE?) | open-quote | close-quote | no-open-quote | no-close-quote ]+; content = CONTENT; COUNTER = [ IDENT INT? ]+ | none; diff --git a/test/files/target-counter.html b/test/files/target-counter.html index 7842d1546..f815d1a66 100644 --- a/test/files/target-counter.html +++ b/test/files/target-counter.html @@ -31,6 +31,34 @@ .ref-foofoo::after { content: target-counters("#foofoo-target", foo, "-", lower-roman); } + + .bar { + counter-increment: bar; + } + .bar::after { + content: counter(bar); + } + .barbar:first-child { + counter-reset: bar; + } + .barbar { + counter-increment: bar; + } + .barbar::after { + content: counters(bar, "-"); + } + [data-ref-bar]::before { + content: target-counter(attr(data-ref-bar url), bar); + } + [data-ref-bar]::after { + content: target-counter(attr(data-ref-bar), bar, lower-roman); + } + [data-ref-barbar]::before { + content: target-counters(attr(data-ref-barbar url), bar, "-"); + } + [data-ref-barbar]::after { + content: target-counters(attr(data-ref-barbar), bar, "-", lower-roman); + } @@ -39,9 +67,18 @@

    foo=

    foo=

    (#foo-target) foo=

    -

    foos=, foos=, foo=

    +

    foos=, (#foofoo-target) foos=, foo=

    foo=

    'foo' value of #foo-target = =

    'foo' values of #foofoo-target = =

    +

    'bar' value of #bar-target = =

    +

    'bar' values of #barbar-target = =

    +

    bar=

    +

    bar=

    +

    (#bar-target) bar=

    +

    bars=, (#barbar-target) bars=, bar=

    +

    bar=

    +

    'bar' value of #bar-target = =

    +

    'bar' values of #barbar-target = =

    \ No newline at end of file From 6b431200f3dfef4d800efc3de560bc3c7d8a8c0e Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 31 May 2016 20:13:17 +0900 Subject: [PATCH 03/23] Merge functionalities of `vivliostyle.page.PageCounterStore` into `vivliostyle.counters.CounterStore` - `adapt.csscasc.PageCounterResolver` interface has been removed. Its methods are merged into `adapt.csscasc.CounterResolver`. - `vivliostyle.page.PageCounterStore` class has bee removed. Its functionalities are merged into `vivliostyle.counters.CounterStore`. --- src/adapt/csscasc.js | 51 +++++++---------- src/adapt/cssstyler.js | 5 +- src/adapt/epub.js | 3 - src/adapt/ops.js | 14 ++--- src/vivliostyle/counters.js | 101 +++++++++++++++++++++++++++++++-- src/vivliostyle/page.js | 109 ------------------------------------ 6 files changed, 126 insertions(+), 157 deletions(-) diff --git a/src/adapt/csscasc.js b/src/adapt/csscasc.js index 55e479391..fae5c17e5 100644 --- a/src/adapt/csscasc.js +++ b/src/adapt/csscasc.js @@ -1748,6 +1748,22 @@ adapt.csscasc.CounterListener.prototype.countersOfId = function(id, counters) {} */ adapt.csscasc.CounterResolver = function() {}; +/** + * Returns an adapt.expr.Val, whose value is calculated at the layout time by retrieving the innermost page-based counter (null if it does not exist) by its name and formatting the value into a string. + * @param {string} name Name of the page-based counter to be retrieved + * @param {function(?number):string} format A function that formats the counter value into a string + * @returns {adapt.expr.Val} + */ +adapt.csscasc.CounterResolver.prototype.getPageCounterVal = function(name, format) {}; + +/** + * Returns an adapt.expr.Val, whose value is calculated at the layout time by retrieving the page-based counters by its name and formatting the values into a string. + * @param {string} name Name of the page-based counters to be retrieved + * @param {function(!Array.):string} format A function that formats the counter values (passed as an array ordered by the nesting depth with the outermost counter first and the innermost last) into a string + * @returns {adapt.expr.Val} + */ +adapt.csscasc.CounterResolver.prototype.getPageCountersVal = function(name, format) {}; + /** * @param {string} url * @param {string} name @@ -1764,28 +1780,6 @@ adapt.csscasc.CounterResolver.prototype.getTargetCounterVal = function(url, name */ adapt.csscasc.CounterResolver.prototype.getTargetCountersVal = function(url, name, format) {}; -/** - * Interface representing an object which can resolve a page-based counter by its name. - * @interface - */ -adapt.csscasc.PageCounterResolver = function() {}; - -/** - * Returns an adapt.expr.Val, whose value is calculated at the layout time by retrieving the innermost page-based counter (null if it does not exist) by its name and formatting the value into a string. - * @param {string} name Name of the page-based counter to be retrieved - * @param {function(?number):string} format A function that formats the counter value into a string - * @returns {adapt.expr.Val} - */ -adapt.csscasc.PageCounterResolver.prototype.getCounterVal = function(name, format) {}; - -/** - * Returns an adapt.expr.Val, whose value is calculated at the layout time by retrieving the page-based counters by its name and formatting the values into a string. - * @param {string} name Name of the page-based counters to be retrieved - * @param {function(!Array.):string} format A function that formats the counter values (passed as an array ordered by the nesting depth with the outermost counter first and the innermost last) into a string - * @returns {adapt.expr.Val} - */ -adapt.csscasc.PageCounterResolver.prototype.getCountersVal = function(name, format) {}; - /** * @constructor * @param {Element} element @@ -2141,7 +2135,7 @@ adapt.csscasc.ContentPropVisitor.prototype.visitFuncCounter = function(values) { return new adapt.css.Str(this.format(numval, type)); } else { var self = this; - var c = new adapt.css.Expr(this.cascade.pageCounterResolver.getCounterVal(counterName, function(numval) { + var c = new adapt.css.Expr(this.counterResolver.getPageCounterVal(counterName, function(numval) { return self.format(numval || 0, type); })); return new adapt.css.SpaceList([c]); @@ -2166,7 +2160,7 @@ adapt.csscasc.ContentPropVisitor.prototype.visitFuncCounters = function(values) } } var self = this; - var c = new adapt.css.Expr(this.cascade.pageCounterResolver.getCountersVal(counterName, function(numvals) { + var c = new adapt.css.Expr(this.counterResolver.getPageCountersVal(counterName, function(numvals) { var parts = /** @type {Array.} */ ([]); if (numvals.length) { for (var i = 0; i < numvals.length; i++) { @@ -2343,11 +2337,10 @@ adapt.csscasc.Cascade.prototype.insertInTable = function(table, key, action) { * @param {adapt.expr.Context} context * @param {!adapt.csscasc.CounterListener} counterListener * @param {!adapt.csscasc.CounterResolver} counterResolver - * @param {!adapt.csscasc.PageCounterResolver} pageCounterResolver * @return {adapt.csscasc.CascadeInstance} */ -adapt.csscasc.Cascade.prototype.createInstance = function(context, counterListener, counterResolver, pageCounterResolver, lang) { - return new adapt.csscasc.CascadeInstance(this, context, counterListener, counterResolver, pageCounterResolver, lang); +adapt.csscasc.Cascade.prototype.createInstance = function(context, counterListener, counterResolver, lang) { + return new adapt.csscasc.CascadeInstance(this, context, counterListener, counterResolver, lang); }; /** @@ -2363,16 +2356,14 @@ adapt.csscasc.Cascade.prototype.nextOrder = function() { * @param {adapt.expr.Context} context * @param {!adapt.csscasc.CounterListener} counterListener * @param {!adapt.csscasc.CounterResolver} counterResolver - * @param {!adapt.csscasc.PageCounterResolver} pageCounterResolver * @param {string} lang * @constructor */ -adapt.csscasc.CascadeInstance = function(cascade, context, counterListener, counterResolver, pageCounterResolver, lang) { +adapt.csscasc.CascadeInstance = function(cascade, context, counterListener, counterResolver, lang) { /** @const */ this.code = cascade; /** @const */ this.context = context; /** @const */ this.counterListener = counterListener; /** @const */ this.counterResolver = counterResolver; - /** @const */ this.pageCounterResolver = pageCounterResolver; /** @const */ this.stack = /** @type {Array.>} */ ([[],[]]); /** @const */ this.conditions = /** @type {Object.} */ ({}); /** @type {Element} */ this.currentElement = null; diff --git a/src/adapt/cssstyler.js b/src/adapt/cssstyler.js index 4dda49114..76197ca5c 100644 --- a/src/adapt/cssstyler.js +++ b/src/adapt/cssstyler.js @@ -425,11 +425,10 @@ adapt.cssstyler.BoxStack.prototype.nearestBlockStartOffset = function(box) { * @param {adapt.cssvalid.ValidatorSet} validatorSet * @param {!adapt.csscasc.CounterListener} counterListener * @param {!adapt.csscasc.CounterResolver} counterResolver - * @param {!adapt.csscasc.PageCounterResolver} pageCounterResolver * @constructor * @implements {adapt.cssstyler.AbstractStyler} */ -adapt.cssstyler.Styler = function(xmldoc, cascade, scope, context, primaryFlows, validatorSet, counterListener, counterResolver, pageCounterResolver) { +adapt.cssstyler.Styler = function(xmldoc, cascade, scope, context, primaryFlows, validatorSet, counterListener, counterResolver) { /** @const */ this.xmldoc = xmldoc; /** @const */ this.root = xmldoc.root; /** @const */ this.cascadeHolder = cascade; @@ -444,7 +443,7 @@ adapt.cssstyler.Styler = function(xmldoc, cascade, scope, context, primaryFlows, /** @type {adapt.cssstyler.FlowListener} */ this.flowListener = null; /** @type {?string} */ this.flowToReach = null; /** @type {?string} */ this.idToReach = null; - /** @const */ this.cascade = cascade.createInstance(context, counterListener, counterResolver, pageCounterResolver, xmldoc.lang); + /** @const */ this.cascade = cascade.createInstance(context, counterListener, counterResolver, xmldoc.lang); /** @const */ this.offsetMap = new adapt.cssstyler.SlipMap(); /** @type {boolean} */ this.primary = true; /** @const */ this.primaryStack = /** @type {Array.} */ ([]); diff --git a/src/adapt/epub.js b/src/adapt/epub.js index a70a63d7a..d5e4adc1e 100644 --- a/src/adapt/epub.js +++ b/src/adapt/epub.js @@ -1672,9 +1672,6 @@ adapt.epub.OPFView.prototype.getPageViewItem = function() { viewport, self.clientLayout, self.fontMapper, customRenderer, self.opf.fallbackMap, pageNumberOffset, self.opf.documentURLTransformer, self.counterStore); - if (previousViewItem) { - instance.pageCounterStore.copyFrom(previousViewItem.instance.pageCounterStore); - } instance.pref = self.pref; instance.init().then(function() { viewItem = {item: item, xmldoc: xmldoc, instance: instance, diff --git a/src/adapt/ops.js b/src/adapt/ops.js index a71b4085d..a448e628b 100644 --- a/src/adapt/ops.js +++ b/src/adapt/ops.js @@ -177,7 +177,6 @@ adapt.ops.StyleInstance = function(style, xmldoc, defaultLang, viewport, clientL /** @type {Object.} */ this.pageBoxInstances = {}; /** @type {vivliostyle.page.PageManager} */ this.pageManager = null; /** @const */ this.counterStore = counterStore; - /** @const @type {!vivliostyle.page.PageCounterStore} */ this.pageCounterStore = new vivliostyle.page.PageCounterStore(style.pageScope); /** @type {boolean} */ this.regionBreak = false; /** @type {!Object.} */ this.pageBreaks = {}; /** @type {?vivliostyle.constants.PageProgression} */ this.pageProgression = null; @@ -210,10 +209,9 @@ adapt.ops.StyleInstance.prototype.init = function() { /** @type {!adapt.task.Frame.} */ var frame = adapt.task.newFrame("StyleInstance.init"); var counterListener = self.counterStore.createCounterListener(self.xmldoc.url); - var counterResolver = self.counterStore.createCounterResolver(self.xmldoc.url, self.style.rootScope); + var counterResolver = self.counterStore.createCounterResolver(self.xmldoc.url, self.style.rootScope, self.style.pageScope); self.styler = new adapt.cssstyler.Styler(self.xmldoc, self.style.cascade, - self.style.rootScope, self, this.primaryFlows, self.style.validatorSet, counterListener, counterResolver, - this.pageCounterStore); + self.style.rootScope, self, this.primaryFlows, self.style.validatorSet, counterListener, counterResolver); counterResolver.setStyler(self.styler); self.styler.resetFlowChunkStream(self); self.stylerMap = {}; @@ -222,7 +220,7 @@ adapt.ops.StyleInstance.prototype.init = function() { self.pageProgression = vivliostyle.page.resolvePageProgression(docElementStyle); var rootBox = this.style.rootBox; this.rootPageBoxInstance = new adapt.pm.RootPageBoxInstance(rootBox); - var cascadeInstance = this.style.cascade.createInstance(self, counterListener, counterResolver, this.pageCounterStore, this.lang); + var cascadeInstance = this.style.cascade.createInstance(self, counterListener, counterResolver, this.lang); this.rootPageBoxInstance.applyCascadeAndInit(cascadeInstance, docElementStyle); this.rootPageBoxInstance.resolveAutoSizing(self); this.pageManager = new vivliostyle.page.PageManager(cascadeInstance, this.style.pageScope, this.rootPageBoxInstance, self, docElementStyle); @@ -261,9 +259,9 @@ adapt.ops.StyleInstance.prototype.getStylerForDoc = function(xmldoc) { // We need a separate content, so that variables can get potentially different values. var context = new adapt.expr.Context(style.rootScope, this.pageWidth(), this.pageHeight(), this.initialFontSize); var counterListener = this.counterStore.createCounterListener(xmldoc.url); - var counterResolver = this.counterStore.createCounterResolver(xmldoc.url, style.rootScope); + var counterResolver = this.counterStore.createCounterResolver(xmldoc.url, style.rootScope, style.pageScope); styler = new adapt.cssstyler.Styler(xmldoc, style.cascade, - style.rootScope, context, this.primaryFlows, style.validatorSet, counterListener, counterResolver, this.pageCounterStore); + style.rootScope, context, this.primaryFlows, style.validatorSet, counterListener, counterResolver); this.stylerMap[xmldoc.url] = styler; } return styler; @@ -874,7 +872,7 @@ adapt.ops.StyleInstance.prototype.layoutNextPage = function(page, cp) { if (pageMaster.pageBox.specified["height"].value === adapt.css.fullHeight) { page.setAutoPageHeight(true); } - self.pageCounterStore.updatePageCounters(cascadedPageStyle, self); + self.counterStore.updatePageCounters(cascadedPageStyle, self); // setup bleed area and crop marks var evaluatedPageSizeAndBleed = vivliostyle.page.evaluatePageSizeAndBleed( diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index beb981e37..cf280d426 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -38,16 +38,20 @@ goog.scope(function() { * * @param {!adapt.base.DocumentURLTransformer} documentURLTransformer * @param {!Object>>} countersById + * @param {!Object.>} currentPageCounters * @param {string} baseURL * @param {adapt.expr.LexicalScope} rootScope + * @param {adapt.expr.LexicalScope} pageScope * @constructor * @implements {adapt.csscasc.CounterResolver} */ - function CounterResolver(documentURLTransformer, countersById, baseURL, rootScope) { + function CounterResolver(documentURLTransformer, countersById, currentPageCounters, baseURL, rootScope, pageScope) { /** @const */ this.documentURLTransformer = documentURLTransformer; /** @const */ this.countersById = countersById; + /** @const */ this.currentPageCounters = currentPageCounters; /** @const */ this.baseURL = baseURL; /** @const */ this.rootScope = rootScope; + /** @const */ this.pageScope = pageScope; /** @type {?adapt.cssstyler.Styler} */ this.styler = null; } @@ -82,6 +86,33 @@ goog.scope(function() { return transformedId; }; + /** + * @override + */ + CounterResolver.prototype.getPageCounterVal = function(name, format) { + var self = this; + function getCounterNumber() { + var values = self.currentPageCounters[name]; + return (values && values.length) ? values[values.length - 1] : null; + } + return new adapt.expr.Native(this.pageScope, function() { + return format(getCounterNumber()); + }, "page-counter-" + name); + }; + + /** + * @override + */ + CounterResolver.prototype.getPageCountersVal = function(name, format) { + var self = this; + function getCounterNumbers() { + return self.currentPageCounters[name] || []; + } + return new adapt.expr.Native(this.pageScope, function() { + return format(getCounterNumbers()); + }, "page-counters-" + name) + }; + /** * @override */ @@ -122,13 +153,14 @@ goog.scope(function() { }; /** - * * @param {!adapt.base.DocumentURLTransformer} documentURLTransformer * @constructor */ vivliostyle.counters.CounterStore = function(documentURLTransformer) { /** @const */ this.documentURLTransformer = documentURLTransformer; /** @const {!Object.>>} */ this.countersById = {}; + /** @const {!Object.>} */ this.currentPageCounters = {}; + this.currentPageCounters["page"] = [0]; }; /** @@ -144,10 +176,71 @@ goog.scope(function() { * * @param {string} baseURL * @param {adapt.expr.LexicalScope} rootScope + * @param {adapt.expr.LexicalScope} pageScope * @returns {!adapt.csscasc.CounterResolver} */ - vivliostyle.counters.CounterStore.prototype.createCounterResolver = function(baseURL, rootScope) { - return new CounterResolver(this.documentURLTransformer, this.countersById, baseURL, rootScope); + vivliostyle.counters.CounterStore.prototype.createCounterResolver = function(baseURL, rootScope, pageScope) { + return new CounterResolver(this.documentURLTransformer, this.countersById, this.currentPageCounters, + baseURL, rootScope, pageScope); + }; + + /** + * @private + * @param {string} counterName + * @param {number} value + */ + vivliostyle.counters.CounterStore.prototype.definePageCounter = function(counterName, value) { + if (this.currentPageCounters[counterName]) { + this.currentPageCounters[counterName].push(value); + } else { + this.currentPageCounters[counterName] = [value]; + } + }; + + /** + * Update the page-based counters with 'counter-reset' and 'counter-increment' properties within the page context. Call before starting layout of the page. + * @param {!adapt.csscasc.ElementStyle} cascadedPageStyle + * @param {!adapt.expr.Context} context + */ + vivliostyle.counters.CounterStore.prototype.updatePageCounters = function(cascadedPageStyle, context) { + var resetMap; + var reset = cascadedPageStyle["counter-reset"]; + if (reset) { + var resetVal = reset.evaluate(context); + if (resetVal) { + resetMap = adapt.cssprop.toCounters(resetVal, true); + } + } + if (resetMap) { + for (var resetCounterName in resetMap) { + this.definePageCounter(resetCounterName, resetMap[resetCounterName]); + } + } + + var incrementMap; + var increment = cascadedPageStyle["counter-increment"]; + if (increment) { + var incrementVal = increment.evaluate(context); + if (incrementVal) { + incrementMap = adapt.cssprop.toCounters(incrementVal, false); + } + } + // If 'counter-increment' for the builtin 'page' counter is absent, add it with value 1. + if (incrementMap) { + if (!("page" in incrementMap)) { + incrementMap["page"] = 1; + } + } else { + incrementMap = {}; + incrementMap["page"] = 1; + } + for (var incrementCounterName in incrementMap) { + if (!this.currentPageCounters[incrementCounterName]) { + this.definePageCounter(incrementCounterName, 0); + } + var counterValues = this.currentPageCounters[incrementCounterName]; + counterValues[counterValues.length - 1] += incrementMap[incrementCounterName]; + } }; }); diff --git a/src/vivliostyle/page.js b/src/vivliostyle/page.js index eceafb11b..0d6f670d2 100644 --- a/src/vivliostyle/page.js +++ b/src/vivliostyle/page.js @@ -2372,112 +2372,3 @@ vivliostyle.page.PageMarginBoxParserHandler.prototype.simpleProperty = function( var cascval = new adapt.csscasc.CascadeValue(value, specificity); adapt.csscasc.setProp(this.boxStyle, name, cascval); }; - - -/** - * Object storing page-based counters. - * @param {adapt.expr.LexicalScope} pageScope Scope in which a page-based counter's adapt.expr.Val is defined. Since the page-based counters are updated per page, the scope should be a page scope, which is cleared per page. - * @constructor - * @implements {adapt.csscasc.PageCounterResolver} - */ -vivliostyle.page.PageCounterStore = function(pageScope) { - /** @const */ this.pageScope = pageScope; - /** @const @type {!Object.>} */ this.counters = {}; - this.counters["page"] = [0]; -}; - -/** - * Copy (and override) counter states from another PageCounterstore. - * @param {!vivliostyle.page.PageCounterStore} pageCounterStore - */ -vivliostyle.page.PageCounterStore.prototype.copyFrom = function(pageCounterStore) { - Object.keys(pageCounterStore.counters).forEach(function(key) { - this.counters[key] = Array.from(pageCounterStore.counters[key]); - }, this); -}; - -/** - * @override - */ -vivliostyle.page.PageCounterStore.prototype.getCounterVal = function(name, format) { - var self = this; - function getCounterNumber() { - var values = self.counters[name]; - return (values && values.length) ? values[values.length - 1] : null; - } - return new adapt.expr.Native(this.pageScope, function() { - return format(getCounterNumber()); - }, "page-counter-" + name); -}; - -/** - * @override - */ -vivliostyle.page.PageCounterStore.prototype.getCountersVal = function(name, format) { - var self = this; - function getCounterNumbers() { - return self.counters[name] || []; - } - return new adapt.expr.Native(this.pageScope, function() { - return format(getCounterNumbers()); - }, "page-counters-" + name) -}; - -/** - * @private - * @param {string} counterName - * @param {number} value - */ -vivliostyle.page.PageCounterStore.prototype.defineCounter = function(counterName, value) { - if (this.counters[counterName]) { - this.counters[counterName].push(value); - } else { - this.counters[counterName] = [value]; - } -}; - -/** - * Update the page-based counters with 'counter-reset' and 'counter-increment' properties within the page context. Call before starting layout of the page. - * @param {!adapt.csscasc.ElementStyle} cascadedPageStyle - * @param {!adapt.expr.Context} context - */ -vivliostyle.page.PageCounterStore.prototype.updatePageCounters = function(cascadedPageStyle, context) { - var resetMap; - var reset = cascadedPageStyle["counter-reset"]; - if (reset) { - var resetVal = reset.evaluate(context); - if (resetVal) { - resetMap = adapt.cssprop.toCounters(resetVal, true); - } - } - if (resetMap) { - for (var resetCounterName in resetMap) { - this.defineCounter(resetCounterName, resetMap[resetCounterName]); - } - } - - var incrementMap; - var increment = cascadedPageStyle["counter-increment"]; - if (increment) { - var incrementVal = increment.evaluate(context); - if (incrementVal) { - incrementMap = adapt.cssprop.toCounters(incrementVal, false); - } - } - // If 'counter-increment' for the builtin 'page' counter is absent, add it with value 1. - if (incrementMap) { - if (!("page" in incrementMap)) { - incrementMap["page"] = 1; - } - } else { - incrementMap = {}; - incrementMap["page"] = 1; - } - for (var incrementCounterName in incrementMap) { - if (!this.counters[incrementCounterName]) { - this.defineCounter(incrementCounterName, 0); - } - var counterValues = this.counters[incrementCounterName]; - counterValues[counterValues.length - 1] += incrementMap[incrementCounterName]; - } -}; From 69367852b8a28cd589928146bd34068af4fd5312 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Wed, 1 Jun 2016 02:01:45 +0900 Subject: [PATCH 04/23] [WIP] Support `target-counter()`/`target-counters()` for page-based counters - Only effective for a target ID such that the corresponding element is already laid out before the referring pseudoelement (i.e. the pseudoelement on which `target-counter()`/`target-counters()` is specified) --- src/adapt/ops.js | 2 + src/adapt/vtree.js | 2 +- src/vivliostyle/counters.js | 129 ++++++++++++++++++++++----------- test/files/target-counter.html | 24 +++++- 4 files changed, 111 insertions(+), 46 deletions(-) diff --git a/src/adapt/ops.js b/src/adapt/ops.js index a448e628b..b503501c6 100644 --- a/src/adapt/ops.js +++ b/src/adapt/ops.js @@ -872,6 +872,7 @@ adapt.ops.StyleInstance.prototype.layoutNextPage = function(page, cp) { if (pageMaster.pageBox.specified["height"].value === adapt.css.fullHeight) { page.setAutoPageHeight(true); } + self.counterStore.setCurrentPage(page); self.counterStore.updatePageCounters(cascadedPageStyle, self); // setup bleed area and crop marks @@ -908,6 +909,7 @@ adapt.ops.StyleInstance.prototype.layoutNextPage = function(page, cp) { page.side = isLeftPage.evaluate(self) ? vivliostyle.constants.PageSide.LEFT : vivliostyle.constants.PageSide.RIGHT; self.processLinger(); self.currentLayoutPosition = self.layoutPositionAtPageStart = null; + self.counterStore.finishPage(); cp.highestSeenOffset = self.styler.getReachedOffset(); var triggers = self.style.store.getTriggersForDoc(self.xmldoc); page.finish(triggers, self.clientLayout); diff --git a/src/adapt/vtree.js b/src/adapt/vtree.js index c82bf561a..5642ee1fd 100644 --- a/src/adapt/vtree.js +++ b/src/adapt/vtree.js @@ -116,7 +116,7 @@ adapt.vtree.Page = function(container, bleedBox) { self.dispatchEvent(evt); } }; - /** @type {Object.>} */ this.elementsById = {}; + /** @const {!Object.>} */ this.elementsById = {}; /** @const @type {{width: number, height: number}} */ this.dimensions = {width: 0, height: 0}; /** @type {boolean} */ this.isFirstPage = false; /** @type {boolean} */ this.isLastPage = false; diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index cf280d426..22c324565 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -13,16 +13,13 @@ goog.require("adapt.cssstyler"); goog.scope(function() { /** - * - * @param {!adapt.base.DocumentURLTransformer} documentURLTransformer - * @param {!Object>>} countersById + * @param {!vivliostyle.counters.CounterStore} counterStore * @param {string} baseURL * @constructor * @implements {adapt.csscasc.CounterListener} */ - function CounterListener(documentURLTransformer, countersById, baseURL) { - /** @const */ this.documentURLTransformer = documentURLTransformer; - /** @const */ this.countersById = countersById; + function CounterListener(counterStore, baseURL) { + /** @const */ this.counterStore = counterStore; /** @const */ this.baseURL = baseURL; } @@ -30,25 +27,20 @@ goog.scope(function() { * @override */ CounterListener.prototype.countersOfId = function(id, counters) { - id = this.documentURLTransformer.transformFragment(id, this.baseURL); - this.countersById[id] = counters; + id = this.counterStore.documentURLTransformer.transformFragment(id, this.baseURL); + this.counterStore.countersById[id] = counters; }; /** - * - * @param {!adapt.base.DocumentURLTransformer} documentURLTransformer - * @param {!Object>>} countersById - * @param {!Object.>} currentPageCounters + * @param {!vivliostyle.counters.CounterStore} counterStore * @param {string} baseURL * @param {adapt.expr.LexicalScope} rootScope * @param {adapt.expr.LexicalScope} pageScope * @constructor * @implements {adapt.csscasc.CounterResolver} */ - function CounterResolver(documentURLTransformer, countersById, currentPageCounters, baseURL, rootScope, pageScope) { - /** @const */ this.documentURLTransformer = documentURLTransformer; - /** @const */ this.countersById = countersById; - /** @const */ this.currentPageCounters = currentPageCounters; + function CounterResolver(counterStore, baseURL, rootScope, pageScope) { + /** @const */ this.counterStore = counterStore; /** @const */ this.baseURL = baseURL; /** @const */ this.rootScope = rootScope; /** @const */ this.pageScope = pageScope; @@ -79,7 +71,7 @@ goog.scope(function() { * @returns {string} */ CounterResolver.prototype.getTransformedId = function(url) { - var transformedId = this.documentURLTransformer.transformURL(url, this.baseURL); + var transformedId = this.counterStore.documentURLTransformer.transformURL(url, this.baseURL); if (transformedId.charAt(0) === "#") { transformedId = transformedId.substring(1); } @@ -92,7 +84,7 @@ goog.scope(function() { CounterResolver.prototype.getPageCounterVal = function(name, format) { var self = this; function getCounterNumber() { - var values = self.currentPageCounters[name]; + var values = self.counterStore.currentPageCounters[name]; return (values && values.length) ? values[values.length - 1] : null; } return new adapt.expr.Native(this.pageScope, function() { @@ -106,13 +98,47 @@ goog.scope(function() { CounterResolver.prototype.getPageCountersVal = function(name, format) { var self = this; function getCounterNumbers() { - return self.currentPageCounters[name] || []; + return self.counterStore.currentPageCounters[name] || []; } return new adapt.expr.Native(this.pageScope, function() { return format(getCounterNumbers()); }, "page-counters-" + name) }; + /** + * @private + * @param {?string} id + * @param {string} transformedId + * @param {string} name + * @returns {?Array} + */ + CounterResolver.prototype.getTargetCounters = function(id, transformedId, name) { + var targetCounters = this.counterStore.countersById[transformedId]; + if (!targetCounters && id) { + this.styler.styleUntilIdIsReached(id); + targetCounters = this.counterStore.countersById[transformedId]; + } + return (targetCounters && targetCounters[name]) || null; + }; + + /** + * @private + * @param {string} transformedId + * @param {string} name + * @returns {?Array} + */ + CounterResolver.prototype.getTargetPageCounters = function(transformedId, name) { + if (this.counterStore.currentPage.elementsById[transformedId]) { + return this.counterStore.currentPageCounters[name] || null; + } else { + var targetPageCounters = this.counterStore.pageCountersById[transformedId]; + if (targetPageCounters) { + return targetPageCounters[name] || null; + } + } + return null; + }; + /** * @override */ @@ -120,17 +146,17 @@ goog.scope(function() { var id = this.getFragment(url); var transformedId = this.getTransformedId(url); var self = this; - var scope = this.rootScope; - return new adapt.expr.Native(scope, function() { - var targetCounters = self.countersById[transformedId]; - if (!targetCounters && id) { - self.styler.styleUntilIdIsReached(id); - targetCounters = self.countersById[transformedId]; + return new adapt.expr.Native(this.rootScope, function() { + var arr = self.getTargetCounters(id, transformedId, name); + if (arr && arr.length) { + var numval = arr[arr.length - 1] || null; + return format(numval); + } else { + arr = self.getTargetPageCounters(transformedId, name); + var numval = (arr && arr.length && arr[arr.length - 1]) || null; + return format(numval); } - var arr = targetCounters && targetCounters[name]; - var numval = (arr && arr.length && arr[arr.length - 1]) || null; - return format(numval); - }, "target-counter-" + name + "-of-#" + transformedId); + }, "target-counter-" + name + "-of-" + url); }; /** @@ -140,16 +166,11 @@ goog.scope(function() { var id = this.getFragment(url); var transformedId = this.getTransformedId(url); var self = this; - var scope = this.rootScope; - return new adapt.expr.Native(scope, function() { - var targetCounters = self.countersById[transformedId]; - if (!targetCounters && id) { - self.styler.styleUntilIdIsReached(id); - targetCounters = self.countersById[transformedId]; - } - var arr = targetCounters && targetCounters[name]; - return format(arr || []); - }, "target-counters-" + name + "-of-#" + transformedId); + return new adapt.expr.Native(this.rootScope, function() { + var pageCounters = self.getTargetPageCounters(transformedId, name); + var elementCounters = self.getTargetCounters(id, transformedId, name); + return format((pageCounters ? pageCounters.concat(elementCounters) : elementCounters) || []); + }, "target-counters-" + name + "-of-" + url); }; /** @@ -159,29 +180,35 @@ goog.scope(function() { vivliostyle.counters.CounterStore = function(documentURLTransformer) { /** @const */ this.documentURLTransformer = documentURLTransformer; /** @const {!Object.>>} */ this.countersById = {}; + /** @const {!Object.>>} */ this.pageCountersById = {}; /** @const {!Object.>} */ this.currentPageCounters = {}; this.currentPageCounters["page"] = [0]; + /** @type {adapt.vtree.Page} */ this.currentPage = null; }; /** - * * @param {string} baseURL * @returns {!adapt.csscasc.CounterListener} */ vivliostyle.counters.CounterStore.prototype.createCounterListener = function(baseURL) { - return new CounterListener(this.documentURLTransformer, this.countersById, baseURL); + return new CounterListener(this, baseURL); }; /** - * * @param {string} baseURL * @param {adapt.expr.LexicalScope} rootScope * @param {adapt.expr.LexicalScope} pageScope * @returns {!adapt.csscasc.CounterResolver} */ vivliostyle.counters.CounterStore.prototype.createCounterResolver = function(baseURL, rootScope, pageScope) { - return new CounterResolver(this.documentURLTransformer, this.countersById, this.currentPageCounters, - baseURL, rootScope, pageScope); + return new CounterResolver(this, baseURL, rootScope, pageScope); + }; + + /** + * @param {adapt.vtree.Page} page + */ + vivliostyle.counters.CounterStore.prototype.setCurrentPage = function(page) { + this.currentPage = page; }; /** @@ -243,4 +270,18 @@ goog.scope(function() { } }; + vivliostyle.counters.CounterStore.prototype.finishPage = function() { + var ids = Object.keys(this.currentPage.elementsById); + if (ids.length > 0) { + /** @type {!Object>} */ var counters = {}; + Object.keys(this.currentPageCounters).forEach(function(name) { + counters[name] = Array.from(this.currentPageCounters[name]); + }, this); + ids.forEach(function(id) { + this.pageCountersById[id] = counters; + }, this); + } + this.currentPage = null; + }; + }); diff --git a/test/files/target-counter.html b/test/files/target-counter.html index f815d1a66..c28f4be0e 100644 --- a/test/files/target-counter.html +++ b/test/files/target-counter.html @@ -4,6 +4,17 @@ target-counter @@ -90,10 +96,11 @@

    'foo' value of #foo-target = =

    'foo' values of #foofoo-target = =

    #foo-target is on page =

    +

    #bar-target is on page =

    'bar' value of #bar-target = =

    'bar' values of #barbar-target = =

    -

    #foo-target is on page =

    +

    #bar-target is on page =

    bar=

    bar=

    (#bar-target) bar=

    @@ -102,5 +109,6 @@

    'bar' value of #bar-target = =

    'bar' values of #barbar-target = =

    #foo-target is on page =

    +

    #bar-target is on page =

    \ No newline at end of file From 1d9d3347fd11be72f5915bcca00618b13e58ec27 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Mon, 6 Jun 2016 15:19:59 +0900 Subject: [PATCH 08/23] Fix incorrect page reference in some cases If an element with an ID does not fit in a page, the view node is removed and deferred to the next page. However, the ID and the element are registered to an `adapt.vtree.Page` object during creation of the view node and the regitration has not been removed even if the element does not fit in the page. This has been causing incorrect page references. In this commit, the registered IDs are checked after layout of the page. If the page container does not contain an element (elements) corresponding to a registered ID, the registration of the ID is removed. --- src/adapt/vtree.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/adapt/vtree.js b/src/adapt/vtree.js index 5642ee1fd..c9e743822 100644 --- a/src/adapt/vtree.js +++ b/src/adapt/vtree.js @@ -194,6 +194,22 @@ adapt.vtree.Page.prototype.registerElementWithId = function(element, id) { * @return {void} */ adapt.vtree.Page.prototype.finish = function(triggers, clientLayout) { + // Remove ID of elements which eventually did not fit in the page + // (Some nodes may have been removed after registration if they did not fit in the page) + Object.keys(this.elementsById).forEach(function(id) { + var elems = this.elementsById[id]; + for (var i = 0; i < elems.length;) { + if (this.container.contains(elems[i])) { + i++; + } else { + elems.splice(i, 1); + } + } + if (elems.length === 0) { + delete this.elementsById[id]; + } + }, this); + // use size of the container of the PageMasterInstance var rect = clientLayout.getElementClientRect(this.container); this.dimensions.width = rect.width; From d5d7125c24a80438eebc7df5333a79fd5807ca74 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Mon, 6 Jun 2016 16:03:16 +0900 Subject: [PATCH 09/23] Refresh page display when a currently displayed page is re-laid out by resolutions of cross references --- src/adapt/epub.js | 12 ++++++++++++ src/adapt/viewer.js | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/adapt/epub.js b/src/adapt/epub.js index 421f0d7b8..8e7020029 100644 --- a/src/adapt/epub.js +++ b/src/adapt/epub.js @@ -1011,6 +1011,12 @@ adapt.epub.OPFView.prototype.getCurrentPageProgression = function() { } }; +/** + * @private + * @param {adapt.epub.OPFViewItem} viewItem + * @param {adapt.vtree.Page} page + * @param {number} pageIndex + */ adapt.epub.OPFView.prototype.finishPageContainer = function(viewItem, page, pageIndex) { page.container.style.display = "none"; page.container.style.visibility = "visible"; @@ -1021,6 +1027,12 @@ adapt.epub.OPFView.prototype.finishPageContainer = function(viewItem, page, page var oldPage = viewItem.pages[pageIndex]; if (oldPage) { viewItem.instance.viewport.contentContainer.replaceChild(page.container, oldPage.container); + oldPage.dispatchEvent({ + type: "replaced", + target: null, + currentTarget: null, + newPage: page + }); } else { viewItem.instance.viewport.contentContainer.appendChild(page.container); } diff --git a/src/adapt/viewer.js b/src/adapt/viewer.js index 66091dd3f..8da4af6a1 100644 --- a/src/adapt/viewer.js +++ b/src/adapt/viewer.js @@ -56,6 +56,7 @@ adapt.viewer.Viewer = function(window, viewportElement, instanceId, callbackFn) self.needResize = true; self.kick(); }; + /** @const */ this.pageReplacedListener = this.pageReplacedListener.bind(this); /** @type {adapt.base.EventListener} */ this.hyperlinkListener = function(evt) {}; /** @const */ this.pageRuleStyleElement = document.getElementById("vivliostyle-page-rules"); /** @type {boolean} */ this.pageSheetSizeAlreadySet = false; @@ -329,6 +330,18 @@ adapt.viewer.Viewer.prototype.configure = function(command) { return adapt.task.newResult(true); }; +/** + * Refresh view when a currently displayed page is replaced (by re-layout caused by cross reference resolutions) + * @param {adapt.base.Event} evt + */ +adapt.viewer.Viewer.prototype.pageReplacedListener = function(evt) { + var currentPage = this.currentPage; + if (currentPage === evt.target) { + currentPage = evt.newPage; + } + this.showCurrent(currentPage); +}; + /** * Hide current pages (this.currentPage, this.currentSpread) * @private @@ -348,6 +361,7 @@ adapt.viewer.Viewer.prototype.hidePages = function() { if (page) { adapt.base.setCSSProperty(page.container, "display", "none"); page.removeEventListener("hyperlink", this.hyperlinkListener, false); + page.removeEventListener("replaced", this.pageReplacedListener, false); } }, this); }; @@ -358,6 +372,7 @@ adapt.viewer.Viewer.prototype.hidePages = function() { */ adapt.viewer.Viewer.prototype.showSinglePage = function(page) { page.addEventListener("hyperlink", this.hyperlinkListener, false); + page.addEventListener("replaced", this.pageReplacedListener, false); adapt.base.setCSSProperty(page.container, "visibility", "visible"); adapt.base.setCSSProperty(page.container, "display", "block"); }; From f6211a0dbf9dcc73025c1c43cd16f2f65be8e732 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 7 Jun 2016 12:20:48 +0900 Subject: [PATCH 10/23] Properly resolves references (by 'target-counter()') to another source document in the EPUB --- src/vivliostyle/counters.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index c8ae597b2..af2a12f67 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -113,7 +113,8 @@ goog.scope(function() { * @returns {string} */ CounterResolver.prototype.getTransformedId = function(url) { - var transformedId = this.counterStore.documentURLTransformer.transformURL(url, this.baseURL); + var transformedId = this.counterStore.documentURLTransformer.transformURL( + adapt.base.resolveURL(url, this.baseURL), this.baseURL); if (transformedId.charAt(0) === "#") { transformedId = transformedId.substring(1); } @@ -211,8 +212,12 @@ goog.scope(function() { return "??"; // TODO more reasonable placeholder? } } + } else { + // The style of target element has not been calculated yet. + // (The element is in another source document that is not parsed yet) + self.counterStore.saveReferenceOfCurrentPage(transformedId, false); + return "??"; // TODO more reasonable placeholder? } - // TODO: handle the case where counters == null }, "target-counter-" + name + "-of-" + url); }; From 95e5ab90792efa3529d78b7195705d0bb7163885 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 7 Jun 2016 14:15:21 +0900 Subject: [PATCH 11/23] Make 'target-counter()` work even if the target URL points to another source document in EPUB and does not have a fragment --- src/adapt/csscasc.js | 5 +++-- src/adapt/vgen.js | 8 +++++++- test/files/multiple_html/first.html | 26 +++++++++++++++---------- test/files/multiple_html/second.html | 29 ++++++++++++++++++---------- 4 files changed, 45 insertions(+), 23 deletions(-) diff --git a/src/adapt/csscasc.js b/src/adapt/csscasc.js index 57d4afdc0..f00d08dd2 100644 --- a/src/adapt/csscasc.js +++ b/src/adapt/csscasc.js @@ -2632,6 +2632,7 @@ adapt.csscasc.CascadeInstance.prototype.pushElement = function(element, baseStyl new adapt.csscasc.RestoreLangItem(this.lang)); this.lang = lang.toLowerCase(); } + var isRoot = this.isRoot; var siblingOrderStack = this.siblingOrderStack; this.currentSiblingOrder = ++siblingOrderStack[siblingOrderStack.length - 1]; @@ -2677,8 +2678,8 @@ adapt.csscasc.CascadeInstance.prototype.pushElement = function(element, baseStyl } } this.pushCounters(this.currentStyle); - var id = this.currentId || this.currentXmlId; - if (id) { + var id = this.currentId || this.currentXmlId || ""; + if (isRoot || id) { /** @type {!Object>} */ var counters = {}; Object.keys(this.counters).forEach(function(name) { counters[name] = Array.from(this.counters[name]); diff --git a/src/adapt/vgen.js b/src/adapt/vgen.js index 43fb86798..2768f1b6b 100644 --- a/src/adapt/vgen.js +++ b/src/adapt/vgen.js @@ -568,8 +568,9 @@ adapt.vgen.ViewFactory.prototype.createElementView = function(firstTime, atUnfor frame.finish(false); return frame.result(); } + var isRoot = self.nodeContext.parent == null; self.nodeContext.flexContainer = (display === adapt.css.ident.flex); - self.createShadows(element, self.nodeContext.parent == null, elementStyle, computedStyle, styler, + self.createShadows(element, isRoot, elementStyle, computedStyle, styler, self.context, self.nodeContext.shadowContext).then(function(shadowParam) { self.nodeContext.nodeShadow = shadowParam; var position = computedStyle["position"]; @@ -844,6 +845,11 @@ adapt.vgen.ViewFactory.prototype.createElementView = function(firstTime, atUnfor } } } + // necessary for target-counter resolution + if (isRoot && firstTime) { + var rootId = self.documentURLTransformer.transformFragment("", self.xmldoc.url); + self.page.registerElementWithId(result, rootId); + } if (delayedSrc) { var imageFetcher = adapt.taskutil.loadElement(result, delayedSrc); var w = computedStyle["width"]; diff --git a/test/files/multiple_html/first.html b/test/files/multiple_html/first.html index b2a2ef50c..1c3229446 100644 --- a/test/files/multiple_html/first.html +++ b/test/files/multiple_html/first.html @@ -10,24 +10,30 @@ content: "Document 1, page " counter(page); } } - div { + section { page-break-after: always; + counter-increment: section; + } + section::before { + display: block; + content: "Section " counter(section) ", page " counter(page); + } + a[href]::after { + content: "Section " target-counter(attr(href url), section) ", page " target-counter(attr(href url), page); } -
    - This is the first page. - link to the second page -
    +
    + link to +
    - +

    link to (second HTML)

    +

    link to (second HTML)

    + \ No newline at end of file diff --git a/test/files/multiple_html/second.html b/test/files/multiple_html/second.html index aa1f77fbf..3e8cd59cc 100644 --- a/test/files/multiple_html/second.html +++ b/test/files/multiple_html/second.html @@ -10,24 +10,33 @@ content: "Document 2, page " counter(page); } } - div { + html { + counter-reset: section 2; + } + section { page-break-after: always; + counter-increment: section; + } + section::before { + display: block; + content: "Section " counter(section) ", page " counter(page); + } + a[href]::after { + content: "Section " target-counter(attr(href url), section) ", page " target-counter(attr(href url), page); } -
    - This is the third page. - link to the fourth page -
    +
    + link to +
    - +

    link to (first HTML)

    +

    link to (first HTML)

    + \ No newline at end of file From cf1a2d90bd0dc71c101f3f5cbc3ab685e90c50ae Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 7 Jun 2016 17:02:01 +0900 Subject: [PATCH 12/23] Support 'target-counters()' for an element on a later page --- src/vivliostyle/counters.js | 17 ++++++++++++----- test/files/target-counter.html | 4 ++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index af2a12f67..0cd76d271 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -228,12 +228,19 @@ goog.scope(function() { var id = this.getFragment(url); var transformedId = this.getTransformedId(url); var self = this; - return new adapt.expr.Native(this.rootScope, function() { + return new adapt.expr.Native(this.pageScope, function() { var pageCounters = self.getTargetPageCounters(transformedId); - var pageCountersOfName = (pageCounters && pageCounters[name]) || []; - var elementCounters = self.getTargetCounters(id, transformedId); - var elementCountersOfName = (elementCounters && elementCounters[name]) || []; - return format(pageCountersOfName.concat(elementCountersOfName)); + if (!pageCounters) { + // The target element has not been laid out yet. + self.counterStore.saveReferenceOfCurrentPage(transformedId, false); + return "??"; // TODO more reasonable placeholder? + } else { + self.counterStore.resolveReference(transformedId); + var pageCountersOfName = pageCounters[name] || []; + var elementCounters = self.getTargetCounters(id, transformedId); + var elementCountersOfName = elementCounters[name] || []; + return format(pageCountersOfName.concat(elementCountersOfName)); + } }, "target-counters-" + name + "-of-" + url); }; diff --git a/test/files/target-counter.html b/test/files/target-counter.html index 392333093..7bfae90f0 100644 --- a/test/files/target-counter.html +++ b/test/files/target-counter.html @@ -95,6 +95,8 @@

    foo=

    'foo' value of #foo-target = =

    'foo' values of #foofoo-target = =

    +

    'bar' value of #bar-target = =

    +

    'bar' values of #barbar-target = =

    #foo-target is on page =

    #bar-target is on page =

    @@ -106,6 +108,8 @@

    (#bar-target) bar=

    bars=, (#barbar-target) bars=, bar=

    bar=

    +

    'foo' value of #foo-target = =

    +

    'foo' values of #foofoo-target = =

    'bar' value of #bar-target = =

    'bar' values of #barbar-target = =

    #foo-target is on page =

    From 2767d8f50e4852bf8c02b43eb4da7d28eb5091f6 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 7 Jun 2016 17:19:08 +0900 Subject: [PATCH 13/23] Optimize 'target-counter()' for a case where the target element comes before the reference --- src/vivliostyle/counters.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index 0cd76d271..42e384ef1 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -154,11 +154,12 @@ goog.scope(function() { * @private * @param {?string} id Original ID value * @param {string} transformedId ID transformed by DocumentURLTransformer to handle a reference across multiple source documents + * @param {boolean} lookForElement If true, look ahead for an element with the ID in the current source document when such an element has not appeared yet. Do not set to true during Styler.styleUntil is being called, since in that case Styler.styleUntil can be called again and may lead to internal inconsistency. * @returns {?adapt.csscasc.CounterValues} */ - CounterResolver.prototype.getTargetCounters = function(id, transformedId) { + CounterResolver.prototype.getTargetCounters = function(id, transformedId, lookForElement) { var targetCounters = this.counterStore.countersById[transformedId]; - if (!targetCounters && id) { + if (!targetCounters && lookForElement && id) { this.styler.styleUntilIdIsReached(id); targetCounters = this.counterStore.countersById[transformedId]; } @@ -186,9 +187,19 @@ goog.scope(function() { CounterResolver.prototype.getTargetCounterVal = function(url, name, format) { var id = this.getFragment(url); var transformedId = this.getTransformedId(url); + + // Since this method is executed during Styler.styleUntil is being called, set false to lookForElement argument. + var counters = this.getTargetCounters(id, transformedId, false); + if (counters && counters[name]) { + // Since an element-based counter is defined, any page-based counter is obscured even if it exists. + var countersOfName = counters[name]; + return new adapt.expr.Const(this.rootScope, format(countersOfName[countersOfName.length - 1] || null)); + } + var self = this; return new adapt.expr.Native(this.pageScope, function() { - var counters = self.getTargetCounters(id, transformedId); + // Since This block is evaluated during layout, lookForElement argument can be set to true. + counters = self.getTargetCounters(id, transformedId, true); if (counters) { if (counters[name]) { // Since an element-based counter is defined, any page-based counter is obscured even if it exists. From ff40b52d2e0e0e24846873b3478ccec6a7c4513b Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 7 Jun 2016 20:37:14 +0900 Subject: [PATCH 14/23] Optimize 'target-counter()' for a case where the target element comes before the reference --- src/vivliostyle/counters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index 42e384ef1..367a30344 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -248,7 +248,7 @@ goog.scope(function() { } else { self.counterStore.resolveReference(transformedId); var pageCountersOfName = pageCounters[name] || []; - var elementCounters = self.getTargetCounters(id, transformedId); + var elementCounters = self.getTargetCounters(id, transformedId, true); var elementCountersOfName = elementCounters[name] || []; return format(pageCountersOfName.concat(elementCountersOfName)); } From aed6cf93980427d57029dd07387b3c41c9f24dbd Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 7 Jun 2016 20:46:20 +0900 Subject: [PATCH 15/23] Re-layout next page too if page break position has been moved by cross reference resolution --- src/adapt/epub.js | 85 ++++++++++++-------- src/adapt/vtree.js | 137 ++++++++++++++++++++++++++++++++- test/files/target-counter.html | 97 +++++++++++++++-------- 3 files changed, 254 insertions(+), 65 deletions(-) diff --git a/src/adapt/epub.js b/src/adapt/epub.js index 8e7020029..daf8065fa 100644 --- a/src/adapt/epub.js +++ b/src/adapt/epub.js @@ -1069,52 +1069,69 @@ adapt.epub.OPFView.prototype.renderSinglePage = function(viewItem, pos) { self.finishPageContainer(viewItem, page, pageIndex); self.counterStore.finishPage(self.spineIndex, pageIndex); - var unresolvedRefs = self.counterStore.getUnresolvedRefsToPage(page); // backup original values var origSpineIndex = self.spineIndex; var origPageIndex = self.pageIndex; var origOffsetInItem = self.offsetInItem; - var index = 0; - frame.loopWithFrame(function(loopFrame) { - index++; - if (index > unresolvedRefs.length) { - loopFrame.breakLoop(); - return; - } - var refs = unresolvedRefs[index - 1]; - refs.refs = refs.refs.filter(function(ref) { return !ref.isResolved(); }); - if (refs.refs.length === 0) { - loopFrame.continueLoop(); - return; + // If the position of the page break change, we should re-layout the next page too. + var cont = null; + if (pos) { + var prevPos = viewItem.layoutPositions[pos.page]; + if (prevPos) { + if (!pos.isSamePosition(prevPos)) { + self.pageIndex = pos.page; + cont = self.renderSinglePage(viewItem, pos); + } } + } + if (!cont) { + cont = adapt.task.newResult(true); + } - self.spineIndex = refs.spineIndex; - self.pageIndex = refs.pageIndex; - self.getPageViewItem().then(function(viewItem) { - if (!viewItem) { - loopFrame.continueLoop(); + cont.then(function() { + var unresolvedRefs = self.counterStore.getUnresolvedRefsToPage(page); + var index = 0; + frame.loopWithFrame(function(loopFrame) { + index++; + if (index > unresolvedRefs.length) { + loopFrame.breakLoop(); return; } - self.counterStore.pushPageCounters(refs.pageCounters); - self.counterStore.pushReferencesToSolve(refs.refs); - var pos = viewItem.layoutPositions[self.pageIndex]; - self.renderSinglePage(viewItem, pos).then(function() { - self.counterStore.popPageCounters(); - self.counterStore.popReferencesToSolve(); + var refs = unresolvedRefs[index - 1]; + refs.refs = refs.refs.filter(function(ref) { return !ref.isResolved(); }); + if (refs.refs.length === 0) { loopFrame.continueLoop(); + return; + } + + self.spineIndex = refs.spineIndex; + self.pageIndex = refs.pageIndex; + self.getPageViewItem().then(function(viewItem) { + if (!viewItem) { + loopFrame.continueLoop(); + return; + } + self.counterStore.pushPageCounters(refs.pageCounters); + self.counterStore.pushReferencesToSolve(refs.refs); + var pos = viewItem.layoutPositions[self.pageIndex]; + self.renderSinglePage(viewItem, pos).then(function() { + self.counterStore.popPageCounters(); + self.counterStore.popReferencesToSolve(); + loopFrame.continueLoop(); + }); + }); + }).then(function() { + // restore original values + self.spineIndex = origSpineIndex; + self.pageIndex = origPageIndex; + self.offsetInItem = origOffsetInItem; + frame.finish({ + page: page, + position: pos, + pageIndex: pageIndex }); - }); - }).then(function() { - // restore original values - self.spineIndex = origSpineIndex; - self.pageIndex = origPageIndex; - self.offsetInItem = origOffsetInItem; - frame.finish({ - page: page, - position: pos, - pageIndex: pageIndex }); }); diff --git a/src/adapt/vtree.js b/src/adapt/vtree.js index c9e743822..f061b8126 100644 --- a/src/adapt/vtree.js +++ b/src/adapt/vtree.js @@ -510,6 +510,25 @@ adapt.vtree.LayoutContext.prototype.getPageFloatHolder = function() {}; */ adapt.vtree.NodePositionStep; +/** + * @param {adapt.vtree.NodePositionStep} nps1 + * @param {adapt.vtree.NodePositionStep} nps2 + * @returns {boolean} + */ +adapt.vtree.isSameNodePositionStep = function(nps1, nps2) { + if (nps1 === nps2) { + return true; + } + if (!nps1 || !nps2) { + return false; + } + return nps1.node === nps2.node && + nps1.shadowType === nps2.shadowType && + nps1.shadowContext === nps2.shadowContext && + nps1.nodeShadow === nps2.nodeShadow && + nps1.shadowSibling === nps2.shadowSibling; +}; + /** * NodePosition represents a position in the document * @typedef {{ @@ -520,6 +539,29 @@ adapt.vtree.NodePositionStep; */ adapt.vtree.NodePosition; +/** + * @param {adapt.vtree.NodePosition} np1 + * @param {adapt.vtree.NodePosition} np2 + * @returns {boolean} + */ +adapt.vtree.isSameNodePosition = function(np1, np2) { + if (np1 === np2) { + return true; + } + if (!np1 || !np2) { + return false; + } + if (np1.offsetInNode !== np2.offsetInNode || np1.after !== np2.after || np1.steps.length !== np2.steps.length) { + return false; + } + for (var i = 0; i < np1.steps.length; i++) { + if (!adapt.vtree.isSameNodePositionStep(np1[i], np2[i])) { + return false; + } + } + return true; +}; + /** * @param {Node} node * @return {adapt.vtree.NodePosition} @@ -822,6 +864,47 @@ adapt.vtree.ChunkPosition.prototype.clone = function() { return result; }; +/** + * @param {adapt.vtree.ChunkPosition} other + * @returns {boolean} + */ +adapt.vtree.ChunkPosition.prototype.isSamePosition = function(other) { + if (!other) { + return false; + } + if (this === other) { + return true; + } + if (!adapt.vtree.isSameNodePosition(this.primary, other.primary)) { + return false; + } + if (this.floats) { + if (!other.floats || this.floats.length !== other.floats.length) { + return false; + } + for (var i = 0; i < this.floats.length; i++) { + if (!adapt.vtree.isSameNodePosition(this.floats[i], other.floats[i])) { + return false; + } + } + } else if (other.floats) { + return false; + } + if (this.footnotes) { + if (!other.footnotes || this.footnotes.length !== other.footnotes.length) { + return false; + } + for (var i = 0; i < this.footnotes.length; i++) { + if (!adapt.vtree.isSameNodePosition(this.footnotes[i], other.footnotes[i])) { + return false; + } + } + } else if (other.footnotes) { + return false; + } + return true; +}; + /** * @param {adapt.vtree.ChunkPosition} chunkPosition * @param {adapt.vtree.FlowChunk} flowChunk @@ -840,6 +923,14 @@ adapt.vtree.FlowChunkPosition.prototype.clone = function() { this.flowChunk); }; +/** + * @param {adapt.vtree.FlowChunkPosition} other + * @returns {boolean} + */ +adapt.vtree.FlowChunkPosition.prototype.isSamePosition = function(other) { + return !!other && (this === other || this.chunkPosition.isSamePosition(other.chunkPosition)); +}; + /** * @constructor */ @@ -868,6 +959,25 @@ adapt.vtree.FlowPosition.prototype.clone = function() { return newfp; }; +/** + * @param {adapt.vtree.FlowPosition} other + * @returns {boolean} + */ +adapt.vtree.FlowPosition.prototype.isSamePosition = function(other) { + if (this === other) { + return true; + } + if (!other || this.positions.length !== other.positions.length) { + return false; + } + for (var i = 0; i < this.positions.length; i++) { + if (!this.positions[i].isSamePosition(other.positions[i])) { + return false; + } + } + return true; +}; + /** * @param {number} offset * @return {boolean} @@ -891,7 +1001,7 @@ adapt.vtree.LayoutPosition = function() { */ this.flows = {}; /** - * @type {Object.} + * @type {!Object.} */ this.flowPositions = {}; /** @@ -917,6 +1027,31 @@ adapt.vtree.LayoutPosition.prototype.clone = function() { return newcp; }; +/** + * @param {adapt.vtree.LayoutPosition} other + * @returns {boolean} + */ +adapt.vtree.LayoutPosition.prototype.isSamePosition = function(other) { + if (this === other) { + return true; + } + if (!other || this.page !== other.page || this.highestSeenOffset !== other.highestSeenOffset) { + return false; + } + var thisFlowNames = Object.keys(this.flowPositions); + var otherFlowNames = Object.keys(other.flowPositions); + if (thisFlowNames.length !== otherFlowNames.length) { + return false; + } + for (var i = 0; i < thisFlowNames.length; i++) { + var flowName = thisFlowNames[i]; + if (!this.flowPositions[flowName].isSamePosition(other.flowPositions[flowName])) { + return false; + } + } + return true; +}; + /** * @param {string} name flow name. * @param {number} offset diff --git a/test/files/target-counter.html b/test/files/target-counter.html index 7bfae90f0..78e8e5dc9 100644 --- a/test/files/target-counter.html +++ b/test/files/target-counter.html @@ -5,13 +5,31 @@ target-counter -

    'foo' value of #foo-target = =

    -

    'foo' values of #foofoo-target = =

    -

    #foo-target is on page =

    -

    foo=

    -

    foo=

    -

    (#foo-target) foo=

    -

    foos=, (#foofoo-target) foos=, foo=

    -

    foo=

    -

    'foo' value of #foo-target = =

    -

    'foo' values of #foofoo-target = =

    -

    'bar' value of #bar-target = =

    -

    'bar' values of #barbar-target = =

    -

    #foo-target is on page =

    -

    #bar-target is on page =

    +
    +

    'foo' value of #foo-target = =

    +

    'foo' values of #foofoo-target = =

    +

    #foo-target is on page =

    +

    foo=

    +

    foo=

    +

    (#foo-target) foo=

    +

    foos=, (#foofoo-target) foos=, foo=

    +

    foo=

    +

    'foo' value of #foo-target = =

    +

    'foo' values of #foofoo-target = =

    +

    'bar' value of #bar-target = =

    +

    'bar' values of #barbar-target = =

    +

    #foo-target is on page =

    +

    #bar-target is on page =

    +
    + +
    +

    'bar' value of #bar-target = =

    +

    'bar' values of #barbar-target = =

    +

    #bar-target is on page =

    +

    bar=

    +

    bar=

    +

    (#bar-target) bar=

    +

    bars=, (#barbar-target) bars=, bar=

    +

    bar=

    +

    'foo' value of #foo-target = =

    +

    'foo' values of #foofoo-target = =

    +

    'bar' value of #bar-target = =

    +

    'bar' values of #barbar-target = =

    +

    #foo-target is on page =

    +

    #bar-target is on page =

    +
    -

    'bar' value of #bar-target = =

    -

    'bar' values of #barbar-target = =

    -

    #bar-target is on page =

    -

    bar=

    -

    bar=

    -

    (#bar-target) bar=

    -

    bars=, (#barbar-target) bars=, bar=

    -

    bar=

    -

    'foo' value of #foo-target = =

    -

    'foo' values of #foofoo-target = =

    -

    'bar' value of #bar-target = =

    -

    'bar' values of #barbar-target = =

    -

    #foo-target is on page =

    -

    #bar-target is on page =

    +
    +

    #baz1-target is on baz This sentence contains #baz1-target → inside it. The previous sentence should be "This sentence contains #baz1-target → inside it."

    +
    \ No newline at end of file From ff87651264ae7ff67e9d03169583f0624354f3b3 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Tue, 7 Jun 2016 22:39:07 +0900 Subject: [PATCH 16/23] Do not save an unresolved reference if the same reference is already saved --- src/vivliostyle/counters.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index 367a30344..b310bfe65 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -39,6 +39,23 @@ goog.scope(function() { /** @type {number} */ this.pageIndex = -1; }; + /** + * @param {vivliostyle.counters.TargetCounterReference} other + * @returns {boolean} + */ + vivliostyle.counters.TargetCounterReference.prototype.equals = function(other) { + if (this === other) { + return true; + } + if (!other) { + return false; + } + return this.targetId === other.targetId && + this.resolved === other.resolved && + this.spineIndex === other.spineIndex && + this.pageIndex === other.pageIndex; + }; + /** * Returns if the reference is resolved or not. * @returns {boolean} @@ -438,7 +455,9 @@ goog.scope(function() { if (!arr) { arr = this.unresolvedReferences[ref.targetId] = []; } - arr.push(ref); + if (arr.every(function(r) { return !ref.equals(r); })) { + arr.push(ref); + } } } From f738271a18485c45cace82dfe1e93b06d5d2a89e Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Wed, 8 Jun 2016 00:52:48 +0900 Subject: [PATCH 17/23] Save page break position right after layout of a single page is done --- src/adapt/epub.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/adapt/epub.js b/src/adapt/epub.js index daf8065fa..8098fc934 100644 --- a/src/adapt/epub.js +++ b/src/adapt/epub.js @@ -1079,6 +1079,7 @@ adapt.epub.OPFView.prototype.renderSinglePage = function(viewItem, pos) { var cont = null; if (pos) { var prevPos = viewItem.layoutPositions[pos.page]; + viewItem.layoutPositions[pos.page] = pos; if (prevPos) { if (!pos.isSamePosition(prevPos)) { self.pageIndex = pos.page; @@ -1191,7 +1192,6 @@ adapt.epub.OPFView.prototype.renderPage = function() { pos = result.position; var pageIndex = result.pageIndex; if (pos) { - viewItem.layoutPositions.push(pos); if (seekOffset >= 0) { // Searching for offset, don't know the page number. var offset = viewItem.instance.getPosition(pos); @@ -1227,9 +1227,7 @@ adapt.epub.OPFView.prototype.renderPage = function() { self.renderSinglePage(viewItem, pos).then(function(result) { var page = result.page; pos = result.position; - if (pos) { - viewItem.layoutPositions[self.pageIndex + 1] = pos; - } else { + if (!pos) { viewItem.complete = true; page.isLastPage = viewItem.item.spineIndex == self.opf.spine.length - 1; } From 5bec940a608cf52aa92395ef9e5e950d2d770e80 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Wed, 8 Jun 2016 16:00:55 +0900 Subject: [PATCH 18/23] Re-layout next page too if page break position has been moved by cross reference resolution *only if the next page has been already laid out* --- src/adapt/epub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapt/epub.js b/src/adapt/epub.js index 8098fc934..c96e220c9 100644 --- a/src/adapt/epub.js +++ b/src/adapt/epub.js @@ -1080,7 +1080,7 @@ adapt.epub.OPFView.prototype.renderSinglePage = function(viewItem, pos) { if (pos) { var prevPos = viewItem.layoutPositions[pos.page]; viewItem.layoutPositions[pos.page] = pos; - if (prevPos) { + if (prevPos && viewItem.pages[pos.page]) { if (!pos.isSamePosition(prevPos)) { self.pageIndex = pos.page; cont = self.renderSinglePage(viewItem, pos); From ab3d9c94184f4e2a883920832a8bfcf48d1efaa2 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Wed, 8 Jun 2016 16:02:25 +0900 Subject: [PATCH 19/23] Mark the unresolved reference as resolved even if no counter with the name is defined --- src/vivliostyle/counters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index b310bfe65..097435e93 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -226,8 +226,8 @@ goog.scope(function() { var pageCounters = self.getTargetPageCounters(transformedId); if (pageCounters) { // The target element has already been laid out. + self.counterStore.resolveReference(transformedId); if (pageCounters[name]) { - self.counterStore.resolveReference(transformedId); var pageCountersOfName = pageCounters[name]; return format(pageCountersOfName[pageCountersOfName.length - 1] || null) } else { From b0288a356d78d6d92b0d256e6233b7044e07cde5 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Wed, 8 Jun 2016 17:42:25 +0900 Subject: [PATCH 20/23] Prevent reference target from going back to the previous page by reference resolution If a reference target is allowed to go back to a page earlier than one on which it was placed previously, reference resolution can lead to an infinite loop. To avoid infinite loops, a reference target is allowed to go to a page later than one on which it was placed previously, but it is not allowed to go back to an earlier page. If the target can fit in an earlier page, a page break is inserted just before the target and the target is laid out on the page on which it was placed previously. To represent the influence reference resolution has on layout, `adapt.layout.LayoutConstraint` interface is introduced. `allowLayout` method of `LayoutConstraint` receives a `NodeContext` and returns whether it can be laid out at the current position. In this case, a `NodeContext` is not allowed to be laid out at the current position if the page index of the current position is earlier than the index of the page on which the target was placed previously. --- src/adapt/layout.js | 41 ++++++++++++++++--- src/adapt/ops.js | 12 ++++-- src/vivliostyle/counters.js | 72 ++++++++++++++++++++++++++++++++++ test/files/target-counter.html | 62 +++++++++++++++++++++++++---- 4 files changed, 171 insertions(+), 16 deletions(-) diff --git a/src/adapt/layout.js b/src/adapt/layout.js index 24f89c03b..340341513 100644 --- a/src/adapt/layout.js +++ b/src/adapt/layout.js @@ -112,6 +112,19 @@ adapt.layout.calculateEdge = function(nodeContext, clientLayout, } }; +/** + * Represents a constraint on layout + * @interface + */ +adapt.layout.LayoutConstraint = function() {}; + +/** + * Returns if this constraint allows the node context to be laid out at the current position. + * @param {adapt.vtree.NodeContext} nodeContext + * @return {boolean} + */ +adapt.layout.LayoutConstraint.prototype.allowLayout = function(nodeContext) {}; + /** * Potential breaking position. * @interface @@ -240,14 +253,16 @@ adapt.layout.validateCheckPoints = function(checkPoints) { * @constructor * @param {Element} element * @param {adapt.vtree.LayoutContext} layoutContext - * @param {adapt.vtree.ClientLayout} clientLayout + * @param {adapt.vtree.ClientLayout} clientLayout + * @param {adapt.layout.LayoutConstraint} layoutConstraint * @extends {adapt.vtree.Container} */ -adapt.layout.Column = function(element, layoutContext, clientLayout) { +adapt.layout.Column = function(element, layoutContext, clientLayout, layoutConstraint) { adapt.vtree.Container.call(this, element); /** @type {Node} */ this.last = element.lastChild; /** @type {adapt.vtree.LayoutContext} */ this.layoutContext = layoutContext; /** @type {adapt.vtree.ClientLayout} */ this.clientLayout = clientLayout; + /** @const */ this.layoutConstraint = layoutConstraint; /** @type {Document} */ this.viewDocument = element.ownerDocument; /** @type {boolean} */ this.isFootnote = false; /** @type {number} */ this.startEdge = 0; @@ -275,7 +290,7 @@ goog.inherits(adapt.layout.Column, adapt.vtree.Container); * @return {adapt.layout.Column} */ adapt.layout.Column.prototype.clone = function() { - var copy = new adapt.layout.Column(this.element, this.layoutContext, this.clientLayout); + var copy = new adapt.layout.Column(this.element, this.layoutContext, this.clientLayout, this.layoutConstraint); copy.copyFrom(this); copy.last = this.last; copy.isFootnote = this.isFootnote; @@ -444,6 +459,12 @@ adapt.layout.Column.prototype.buildViewToNextBlockEdge = function(position, chec bodyFrame.breakLoop(); return; } + if (!self.layoutConstraint.allowLayout(position)) { + position = position.modify(); + position.overflow = true; + bodyFrame.breakLoop(); + return; + } if (position.floatSide && !self.vertical) { // TODO: implement floats and footnotes properly self.layoutFloatOrFootnote(position).then(function(positionParam) { @@ -502,7 +523,11 @@ adapt.layout.Column.prototype.buildDeepElementView = function(position) { self.layoutContext.nextInTree(position1).then(function(positionParam) { position = /** @type {adapt.vtree.NodeContext} */ (positionParam); if (!position || position.sourceNode == sourceNode) { - bodyFrame.breakLoop(); + bodyFrame.breakLoop(); + } else if (!self.layoutConstraint.allowLayout(position)) { + position = position.modify(); + position.overflow = true; + bodyFrame.breakLoop(); } else { bodyFrame.continueLoop(); } @@ -748,7 +773,7 @@ adapt.layout.Column.prototype.layoutFootnoteInner = function(boxOffset, footnote adapt.base.setCSSProperty(footnoteContainer, "position", "absolute"); var layoutContext = self.layoutContext.clone(); footnoteArea = new adapt.layout.Column(footnoteContainer, - layoutContext, self.clientLayout); + layoutContext, self.clientLayout, self.layoutConstraint); self.footnoteArea = footnoteArea; footnoteArea.vertical = self.layoutContext.applyFootnoteStyle(self.vertical, footnoteContainer); footnoteArea.isFootnote = true; @@ -1793,6 +1818,12 @@ adapt.layout.Column.prototype.skipEdges = function(nodeContext, leadingEdge) { // Non-displayable content, skip break; } + if (!self.layoutConstraint.allowLayout(nodeContext)) { + nodeContext = nodeContext.modify(); + nodeContext.overflow = true; + loopFrame.breakLoop(); + return; + } if (nodeContext.inline && nodeContext.viewNode.nodeType != 1) { if (adapt.vtree.canIgnore(nodeContext.viewNode, nodeContext.whitespace)) { // Ignorable text content, skip diff --git a/src/adapt/ops.js b/src/adapt/ops.js index d945b0ebf..b21742438 100644 --- a/src/adapt/ops.js +++ b/src/adapt/ops.js @@ -595,8 +595,13 @@ adapt.ops.StyleInstance.prototype.layoutColumn = function(region, flowName, regi return frame.result(); }; +adapt.ops.StyleInstance.prototype.createLayoutConstraint = function() { + var pageIndex = this.currentLayoutPosition.page - 1; + return this.counterStore.createLayoutConstraint(pageIndex); +}; + /** - * @param {adapt.vtree.Page} page + * @param {!adapt.vtree.Page} page * @param {adapt.pm.PageBoxInstance} boxInstance * @param {HTMLElement} parentContainer * @param {number} offsetX @@ -667,6 +672,7 @@ adapt.ops.StyleInstance.prototype.layoutContainer = function(page, boxInstance, self.viewport, self.styler, regionIds, self.xmldoc, self.faces, self.style.footnoteProps, self, page, self.customRenderer, self.fallbackMap, pageFloatHolder, this.documentURLTransformer); + var layoutConstraint = this.createLayoutConstraint(); var columnIndex = 0; var region = null; frame.loopWithFrame(function(loopFrame) { @@ -676,7 +682,7 @@ adapt.ops.StyleInstance.prototype.layoutContainer = function(page, boxInstance, var columnContainer = self.viewport.document.createElement("div"); adapt.base.setCSSProperty(columnContainer, "position", "absolute"); boxContainer.appendChild(columnContainer); - region = new adapt.layout.Column(columnContainer, layoutContext, self.clientLayout); + region = new adapt.layout.Column(columnContainer, layoutContext, self.clientLayout, layoutConstraint); region.vertical = layoutContainer.vertical; region.snapHeight = layoutContainer.snapHeight; region.snapWidth = layoutContainer.snapWidth; @@ -696,7 +702,7 @@ adapt.ops.StyleInstance.prototype.layoutContainer = function(page, boxInstance, region.originX = offsetX + layoutContainer.paddingLeft; region.originY = offsetY + layoutContainer.paddingTop; } else { - region = new adapt.layout.Column(boxContainer, layoutContext, self.clientLayout); + region = new adapt.layout.Column(boxContainer, layoutContext, self.clientLayout, layoutConstraint); region.copyFrom(layoutContainer); layoutContainer = region; } diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index 097435e93..7e3ca0117 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -8,7 +8,9 @@ goog.provide("vivliostyle.counters"); goog.require("adapt.base"); goog.require("adapt.expr"); goog.require("adapt.csscasc"); +goog.require("adapt.vtree"); goog.require("adapt.cssstyler"); +goog.require("adapt.layout"); goog.scope(function() { @@ -71,6 +73,13 @@ goog.scope(function() { this.resolved = true; }; + /** + * Marks that this reference is unresolved. + */ + vivliostyle.counters.TargetCounterReference.prototype.unresolve = function() { + this.resolved = false; + }; + /** * @param {!vivliostyle.counters.CounterStore} counterStore * @param {string} baseURL @@ -284,11 +293,13 @@ goog.scope(function() { this.currentPageCounters["page"] = [0]; /** @type {!adapt.csscasc.CounterValues} */ this.previousPageCounters = {}; /** @const {!Array} */ this.currentPageCountersStack = []; + /** @const {!Object} */ this.pageIndicesById = {}; /** @type {adapt.vtree.Page} */ this.currentPage = null; /** @const {!Array} */ this.newReferencesOfCurrentPage = []; /** @type {!Array} */ this.referencesToSolve = []; /** @type {!Array>} */ this.referencesToSolveStack = []; /** @const {!Object>} */ this.unresolvedReferences = {}; + /** @const {!Object>} */ this.resolvedReferences = {}; }; /** @@ -401,6 +412,10 @@ goog.scope(function() { */ vivliostyle.counters.CounterStore.prototype.resolveReference = function(id) { var unresolvedRefs = this.unresolvedReferences[id]; + var resolvedRefs = this.resolvedReferences[id]; + if (!resolvedRefs) { + resolvedRefs = this.resolvedReferences[id] = []; + } for (var i = 0; i < this.referencesToSolve.length;) { var ref = this.referencesToSolve[i]; if (ref.targetId === id) { @@ -412,6 +427,7 @@ goog.scope(function() { unresolvedRefs.splice(j, 1); } } + resolvedRefs.push(ref); } else { i++; } @@ -441,6 +457,22 @@ goog.scope(function() { var currentPageCounters = cloneCounterValues(this.currentPageCounters); ids.forEach(function(id) { this.pageCountersById[id] = currentPageCounters; + var oldPageIndex = this.pageIndicesById[id]; + if (oldPageIndex && oldPageIndex.pageIndex < pageIndex) { + var resolvedRefs = this.resolvedReferences[id]; + if (resolvedRefs) { + var unresolvedRefs = this.unresolvedReferences[id]; + if (!unresolvedRefs) { + unresolvedRefs = this.unresolvedReferences[id] = []; + } + var ref; + while (ref = resolvedRefs.shift()) { + ref.unresolve(); + unresolvedRefs.push(ref); + } + } + } + this.pageIndicesById[id] = {spineIndex: spineIndex, pageIndex: pageIndex}; }, this); } @@ -516,4 +548,44 @@ goog.scope(function() { this.referencesToSolve = this.referencesToSolveStack.pop(); }; + /** + * @param {!vivliostyle.counters.CounterStore} counterStore + * @param {number} pageIndex + * @constructor + * @implements {adapt.layout.LayoutConstraint} + */ + function LayoutConstraint(counterStore, pageIndex) { + /** @const */ this.counterStore = counterStore; + /** @const */ this.pageIndex = pageIndex; + } + + /** + * @override + */ + LayoutConstraint.prototype.allowLayout = function(nodeContext) { + if (!nodeContext || nodeContext.after) { + return true; + } + var viewNode = nodeContext.viewNode; + if (!viewNode || viewNode.nodeType !== 1) { + return true; + } + var id = viewNode.getAttribute("id") || viewNode.getAttribute("name"); + if (!id) { + return true; + } + var pageIndex = this.counterStore.pageIndicesById[id]; + if (!pageIndex) { + return true; + } + return this.pageIndex >= pageIndex.pageIndex; + }; + + /** + * @param {number} pageIndex + * @returns {!adapt.layout.LayoutConstraint} + */ + vivliostyle.counters.CounterStore.prototype.createLayoutConstraint = function(pageIndex) { + return new LayoutConstraint(this, pageIndex); + }; }); diff --git a/test/files/target-counter.html b/test/files/target-counter.html index 78e8e5dc9..cd70ee30c 100644 --- a/test/files/target-counter.html +++ b/test/files/target-counter.html @@ -7,16 +7,16 @@ @page { size: 460px; margin: 50px; - counter-increment: foo bar baz; + counter-increment: foo bar baz bazz bazzz; @top-center { content: "Page " counter(page); } @bottom-center { - content: "baz " counter(baz, upper-roman); + content: "baz: " counter(baz, upper-roman) ", bazz: " counter(bazz, upper-roman) ", bazzz: " counter(bazzz, upper-roman); } } @page:first { - counter-reset: baz 995; + counter-reset: baz 992 bazz 990 bazzz 988; } * { @@ -101,14 +101,42 @@ content: target-counter(attr(data-ref-bar-page), page, lower-roman); } - .baz1 { + .baz { margin-top: 330px; text-indent: 60px; } - .ref-baz1::before { - content: target-counter("#baz1-target", baz, upper-roman); + .ref-baz::before { + content: target-counter(attr(data-href), baz, upper-roman); } - #baz1-target { + #baz1-target, #baz2-target, #baz3-target { + border-left: 1px solid blue; + } + + .bazz { + margin-top: 305px; + } + .ref-bazz { + margin-right: 80px; + } + .ref-bazz::before { + content: target-counter(attr(data-href), bazz, upper-roman); + } + #bazz-target { + border: 1px solid blue; + } + + .bazzz { + margin-top: 310px; + text-indent: 60px; + } + .ref-bazzz::before { + content: target-counter(attr(data-href), bazzz, upper-roman); + } + .bazzz-target-container { + float: left; + width: 170px; + } + #bazzz-target { border-left: 1px solid blue; } @@ -149,7 +177,25 @@
    -

    #baz1-target is on baz This sentence contains #baz1-target → inside it. The previous sentence should be "This sentence contains #baz1-target → inside it."

    +

    #baz1-target is on baz This sentence contains #baz1-target → inside it. The previous sentence should be "This sentence contains #baz1-target → inside it."

    +
    + +
    +

    #baz2-target is on baz #baz2-target → This sentence should be right after #baz2-target. The previous sentence should be "This sentence should be right after #baz2-target."

    +
    + +
    +

    #baz3-target is on baz #baz3-target → This sentence should be right after #baz3-target. The previous sentence should be "This sentence should be right after #baz3-target."

    +
    + +
    +

    #bazz-target is on bazz #bazz-target ↓

    +
    This sentence should be inside #bazz-target. The previous sentence should be "This sentence should be inside #bazz-target."
    +
    + +
    +

    #bazzz-target is on bazzz #bazzz-target ↓

    +
    This is #bazzz-target.
    \ No newline at end of file From 0d9973b25032cdfe887a04913df87dc703208921 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Wed, 8 Jun 2016 19:25:59 +0900 Subject: [PATCH 21/23] Refresh cross reference that was already resolved from the beginning when it has been moved to another page --- src/vivliostyle/counters.js | 21 ++++++++++++++++----- test/files/target-counter.html | 27 ++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/vivliostyle/counters.js b/src/vivliostyle/counters.js index 7e3ca0117..b94daea3b 100644 --- a/src/vivliostyle/counters.js +++ b/src/vivliostyle/counters.js @@ -416,6 +416,7 @@ goog.scope(function() { if (!resolvedRefs) { resolvedRefs = this.resolvedReferences[id] = []; } + var pushed = false; for (var i = 0; i < this.referencesToSolve.length;) { var ref = this.referencesToSolve[i]; if (ref.targetId === id) { @@ -428,10 +429,14 @@ goog.scope(function() { } } resolvedRefs.push(ref); + pushed = true; } else { i++; } } + if (!pushed) { + this.saveReferenceOfCurrentPage(id, true); + } }; /** @@ -482,15 +487,21 @@ goog.scope(function() { ref.pageCounters = prevPageCounters; ref.spineIndex = spineIndex; ref.pageIndex = pageIndex; - if (!ref.isResolved()) { - var arr = this.unresolvedReferences[ref.targetId]; + var arr; + if (ref.isResolved()) { + arr = this.resolvedReferences[ref.targetId]; if (!arr) { - arr = this.unresolvedReferences[ref.targetId] = []; + arr = this.resolvedReferences[ref.targetId] = []; } - if (arr.every(function(r) { return !ref.equals(r); })) { - arr.push(ref); + } else { + arr = this.unresolvedReferences[ref.targetId]; + if (!arr) { + arr = this.unresolvedReferences[ref.targetId] = []; } } + if (arr.every(function(r) { return !ref.equals(r); })) { + arr.push(ref); + } } this.currentPage = null; diff --git a/test/files/target-counter.html b/test/files/target-counter.html index cd70ee30c..26abb3a88 100644 --- a/test/files/target-counter.html +++ b/test/files/target-counter.html @@ -7,16 +7,18 @@ @page { size: 460px; margin: 50px; - counter-increment: foo bar baz bazz bazzz; + counter-increment: foo bar baz bazz bazzz qux; @top-center { + font-size: 10px; content: "Page " counter(page); } @bottom-center { - content: "baz: " counter(baz, upper-roman) ", bazz: " counter(bazz, upper-roman) ", bazzz: " counter(bazzz, upper-roman); + font-size: 10px; + content: "baz: " counter(baz, upper-roman) ", bazz: " counter(bazz, upper-roman) ", bazzz: " counter(bazzz, upper-roman) ", qux: " counter(qux, upper-roman); } } @page:first { - counter-reset: baz 992 bazz 990 bazzz 988; + counter-reset: baz 992 bazz 990 bazzz 988 qux 984; } * { @@ -139,6 +141,17 @@ #bazzz-target { border-left: 1px solid blue; } + + .qux { + margin-top: 330px; + text-indent: 120px; + } + .ref-qux::before { + content: target-counter(attr(data-href), qux, upper-roman); + } + #qux-target, #quxx-target { + border: 1px solid blue; + } @@ -197,5 +210,13 @@

    #bazzz-target is on bazzz #bazzz-target ↓

    This is #bazzz-target.
    + +
    +

    #quxx-target is on qux #qux-target Blah blah blah blah blah.

    +
    + +
    +

    #qux-target is on qux . quxx-target

    +
    \ No newline at end of file From 6aa423e9a6d16451ba0c1acdefeff95cb1a49416 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Wed, 8 Jun 2016 19:35:36 +0900 Subject: [PATCH 22/23] Update Change Log --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42314e0c5..54b7f7b94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ - Support `left`/`right`/`recto`/`verso` values for `(page-)break-before`/`(page-)break-after` - - Spec: [CSS Fragmentation - Page Break Values](http://dev.w3.org/csswg/css-break/#page-break-values) +- Support cross references by `target-counter()`/`target-counters()` + - + - Spec: [CSS Generated Content Module Level 3 - Cross references and the target-* functions](https://drafts.csswg.org/css-content/#cross-references) ### Fixed - Fix a bug that `clear` is ignored when `white-space` property is used before the element From 0d151f1a4a972632138bb74e0f2e9c8c70c365e8 Mon Sep 17 00:00:00 2001 From: KAWAKUBO Toru Date: Wed, 8 Jun 2016 20:10:32 +0900 Subject: [PATCH 23/23] Update Supported Features --- doc/supported-features.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/supported-features.md b/doc/supported-features.md index 585568cee..0cf616889 100644 --- a/doc/supported-features.md +++ b/doc/supported-features.md @@ -19,6 +19,13 @@ Properties where Allowed prefixes is indicated may be used with a - [HSL color values](https://www.w3.org/TR/css3-color/#hsl-color), [HSLA color values](https://www.w3.org/TR/css3-color/#hsla-color) - [Extended color keywords](https://www.w3.org/TR/css3-color/#svg-color) - [‘currentColor’ color keyword](https://www.w3.org/TR/css3-color/#currentcolor) +- [Attribute references: `attr()`](https://www.w3.org/TR/css-values/#attr-notation) + - Supported in all browsers + - Only supported in values of `content` property. + - Only 'string' and 'url' types are supported. +- [Cross references: `target-counter()` and `target-counters()`](https://drafts.csswg.org/css-content/#cross-references) + - Supported in all browsers + - Only supported in values of `content` property. ## Selectors