Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for break-* properties #127

Merged
merged 10 commits into from Jan 7, 2016
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -59,6 +59,12 @@
- <https://github.com/vivliostyle/vivliostyle.js/issues/94>
- Allow units spelled in upper case
- <https://github.com/vivliostyle/vivliostyle.js/issues/36>
- Fix handling of forced and avoid break values; update acceptable values for `break-*` properties
- Spec: [CSS Fragmentation Module Level 3](https://drafts.csswg.org/css-break/)
- Note that the current implementation treats all values of `break-inside` other than `auto` as the same as `avoid`. The fine-grained control (distinguishing `avoid`, `avoid-page`, `avoid-column` and `avoid-region`) will be a future task and tracked with a separate issue.
- Note that though the spec requires to honor multiple `break-before`/`break-after` values at a single break point, the current implementation choose one of them and discard the others. The fine-grained control of these break values will be a future task and tracked with a separate issue.
- <https://github.com/vivliostyle/vivliostyle.js/issues/26>
- <https://github.com/vivliostyle/vivliostyle.js/issues/103>

## [0.2.0](https://github.com/vivliostyle/vivliostyle.js/releases/tag/0.2.0) - 2015-09-16
Beta release.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -3,7 +3,7 @@
"version": "0.2.1-pre",
"description": "Library for web sites with rich paged viewing and EPUB support, shared with Vivliostyle Formatter & Browser",
"scripts": {
"build": "mkdirp lib && java -jar node_modules/google-closure-compiler/compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --warning_level VERBOSE --output_wrapper_file src/wrapper.js --externs src/externs.js --js_output_file lib/vivliostyle.min.js src/closure/goog/base.js src/closure/goog/*/*.js src/adapt/*.js src/vivliostyle/*.js",
"build": "mkdirp lib && java -jar node_modules/google-closure-compiler/compiler.jar --compilation_level ADVANCED_OPTIMIZATIONS --language_in ECMASCRIPT5 --warning_level VERBOSE --output_wrapper_file src/wrapper.js --externs src/externs.js --js_output_file lib/vivliostyle.min.js src/closure/goog/base.js src/closure/goog/*/*.js src/adapt/*.js src/vivliostyle/*.js",
"test": "karma start test/conf/karma-sauce.conf.js",
"test:local": "karma start test/conf/karma-local.conf.js"
},
Expand Down
4 changes: 2 additions & 2 deletions resources/validation.txt
Expand Up @@ -229,10 +229,10 @@ opacity = NUM;
[moz,webkit]column-rule-color = COLOR;
[moz,webkit]column-rule-style = BORDER_SIDE_STYLE;
[moz,webkit]column-rule-width = BORDER_SIDE_WIDTH;
BREAK = auto | always | avoid | left | right | page | column | region;
BREAK = auto | avoid | avoid-page | page | left | right | recto | verso | avoid-column | column | avoid-region | region;
break-before = BREAK;
break-after = BREAK;
break-inside = auto | avoid | avoid-page | avoid-column;
break-inside = auto | avoid | avoid-page | avoid-column | avoid-region;
[webkit]column-span = none | all;
[moz]column-fill = auto | balance;

Expand Down
2 changes: 1 addition & 1 deletion samples/adaptive-layout/apollo8/style.css
Expand Up @@ -36,7 +36,7 @@ h2.subtitle {
font-style: italic;
font-family: serif;
color: white;
break-after: always;
break-after: page;
}

.cover {
Expand Down
2 changes: 2 additions & 0 deletions src/adapt/css.js
Expand Up @@ -871,6 +871,7 @@ adapt.css.toNumber = function(val, context) {
adapt.css.ident = {
absolute: adapt.css.getName("absolute"),
all: adapt.css.getName("all"),
always: adapt.css.getName("always"),
auto: adapt.css.getName("auto"),
avoid: adapt.css.getName("avoid"),
block: adapt.css.getName("block"),
Expand All @@ -895,6 +896,7 @@ adapt.css.ident = {
normal: adapt.css.getName("normal"),
oeb_page_foot: adapt.css.getName("oeb-page-foot"),
oeb_page_head: adapt.css.getName("oeb-page-head"),
page: adapt.css.getName("page"),
relative: adapt.css.getName("relative"),
right: adapt.css.getName("right"),
scale: adapt.css.getName("scale"),
Expand Down
11 changes: 11 additions & 0 deletions src/adapt/csscasc.js
Expand Up @@ -6,6 +6,7 @@
goog.provide('adapt.csscasc');

goog.require('vivliostyle.logging');
goog.require('vivliostyle.plugin');
goog.require('adapt.expr');
goog.require('adapt.css');
goog.require('adapt.task');
Expand Down Expand Up @@ -2739,6 +2740,16 @@ adapt.csscasc.CascadeParserHandler.prototype.simpleProperty = function(name, val
this.simpleProperty("flow-into", value, important);
value = adapt.css.ident.block;
}

var hooks = vivliostyle.plugin.getHooksForName("SIMPLE_PROPERTY");
hooks.forEach(function(hook) {
var original = {"name": name, "value": value, "important": important};
var converted = hook(original);
name = converted["name"];
value = converted["value"];
important = converted["important"];
});

var specificity = important ? this.getImportantSpecificity() : this.getBaseSpecificity();
var cascval = this.condition
? new adapt.csscasc.ConditionalCascadeValue(value, specificity, this.condition)
Expand Down
35 changes: 11 additions & 24 deletions src/adapt/layout.js
Expand Up @@ -10,26 +10,11 @@ goog.require('vivliostyle.logging');
goog.require('adapt.base');
goog.require('adapt.geom');
goog.require('adapt.task');
goog.require('vivliostyle.break');
goog.require('adapt.vtree');

goog.provide('adapt.layout');

/**
* @param {?string} break1
* @param {?string} break2
* @return {?string}
*/
adapt.layout.combineBreaks = function(break1, break2) {
if (!break1)
return break2;
if (!break2)
return break1;
// It is important to prioritize "avoid" to prevent breaking after a page/region break.
if (break1 == "avoid" || break2 == "avoid")
return "avoid";
return break1;
};

/** @const */
adapt.layout.mediaTags = {
"img": true,
Expand Down Expand Up @@ -203,7 +188,7 @@ adapt.layout.EdgeBreakPosition.prototype.findAcceptableBreak = function(column,
* @override
*/
adapt.layout.EdgeBreakPosition.prototype.getMinBreakPenalty = function() {
return (this.breakOnEdge == "avoid" ? 1 : 0)
return (vivliostyle.break.isAvoidBreakValue(this.breakOnEdge) ? 1 : 0)
+ (this.overflows ? 3 : 0)
+ (this.position.parent ? this.position.parent.breakPenalty : 0);
};
Expand Down Expand Up @@ -1703,7 +1688,7 @@ adapt.layout.Column.prototype.skipEdges = function(nodeContext, leadingEdge) {
var self = this;
/** @type {!adapt.task.Frame.<adapt.vtree.NodeContext>} */ var frame
= adapt.task.newFrame("skipEdges");
var breakAtTheEdge = leadingEdge ? "avoid" : null;
var breakAtTheEdge = null;
var lastAfterNodeContext = null;
var trailingEdgeContexts = [];
frame.loopWithFrame(function(loopFrame) {
Expand Down Expand Up @@ -1757,7 +1742,7 @@ adapt.layout.Column.prototype.skipEdges = function(nodeContext, leadingEdge) {
// Trailing edge
lastAfterNodeContext = nodeContext.copy();
trailingEdgeContexts.push(lastAfterNodeContext);
breakAtTheEdge = adapt.layout.combineBreaks(nodeContext.breakAfter, breakAtTheEdge);
breakAtTheEdge = vivliostyle.break.resolveEffectiveBreakValue(breakAtTheEdge, nodeContext.breakAfter);
if (style && !(self.zeroIndent(style.paddingBottom) && self.zeroIndent(style.borderBottomWidth))) {
// Non-zero trailing inset.
if (self.saveEdgeAndCheckForOverflow(lastAfterNodeContext, null, true, breakAtTheEdge)) {
Expand All @@ -1774,8 +1759,10 @@ adapt.layout.Column.prototype.skipEdges = function(nodeContext, leadingEdge) {
}
} else {
// Leading edge
breakAtTheEdge = adapt.layout.combineBreaks(nodeContext.breakBefore, breakAtTheEdge);
if (breakAtTheEdge && breakAtTheEdge != "avoid" && breakAtTheEdge != "auto") {
breakAtTheEdge = vivliostyle.break.resolveEffectiveBreakValue(breakAtTheEdge, nodeContext.breakBefore);
// leadingEdge=true means that we are at the beginning of the new column and hence must avoid a break
// (Otherwise leading to an infinite loop)
if (!leadingEdge && vivliostyle.break.isForcedBreakValue(breakAtTheEdge)) {
// explicit page break
loopFrame.breakLoop();
self.pageBreakType = breakAtTheEdge;
Expand Down Expand Up @@ -1880,11 +1867,11 @@ adapt.layout.Column.prototype.skipTailEdges = function(nodeContext) {
var style = (/** @type {HTMLElement} */ (nodeContext.viewNode)).style;
if (nodeContext.after) {
// Trailing edge
breakAtTheEdge = adapt.layout.combineBreaks(nodeContext.breakAfter, breakAtTheEdge);
breakAtTheEdge = vivliostyle.break.resolveEffectiveBreakValue(breakAtTheEdge, nodeContext.breakAfter);
} else {
// Leading edge
breakAtTheEdge = adapt.layout.combineBreaks(nodeContext.breakBefore, breakAtTheEdge);
if (breakAtTheEdge && breakAtTheEdge != "avoid" && breakAtTheEdge != "auto") {
breakAtTheEdge = vivliostyle.break.resolveEffectiveBreakValue(breakAtTheEdge, nodeContext.breakBefore);
if (vivliostyle.break.isForcedBreakValue(breakAtTheEdge)) {
// explicit page break
loopFrame.breakLoop();
self.pageBreakType = breakAtTheEdge;
Expand Down
7 changes: 3 additions & 4 deletions src/adapt/vgen.js
Expand Up @@ -587,8 +587,7 @@ adapt.vgen.ViewFactory.prototype.createElementView = function(firstTime) {
}
var listItem = display === adapt.css.ident.list_item && computedStyle["ua-list-item-count"];
if (floating ||
computedStyle["break-inside"] === adapt.css.ident.avoid ||
computedStyle["page-break-inside"] === adapt.css.ident.avoid) {
(computedStyle["break-inside"] && computedStyle["break-inside"] !== adapt.css.ident.auto)) {
self.nodeContext.breakPenalty++;
} else if (display === adapt.css.ident.table_row) {
self.nodeContext.breakPenalty += 10;
Expand All @@ -597,11 +596,11 @@ adapt.vgen.ViewFactory.prototype.createElementView = function(firstTime) {
self.nodeContext.floatSide = floating ? floatSide.toString() : null;
self.nodeContext.floatReference = floatReference ? floatReference.toString() : null;
if (!self.nodeContext.inline) {
var breakAfter = computedStyle["break-after"] || computedStyle["page-break-after"];
var breakAfter = computedStyle["break-after"];
if (breakAfter) {
self.nodeContext.breakAfter = breakAfter.toString();
}
var breakBefore = computedStyle["break-before"] || computedStyle["page-break-before"];
var breakBefore = computedStyle["break-before"];
if (breakBefore) {
self.nodeContext.breakBefore = breakBefore.toString();
}
Expand Down
2 changes: 2 additions & 0 deletions src/source-list.js
Expand Up @@ -18,6 +18,7 @@
"vivliostyle/profile.js",
"vivliostyle/constants.js",
"vivliostyle/util.js",
"vivliostyle/plugin.js",
"vivliostyle/logical.js",
"adapt/base.js",
"adapt/sha1.js",
Expand All @@ -28,6 +29,7 @@
"adapt/xmldoc.js",
"adapt/expr.js",
"adapt/css.js",
"vivliostyle/break.js",
"adapt/csstok.js",
"adapt/cssparse.js",
"adapt/cssvalid.js",
Expand Down
110 changes: 110 additions & 0 deletions src/vivliostyle/break.js
@@ -0,0 +1,110 @@
/**
* Copyright 2016 Vivliostyle Inc.
* @fileoverview Control fragmentation
*/
goog.provide("vivliostyle.break");

goog.require("vivliostyle.plugin");
goog.require("adapt.css");

goog.scope(function() {

/**
* Convert old page-break-* properties to break-* properties with appropriate values
* as specified by CSS Fragmentation module: https://drafts.csswg.org/css-break/#page-break-properties
* @private
* @param {!{name: string, value: !adapt.css.Val, important: boolean}} original
* @returns {!{name: string, value: !adapt.css.Val, important: boolean}}
*/
vivliostyle.break.convertPageBreakAliases = function(original) {
var name = original["name"];
var value = original["value"];
switch (name) {
case "page-break-before":
case "page-break-after":
case "page-break-inside":
return {
"name": name.replace(/^page-/, ""),
"value": value === adapt.css.ident.always ? adapt.css.ident.page : value,
"important": original["important"]
};
default:
return original;
}
};
vivliostyle.plugin.registerHook("SIMPLE_PROPERTY", vivliostyle.break.convertPageBreakAliases);

/**
* @private
* @const {Object<?string, ?boolean>}
*/
vivliostyle.break.forcedBreakValues = {
"page": true,
"left": true,
"right": true,
"recto": true,
"verso": true,
"column": true,
"region": true
};

/**
* Returns if the value is one of the forced break values.
* @param {?string} value The break value to be judged. Treats null as 'auto'.
* @returns {boolean}
*/
vivliostyle.break.isForcedBreakValue = function(value) {
return !!vivliostyle.break.forcedBreakValues[value];
};

/**
* @private
* @const {Object<?string, ?boolean>}
*/
vivliostyle.break.avoidBreakValues = {
"avoid": true,
"avoid-page": true,
"avoid-column": true,
"avoid-region": true
};

/**
* Returns if the value is one of the avoid break values.
* @param {?string} value The break value to be judged. Treats null as 'auto'.
* @returns {boolean}
*/
vivliostyle.break.isAvoidBreakValue = function(value) {
return !!vivliostyle.break.avoidBreakValues[value];
};

/**
* Resolves the effective break value given two break values at a single break point.
* The order of the arguments are relevant, since a value specified on the latter element takes precedence over one on the former.
* A forced break value is chosen if present. Otherwise, an avoid break value is chosen if present.
* See CSS Fragmentation Module for the rule:
* https://drafts.csswg.org/css-break/#forced-breaks
* https://drafts.csswg.org/css-break/#unforced-breaks
* Note that though the spec requires to honor multiple break values at a single break point, the current implementation choose one of them and discard the others.
* @param {?string} first The break value specified on the former element. null means 'auto' (not specified)
* @param {?string} second The break value specified on the latter element. null means 'auto' (not specified)
* @returns {?string}
*/
vivliostyle.break.resolveEffectiveBreakValue = function(first, second) {
if (!first) {
return second;
} else if (!second) {
return first;
} else if (vivliostyle.break.isForcedBreakValue(second)) {
return second;
} else if (vivliostyle.break.isForcedBreakValue(first)) {
return first;
} else if (vivliostyle.break.isAvoidBreakValue(second)) {
return second;
} else if (vivliostyle.break.isAvoidBreakValue(first)) {
return first;
} else {
return second;
}
};

});