blob: 079b896d47508c52e8dbf921a4ce0c0852df7a9c [file] [log] [blame] [edit]
package org.unicode.cldr.tool;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.util.VersionInfo;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import org.unicode.cldr.draft.FileUtilities;
import org.unicode.cldr.tool.ToolConstants.ChartStatus;
import org.unicode.cldr.util.CLDRConfig;
import org.unicode.cldr.util.CLDRPaths;
import org.unicode.cldr.util.CldrUtility;
import org.unicode.cldr.util.DtdData;
import org.unicode.cldr.util.DtdData.Attribute;
import org.unicode.cldr.util.DtdData.AttributeStatus;
import org.unicode.cldr.util.DtdData.Element;
import org.unicode.cldr.util.DtdType;
import org.unicode.cldr.util.SupplementalDataInfo;
/**
* Changed ShowDtdDiffs into a chart.
*
* @author markdavis
*/
public class ChartDtdDelta extends Chart {
private static final Splitter SPLITTER_SPACE = Splitter.on(' ');
private static final String NEW_PREFIX = "+";
private static final String DEPRECATED_PREFIX = "⊖";
private static final String UNDEPRECATED_PREFIX = "⊙"; // no occurances yet
private static final String ORDERED_SIGN = "⇣";
private static final String UNORDERED_SIGN = "⇟";
private static final String TECHPREVIEW_SIGN = "🅟";
private static final String UNTECHPREVIEW_SIGN = "ⓟ";
private static final Set<String> OMITTED_ATTRIBUTES = Collections.singleton("⊕");
public static void main(String[] args) {
new ChartDtdDelta().writeChart(null);
}
@Override
public String getDirectory() {
return FormattedFileWriter.CHART_TARGET_DIR;
}
@Override
public String getTitle() {
return "DTD Deltas";
}
@Override
public String getExplanation() {
return "<p>Changes to the LDML DTDs over time.</p>\n"
+ "<ul>\n"
+ "<li>New elements or attributes are indicated with a + sign, and newly deprecated ones with a ⊖ sign.</li>\n"
+ "<li>Element attributes are abbreviated as ⊕ where is no change to them, "
+ "but the element is newly the child of another.</li>\n"
+ "<li>LDML DTDs have augmented data:\n"
+ "<ul><li>Attribute status is marked by: "
+ AttributeStatus.distinguished.shortName
+ "="
+ AttributeStatus.distinguished
+ ", "
+ AttributeStatus.value.shortName
+ "="
+ AttributeStatus.value
+ ", or "
+ AttributeStatus.metadata.shortName
+ "="
+ AttributeStatus.metadata
+ ".</li>\n"
+ "<li>Attribute value constraints are marked with ⟨…⟩ (for DTD constraints) and ⟪…⟫ (for augmented constraints, added in v35.0).</li>\n"
+ "<li>Changes in status or constraints are shown with ➠, with identical sections shown with ….</li>\n"
+ "<li>Newly ordered elements are indicated with "
+ ORDERED_SIGN
+ "; newly unordered with "
+ UNORDERED_SIGN
+ ".</li>\n"
+ "<li>Newly tech-preview items are marked with "
+ TECHPREVIEW_SIGN
+ "; newly graduated from tech preview with "
+ UNTECHPREVIEW_SIGN
+ ".</li>\n"
+ "<li>The following elements are skipped: "
+ SKIP_ELEMENTS
+ " and "
+ SKIP_TYPE_ELEMENTS
+ "</li>\n"
+ "<li>The following attributes are skipped: "
+ SKIP_ATTRIBUTES
+ " and "
+ SKIP_ATTRIBUTE_MATCHES
+ "</li>\n"
+ "</ul></li></ul>\n"
+ "<p>For more information, see the LDML spec.</p>";
}
@Override
public void writeContents(FormattedFileWriter pw) throws IOException {
TablePrinter tablePrinter =
new TablePrinter()
.addColumn(
"Version",
"class='source'",
CldrUtility.getDoubleLinkMsg(),
"class='source'",
true)
.setSortPriority(0)
.setSortAscending(false)
.setBreakSpans(true)
.addColumn("Dtd Type", "class='source'", null, "class='source'", true)
.setSortPriority(1)
.addColumn(
"Intermediate Path", "class='source'", null, "class='target'", true)
.setSortPriority(2)
.addColumn("Element", "class='target'", null, "class='target'", true)
.setSpanRows(false)
.addColumn("Attributes", "class='target'", null, "class='target'", true)
.setSpanRows(false);
String last = null;
for (String current :
ToolConstants.CHART_STATUS != ChartStatus.release
? ToolConstants.CLDR_RELEASE_AND_DEV_VERSION_SET
: ToolConstants.CLDR_RELEASE_VERSION_SET) {
System.out.println("DTD delta: " + current);
final boolean finalVersion = current.equals(ToolConstants.DEV_VERSION);
String currentName = finalVersion ? ToolConstants.CHART_DISPLAY_VERSION : current;
for (DtdType type : TYPES) {
String firstVersion = type.firstVersion; // FIRST_VERSION.get(type);
if (firstVersion != null
&& current != null
&& VersionInfo.getInstance(current)
.compareTo(VersionInfo.getInstance(firstVersion))
< 0) {
// skip if current is too old to have “type”
continue;
}
DtdData dtdCurrent = null;
try {
dtdCurrent =
DtdData.getInstance(
type,
finalVersion
// && ToolConstants.CHART_STATUS !=
// ToolConstants.ChartStatus.release
? null
: current);
} catch (Exception e) {
if (!(e.getCause() instanceof FileNotFoundException)) {
throw e;
}
System.out.println(e.getMessage() + ", " + e.getCause().getMessage());
continue;
}
DtdData dtdLast = null;
if (last != null
&& (firstVersion == null
|| VersionInfo.getInstance(last)
.compareTo(VersionInfo.getInstance(firstVersion))
>= 0)) {
// only read if last isn’t too old to have “type”
dtdLast = DtdData.getInstance(type, last);
}
diff(currentName, dtdLast, dtdCurrent);
}
last = current;
if (current.contentEquals(ToolConstants.CHART_VERSION)) {
break;
}
}
for (DiffElement datum : data) {
tablePrinter
.addRow()
.addCell(datum.getVersionString())
.addCell(datum.dtdType)
.addCell(datum.newPath)
.addCell(datum.newElement)
.addCell(datum.attributeNames)
.finishRow();
}
pw.write(tablePrinter.toTable());
pw.write(Utility.repeat("<br>", 50));
try (PrintWriter tsvFile =
FileUtilities.openUTF8Writer(
CLDRPaths.CHART_DIRECTORY + "/tsv/", "dtd_deltas.tsv")) {
tablePrinter.toTsv(tsvFile);
}
}
static final String NONE = " ";
static final SupplementalDataInfo SDI = CLDRConfig.getInstance().getSupplementalDataInfo();
static Set<DtdType> TYPES = EnumSet.allOf(DtdType.class);
static {
TYPES.remove(DtdType.ldmlICU);
}
static final Map<DtdType, String> FIRST_VERSION = new EnumMap<>(DtdType.class);
static {
FIRST_VERSION.put(DtdType.ldmlBCP47, "1.7.2");
FIRST_VERSION.put(DtdType.keyboard3, "22.1");
}
private void diff(String prefix, DtdData dtdLast, DtdData dtdCurrent) {
Map<String, Element> oldNameToElement =
dtdLast == null ? Collections.emptyMap() : dtdLast.getElementFromName();
checkNames(
prefix,
dtdCurrent,
dtdLast,
oldNameToElement,
"/",
dtdCurrent.ROOT,
new HashSet<Element>(),
false);
}
static final DtdType DEBUG_DTD = null; // set to enable
static final String DEBUG_ELEMENT = "lias";
static final boolean SHOW = false;
@SuppressWarnings("unused")
private void checkNames(
String version,
DtdData dtdCurrent,
DtdData dtdLast,
Map<String, Element> oldNameToElement,
String path,
Element element,
HashSet<Element> seen,
boolean showAnyway) {
String name = element.getName();
if (SKIP_ELEMENTS.contains(name)) {
return;
}
if (SKIP_TYPE_ELEMENTS.containsEntry(dtdCurrent.dtdType, name)) {
return;
}
String newPath = path + "/" + element.name;
// if an element is newly a child of another but has already been seen, you'll have special
// indication
if (seen.contains(element)) {
if (showAnyway) {
addData(dtdCurrent, NEW_PREFIX + name, version, newPath, OMITTED_ATTRIBUTES);
}
return;
}
seen.add(element);
if (SHOW && ToolConstants.CHART_DISPLAY_VERSION.equals(version)) {
System.out.println(dtdCurrent.dtdType + "\t" + name);
}
if (DEBUG_DTD == dtdCurrent.dtdType && name.contains(DEBUG_ELEMENT)) {
int debug = 0;
}
Element oldElement = null;
boolean ordered = element.isOrdered();
boolean currentTechPreview = element.isTechPreview();
if (!oldNameToElement.containsKey(name)) {
Set<String> attributeNames =
getAttributeNames(
dtdCurrent,
dtdLast,
name,
Collections.emptyMap(),
element.getAttributes());
final String prefix = NEW_PREFIX + (currentTechPreview ? TECHPREVIEW_SIGN : "");
addData(
dtdCurrent,
prefix + name + (ordered ? ORDERED_SIGN : ""),
version,
newPath,
attributeNames);
} else {
oldElement = oldNameToElement.get(name);
boolean oldOrdered = oldElement.isOrdered();
Set<String> attributeNames =
getAttributeNames(
dtdCurrent,
dtdLast,
name,
oldElement.getAttributes(),
element.getAttributes());
boolean currentDeprecated = element.isDeprecated();
boolean lastDeprecated =
dtdLast == null
? false
: oldElement.isDeprecated(); // + (currentDeprecated ? "ⓓ" : "")
boolean lastTechPreview =
dtdLast == null
? false
: oldElement.isTechPreview(); // + (currentDeprecated ? "ⓓ" : "")
String deprecatedStatus =
currentDeprecated == lastDeprecated
? ""
: currentDeprecated ? DEPRECATED_PREFIX : UNDEPRECATED_PREFIX;
String orderingStatus =
(ordered == oldOrdered || currentDeprecated)
? ""
: ordered ? ORDERED_SIGN : UNORDERED_SIGN;
String previewStatus =
(currentTechPreview == lastTechPreview || currentDeprecated)
? ""
: currentTechPreview ? TECHPREVIEW_SIGN : UNTECHPREVIEW_SIGN;
if (!orderingStatus.isEmpty()
|| !previewStatus.isEmpty()
|| !deprecatedStatus.isEmpty()
|| !attributeNames.isEmpty()) {
addData(
dtdCurrent,
deprecatedStatus + previewStatus + name + orderingStatus,
version,
newPath,
attributeNames);
}
}
if (element.getName().equals("coordinateUnit")) {
System.out.println(version + "\toordinateUnit\t" + element.getChildren().keySet());
}
Set<Element> oldChildren =
oldElement == null ? Collections.emptySet() : oldElement.getChildren().keySet();
for (Element child : element.getChildren().keySet()) {
showAnyway = true;
for (Element oldChild : oldChildren) {
if (oldChild.getName().equals(child.getName())) {
showAnyway = false;
break;
}
}
checkNames(
version,
dtdCurrent,
dtdLast,
oldNameToElement,
newPath,
child,
seen,
showAnyway);
}
}
enum DiffType {
Element,
Attribute,
AttributeValue
}
private static class DiffElement {
private static final String START_ATTR = "<div>";
private static final String END_ATTR = "</div>";
final VersionInfo version;
final DtdType dtdType;
final boolean isBeta;
final String newPath;
final String newElement;
final String attributeNames;
public DiffElement(
DtdData dtdCurrent,
String version,
String newPath,
String newElement,
Set<String> attributeNames2) {
isBeta = version.endsWith("β");
try {
this.version =
isBeta
? VersionInfo.getInstance(
version.substring(0, version.length() - 1))
: VersionInfo.getInstance(version);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
dtdType = dtdCurrent.dtdType;
this.newPath = fix(newPath);
this.attributeNames =
attributeNames2.isEmpty()
? NONE
: START_ATTR
+ Joiner.on(END_ATTR + START_ATTR).join(attributeNames2)
+ END_ATTR;
this.newElement = newElement;
}
private String fix(String substring) {
int base = substring.indexOf('/', 2);
if (base < 0) return "";
int last = substring.lastIndexOf('/');
if (last <= base) return "/";
substring = substring.substring(base, last);
return substring.replace("/", "\u200B/") + "/";
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("version", getVersionString())
.add("dtdType", dtdType)
.add("newPath", newPath)
.add("newElement", newElement)
.add("attributeNames", attributeNames)
.toString();
}
private String getVersionString() {
return version.getVersionString(2, 4) + (isBeta ? "β" : "");
}
}
List<DiffElement> data = new ArrayList<>();
private void addData(
DtdData dtdCurrent,
String element,
String prefix,
String newPath,
Set<String> attributeNames) {
DiffElement item = new DiffElement(dtdCurrent, prefix, newPath, element, attributeNames);
data.add(item);
}
static final Set<String> SKIP_ELEMENTS =
ImmutableSet.of("generation", "identity", "special"); // , "telephoneCodeData"
static final Multimap<DtdType, String> SKIP_TYPE_ELEMENTS =
ImmutableMultimap.of(DtdType.ldml, "alias");
static final Set<String> SKIP_ATTRIBUTES = ImmutableSet.of("references", "standard", "draft");
static final Multimap<String, String> SKIP_ATTRIBUTE_MATCHES =
ImmutableMultimap.of("alt", "", "alt", "⟪literal/variant⟫");
private static Set<String> getAttributeNames(
DtdData dtdCurrent,
DtdData dtdLast,
String elementName,
Map<Attribute, Integer> attributesOld,
Map<Attribute, Integer> attributes) {
Set<String> names = new LinkedHashSet<>();
if (elementName.equals("coordinateUnit")) {
int debug = 0;
}
main:
// we want to add a name that is new or that becomes deprecated
for (Attribute attribute : attributes.keySet()) {
String name = attribute.getName();
if (SKIP_ATTRIBUTES.contains(name)) {
continue;
}
String match = attribute.getMatchString();
AttributeStatus status = attribute.attributeStatus;
String display = NEW_PREFIX + name;
// if (isDeprecated(dtdCurrent, elementName, name)) { //
// SDI.isDeprecated(dtdCurrent, elementName, name, "*")) {
// continue;
// }
String oldMatch = "?";
String pre, post;
Attribute attributeOld = attribute.getMatchingName(attributesOld);
if (attributeOld == null) {
if (SKIP_ATTRIBUTE_MATCHES.containsEntry(name, match)) {
continue main;
}
display =
NEW_PREFIX
+ name
+ " "
+ AttributeStatus.getShortName(status)
+ " "
+ match;
} else if (attribute.isDeprecated() && !attributeOld.isDeprecated()) {
display = DEPRECATED_PREFIX + name;
} else {
oldMatch = attributeOld.getMatchString();
AttributeStatus oldStatus = attributeOld.attributeStatus;
boolean matchEquals = match.equals(oldMatch);
if (status != oldStatus) {
pre = AttributeStatus.getShortName(oldStatus);
post = AttributeStatus.getShortName(status);
if (!matchEquals) {
pre += " " + oldMatch;
post += " " + match;
}
} else if (!matchEquals) {
if (oldMatch.isEmpty() && SKIP_ATTRIBUTE_MATCHES.containsEntry(name, match)) {
continue main;
}
pre = oldMatch;
post = match;
} else {
continue main; // skip attribute entirely;
}
display = name + " " + diff(pre, post);
}
names.add(display);
}
return names;
}
public static String diff(String pre, String post) {
Matcher matcherPre = Attribute.LEAD_TRAIL.matcher(pre);
Matcher matcherPost = Attribute.LEAD_TRAIL.matcher(post);
if (matcherPre.matches() && matcherPost.matches()) {
List<String> preParts = SPLITTER_SPACE.splitToList(matcherPre.group(2));
List<String> postParts = SPLITTER_SPACE.splitToList(matcherPost.group(2));
pre = matcherPre.group(1) + remove(preParts, postParts) + matcherPre.group(3);
post = matcherPost.group(1) + remove(postParts, preParts) + matcherPost.group(3);
}
return pre + "➠" + post;
}
private static String remove(List<String> main, List<String> toRemove) {
List<String> result = new ArrayList<>();
boolean removed = false;
for (String s : main) {
if (toRemove.contains(s)) {
removed = true;
} else {
if (removed) {
result.add("…");
removed = false;
}
result.add(s);
}
}
if (removed) {
result.add("…");
}
return Joiner.on(" ").join(result);
}
// private static boolean isDeprecated(DtdData dtdCurrent, String elementName, String
// attributeName) {
// try {
// return dtdCurrent.isDeprecated(elementName, attributeName, "*");
// } catch (DtdData.IllegalByDtdException e) {
// return true;
// }
// }
}