Skip to content

Commit 9f75a1d

Browse files
committed
feat: update viewport meta element requirements
This commit implements the post-CR changes on the requirements for the viewport meta element used to define the ICB in fixed-layout documents. Viewport syntax: - properties can now have several values - the `ViewportMeta` object now return a list of string values when looking up values for a name. ICB definition checks: - only the first `viewport` `meta` element of fixed-layout documents is checked - new `HTM-059` error is reported when `height` or `width` dimensions are specified more than once - new `HTM-060a` usage is reported for any subsequent `viewport` `meta` element found in fixed-layout documents - new `HTM-060b` usage is reported fo any `viewport` `meta` element found in reflowable documents Fix #1401, Fix #1449
1 parent 5fca49f commit 9f75a1d

File tree

35 files changed

+396
-113
lines changed

35 files changed

+396
-113
lines changed

src/main/java/com/adobe/epubcheck/messages/DefaultSeverities.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ private void initialize()
142142
severities.put(MessageId.HTM_056, Severity.ERROR);
143143
severities.put(MessageId.HTM_057, Severity.ERROR);
144144
severities.put(MessageId.HTM_058, Severity.ERROR);
145+
severities.put(MessageId.HTM_059, Severity.ERROR);
146+
severities.put(MessageId.HTM_060a, Severity.USAGE);
147+
severities.put(MessageId.HTM_060b, Severity.USAGE);
145148

146149
// Media
147150
severities.put(MessageId.MED_001, Severity.SUPPRESSED);

src/main/java/com/adobe/epubcheck/messages/MessageId.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ public enum MessageId implements Comparable<MessageId>
136136
HTM_056("HTM_056"),
137137
HTM_057("HTM_057"),
138138
HTM_058("HTM_058"),
139+
HTM_059("HTM_059"),
140+
HTM_060a("HTM_060a"),
141+
HTM_060b("HTM_060b"),
139142

140143
// Messages associated with media (images, audio and video)
141144
MED_001("MED-001"),

src/main/java/com/adobe/epubcheck/ops/OPSHandler30.java

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.adobe.epubcheck.ops;
22

3+
import java.util.Arrays;
34
import java.util.Collections;
45
import java.util.EnumSet;
56
import java.util.HashMap;
@@ -10,6 +11,7 @@
1011
import java.util.Map;
1112
import java.util.Set;
1213
import java.util.TreeSet;
14+
import java.util.stream.Collectors;
1315

1416
import org.w3c.epubcheck.constants.MIMEType;
1517
import org.w3c.epubcheck.core.references.Reference;
@@ -763,17 +765,14 @@ protected void processMeta()
763765
String name = e.getAttribute("name");
764766
if ("viewport".equals(Strings.nullToEmpty(name).trim()))
765767
{
766-
// Mark the viewport as seen
767-
// (used when checking the existence of viewport metadata)
768-
hasViewport = true;
769-
// For a fixed-layout documents:
770-
if (context.opfItem.isPresent() && context.opfItem.get().isFixedLayout())
768+
String content = e.getAttribute("content");
769+
// For fixed-layout documents, check the first viewport meta element
770+
if (!hasViewport && context.opfItem.isPresent() && context.opfItem.get().isFixedLayout())
771771
{
772-
String contentAttribute = e.getAttribute("content");
773-
772+
hasViewport = true;
774773
// parse viewport metadata
775774
List<ViewportMeta.ParseError> syntaxErrors = new LinkedList<>();
776-
ViewportMeta viewport = ViewportMeta.parse(contentAttribute,
775+
ViewportMeta viewport = ViewportMeta.parse(content,
777776
new ViewportMeta.ErrorHandler()
778777
{
779778
@Override
@@ -785,31 +784,46 @@ public void error(ParseError error, int position)
785784
if (!syntaxErrors.isEmpty())
786785
{
787786
// report any syntax error
788-
report.message(MessageId.HTM_047, location(), contentAttribute);
787+
report.message(MessageId.HTM_047, location(), content);
789788
}
790789
else
791790
{
792-
// check that viewport metadata has a valid width value
793-
if (!viewport.hasProperty("width"))
794-
{
795-
report.message(MessageId.HTM_056, location(), "width");
796-
}
797-
else if (!ViewportMeta.isValidWidth(viewport.getValue("width")))
791+
for (String property : Arrays.asList("width", "height"))
798792
{
799-
report.message(MessageId.HTM_057, location(), "width");
793+
// check that viewport metadata has a valid width value
794+
if (!viewport.hasProperty(property))
795+
{
796+
report.message(MessageId.HTM_056, location(), property);
797+
}
798+
else
799+
{
800+
List<String> values = viewport.getValues(property);
801+
if (values.size() > 1)
802+
{
803+
report.message(MessageId.HTM_059, location(), property,
804+
values.stream().map(v -> '"' + v + '"').collect(Collectors.joining(", ")));
805+
}
806+
if (!ViewportMeta.isValidProperty(property, values.get(0)))
807+
{
808+
report.message(MessageId.HTM_057, location(), property);
809+
}
810+
}
800811
}
801812

802-
// check that viewport metadata has a valid height value
803-
if (!viewport.hasProperty("height"))
804-
{
805-
report.message(MessageId.HTM_056, location(), "height");
806-
}
807-
else if (!ViewportMeta.isValidHeight(viewport.getValue("height")))
808-
{
809-
report.message(MessageId.HTM_057, location(), "height");
810-
}
811813
}
812-
814+
}
815+
else
816+
{
817+
// Report ignored secondary viewport meta in fixed-layout documents
818+
if (context.opfItem.isPresent() && context.opfItem.get().isFixedLayout())
819+
{
820+
report.message(MessageId.HTM_060a, location(), content);
821+
}
822+
// Report ignored viewport meta in reflowable documents
823+
else
824+
{
825+
report.message(MessageId.HTM_060b, location(), content);
826+
}
813827
}
814828
}
815829
}

src/main/java/org/w3c/epubcheck/util/microsyntax/ViewportMeta.java

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import static org.w3c.epubcheck.util.infra.InfraUtil.isASCIIWhitespace;
44

55
import java.nio.CharBuffer;
6-
import java.util.Map;
6+
import java.util.List;
77
import java.util.regex.Pattern;
88

99
import com.google.common.base.Preconditions;
10-
import com.google.common.collect.ImmutableMap;
10+
import com.google.common.collect.ImmutableListMultimap;
11+
import com.google.common.collect.ListMultimap;
1112

1213
public class ViewportMeta
1314
{
@@ -19,16 +20,18 @@ public static ViewportMeta parse(String string, ErrorHandler errorHandler)
1920
return new Parser(errorHandler).parse(string);
2021
}
2122

22-
public static boolean isValidHeight(String height)
23+
public static boolean isValidProperty(String name, String value)
2324
{
24-
Preconditions.checkArgument(height != null);
25-
return VIEWPORT_HEIGHT_REGEX.matcher(height).matches();
26-
}
27-
28-
public static boolean isValidWidth(String width)
29-
{
30-
Preconditions.checkArgument(width != null);
31-
return VIEWPORT_WIDTH_REGEX.matcher(width).matches();
25+
Preconditions.checkNotNull(value);
26+
switch (Preconditions.checkNotNull(name))
27+
{
28+
case "width":
29+
return VIEWPORT_WIDTH_REGEX.matcher(value).matches();
30+
case "height":
31+
return VIEWPORT_HEIGHT_REGEX.matcher(value).matches();
32+
default:
33+
return true;
34+
}
3235
}
3336

3437
public static enum ParseError
@@ -51,7 +54,8 @@ public interface ErrorHandler
5154

5255
private final static class Builder
5356
{
54-
public ImmutableMap.Builder<String, String> properties = ImmutableMap.builder();
57+
public ImmutableListMultimap.Builder<String, String> properties = ImmutableListMultimap
58+
.builder();
5559

5660
public ViewportMeta build()
5761
{
@@ -123,40 +127,38 @@ public ViewportMeta parse(CharSequence string)
123127
}
124128
else if (c == '=' || isASCIIWhitespace(c))
125129
{
126-
if (name.length() == 0)
127-
{
128-
error(ParseError.NAME_EMPTY, input.position());
129-
return builder.build();
130-
}
131130
state = State.ASSIGN;
132131
consume = false;
133132
}
134133
else if (c == ',' || c == ';')
135134
{
136-
if (name.length() == 0)
137-
{
138-
error(ParseError.LEADING_SEPARATOR, input.position());
139-
}
140-
else
141-
{
142-
error(ParseError.VALUE_EMPTY, input.position());
143-
}
144-
return builder.build();
135+
state = State.SEPARATOR;
136+
consume = false;
145137
}
146138
else
147139
{
148140
name.append(c);
149141
}
150142
break;
151143
case ASSIGN:
152-
if (isASCIIWhitespace(c))
144+
if (name.length()==0) {
145+
// assign state but no name was found
146+
error(ParseError.NAME_EMPTY, input.position());
147+
return builder.build();
148+
}
149+
else if (isASCIIWhitespace(c))
153150
{
154151
// skip whitespace
155152
}
156153
else if (c == '=')
157154
{
158155
state = State.VALUE;
159156
}
157+
else if (c == ',' || c == ';')
158+
{
159+
state = State.SEPARATOR;
160+
consume = false;
161+
}
160162
else
161163
{
162164
// no '=' was matched (i.e. no value is set)
@@ -200,6 +202,11 @@ else if (c == '=')
200202
consume = false;
201203
}
202204
case SEPARATOR:
205+
if (name.length() == 0)
206+
{
207+
error(ParseError.LEADING_SEPARATOR, input.position());
208+
return builder.build();
209+
}
203210
if (c == ',' || c == ';' || isASCIIWhitespace(c))
204211
{
205212
// skip repeating separators
@@ -215,13 +222,12 @@ else if (c == '=')
215222
break;
216223
}
217224
}
218-
if (value.length() != 0)
219-
{
220-
builder.withProperty(name.toString(), value.toString());
221-
}
222-
else if (name.length() != 0)
225+
// finalize, report if unexpected final state
226+
if (state == State.VALUE && value.length() == 0)
223227
{
224228
error(ParseError.VALUE_EMPTY, input.position());
229+
} else {
230+
builder.withProperty(name.toString(), value.toString());
225231
}
226232
if (state == State.SEPARATOR)
227233
{
@@ -231,7 +237,7 @@ else if (name.length() != 0)
231237
}
232238
}
233239

234-
private final Map<String, String> properties;
240+
private final ImmutableListMultimap<String, String> properties;
235241

236242
private ViewportMeta(Builder builder)
237243
{
@@ -243,9 +249,14 @@ public boolean hasProperty(String name)
243249
return properties.containsKey(name);
244250
}
245251

246-
public String getValue(String name)
252+
public List<String> getValues(String name)
247253
{
248254
return properties.get(name);
249255
}
250256

257+
public ListMultimap<String, String> asMultimap()
258+
{
259+
return properties;
260+
}
261+
251262
}

src/main/resources/com/adobe/epubcheck/messages/MessageBundle.properties

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ HTM_054=Custom attribute namespace ("%1$s") must not include the string "%2$s" i
6060
HTM_055=The "%1$s" element should not be used (discouraged construct)
6161
HTM_056=Viewport metadata has no "%1$s" dimension (both "width" and "height" properties are required)
6262
HTM_057=Viewport "%1$s" value must be a positive number or the keyword "device-%1$s"
63-
HTM_058=HTML documents must be encoded in UTF-8, but UTF-16 was detected.
63+
HTM_058=HTML documents must be encoded in UTF-8, but UTF-16 was detected.
64+
HTM_059=Viewport "%1$s" property must not be defined more than once, but found values [%2$s].
65+
HTM_060a=EPUB reading systems must ignore secondary viewport meta elements in fixed-layout documents; viewport declaration "%1$s" will be ignored.
66+
HTM_060b=EPUB reading systems must ignore viewport meta elements in reflowable documents; viewport declaration "%1$s" will be ignored.
6467

6568
#media
6669
MED_003=Picture "img" elements must reference core media type resources, but found resource "%1$s" of type "%2$s".

src/test/java/org/w3c/epubcheck/util/microsyntax/ViewportSteps.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static org.hamcrest.MatcherAssert.assertThat;
44
import static org.hamcrest.Matchers.contains;
5+
import static org.hamcrest.Matchers.equalTo;
56
import static org.hamcrest.Matchers.empty;
67
import static org.hamcrest.Matchers.is;
78

@@ -12,6 +13,7 @@
1213
import org.w3c.epubcheck.util.microsyntax.ViewportMeta.ParseError;
1314

1415
import com.google.common.collect.ImmutableList;
16+
import com.google.common.collect.ImmutableListMultimap;
1517

1618
import io.cucumber.java.ParameterType;
1719
import io.cucumber.java.en.Then;
@@ -20,6 +22,8 @@
2022
public class ViewportSteps
2123
{
2224

25+
private ViewportMeta viewport;
26+
2327
public static final class TestErrorHandler implements ErrorHandler
2428
{
2529
public final List<ParseError> errors = new LinkedList<>();
@@ -58,7 +62,7 @@ public ParseError error(String error)
5862
@When("parsing viewport {string}")
5963
public void parseViewport(String content)
6064
{
61-
ViewportMeta.parse(content, handler);
65+
viewport = ViewportMeta.parse(content, handler);
6266
}
6367

6468
@Then("no error is returned")
@@ -67,6 +71,12 @@ public void assertValid()
6771
assertThat("Unexpected errors", handler.errors(), is(empty()));
6872
}
6973

74+
@Then("the parsed viewport equals {multimap}")
75+
public void assertResult(ImmutableListMultimap<String, String> multimap)
76+
{
77+
assertThat(viewport.asMultimap(), is(equalTo(multimap)));
78+
}
79+
7080
@Then("error {error} is returned")
7181
public void assertError(ParseError error)
7282
{

src/test/resources/epub3/08-layout/files/content-fxl-xhtml-viewport-multiple-valid/EPUB/content_001.xhtml renamed to src/test/resources/epub3/08-layout/files/content-fxl-xhtml-viewport-duplicate-width-height-error/EPUB/content_001.xhtml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
33
<head>
44
<meta charset="utf-8"/>
5-
<meta name="viewport" content="width=600,height=1200" />
6-
<meta name="viewport" content="width=600,height=1200" />
5+
<meta name="viewport" content="width=600,height=1200,width=device-width,height=device-height" />
76
<title>Minimal EPUB</title>
87
</head>
98
<body>

0 commit comments

Comments
 (0)