Skip to content

Commit

Permalink
fix for #313
Browse files Browse the repository at this point in the history
  • Loading branch information
msangel committed Nov 27, 2024
1 parent e16d5ed commit 4069ad9
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 77 deletions.
5 changes: 5 additions & 0 deletions src/main/java/liqp/TemplateContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import liqp.RenderTransformer.ObjectAppender;
import liqp.exceptions.ExceededMaxIterationsException;
import liqp.filters.date.BasicDateParser;

public class TemplateContext {

Expand Down Expand Up @@ -67,6 +68,10 @@ public TemplateParser getParser() {
return parser;
}

public BasicDateParser getDateParser() {
return parser.getDateParser();
}

public void addError(Exception exception) {
this.errors.add(exception);
}
Expand Down
26 changes: 23 additions & 3 deletions src/main/java/liqp/TemplateParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import liqp.blocks.Block;
import liqp.filters.Filter;
import liqp.filters.Filters;
import liqp.filters.date.BasicDateParser;
import liqp.filters.date.Parser;
import liqp.parser.Flavor;
import liqp.parser.LiquidSupport;
import liqp.tags.Tag;
Expand Down Expand Up @@ -36,7 +38,6 @@ public class TemplateParser {

public static final TemplateParser DEFAULT_JEKYLL = Flavor.JEKYLL.defaultParser();


/**
* Equivalent of
* <code>
Expand Down Expand Up @@ -91,6 +92,7 @@ public enum ErrorMode {
private final int limitMaxSizeRenderedString;
private final long limitMaxRenderTimeMillis;
private final long limitMaxTemplateSizeBytes;
private final BasicDateParser dateParser;

public enum EvaluateMode {
LAZY,
Expand Down Expand Up @@ -158,6 +160,7 @@ public static class Builder {
private Long limitMaxRenderTimeMillis = Long.MAX_VALUE;
private Long limitMaxTemplateSizeBytes = Long.MAX_VALUE;
private NameResolver nameResolver;
private BasicDateParser dateParser;

public Builder() {
}
Expand Down Expand Up @@ -188,6 +191,7 @@ public Builder(TemplateParser parser) {

this.errorMode = parser.errorMode;
this.nameResolver = parser.nameResolver;
this.dateParser = parser.dateParser;
}

@SuppressWarnings("hiding")
Expand Down Expand Up @@ -360,6 +364,11 @@ public Builder withErrorMode(ErrorMode errorMode) {
return this;
}

public Builder withDateParser(BasicDateParser dateParser) {
this.dateParser = dateParser;
return this;
}


@SuppressWarnings("hiding")
public TemplateParser build() {
Expand Down Expand Up @@ -415,8 +424,12 @@ public TemplateParser build() {
nameResolver = new LocalFSNameResolver(snippetsFolderName);
}

if (dateParser == null) {
dateParser = new Parser();
}

return new TemplateParser(strictVariables, showExceptionsFromInclude, evaluateMode, renderTransformer, locale, defaultTimeZone, environmentMapConfigurator, errorMode, fl, stripSpacesAroundTags, stripSingleLine, mapper,
allInsertions, finalFilters, evaluateInOutputTag, strictTypedExpressions, liquidStyleInclude, liquidStyleWhere, nameResolver, limitMaxIterations, limitMaxSizeRenderedString, limitMaxRenderTimeMillis, limitMaxTemplateSizeBytes);
allInsertions, finalFilters, evaluateInOutputTag, strictTypedExpressions, liquidStyleInclude, liquidStyleWhere, nameResolver, limitMaxIterations, limitMaxSizeRenderedString, limitMaxRenderTimeMillis, limitMaxTemplateSizeBytes, dateParser);
}
}

Expand All @@ -428,7 +441,8 @@ public TemplateParser build() {
Consumer<Map<String, Object>> environmentMapConfigurator, ErrorMode errorMode, Flavor flavor, boolean stripSpacesAroundTags, boolean stripSingleLine,
ObjectMapper mapper, Insertions insertions, Filters filters, boolean evaluateInOutputTag,
boolean strictTypedExpressions,
boolean liquidStyleInclude, Boolean liquidStyleWhere, NameResolver nameResolver, int maxIterations, int maxSizeRenderedString, long maxRenderTimeMillis, long maxTemplateSizeBytes) {
boolean liquidStyleInclude, Boolean liquidStyleWhere, NameResolver nameResolver, int maxIterations, int maxSizeRenderedString, long maxRenderTimeMillis, long maxTemplateSizeBytes,
BasicDateParser dateParser) {
this.flavor = flavor;
this.stripSpacesAroundTags = stripSpacesAroundTags;
this.stripSingleLine = stripSingleLine;
Expand All @@ -455,6 +469,7 @@ public TemplateParser build() {
this.limitMaxSizeRenderedString = maxSizeRenderedString;
this.limitMaxRenderTimeMillis = maxRenderTimeMillis;
this.limitMaxTemplateSizeBytes = maxTemplateSizeBytes;
this.dateParser = dateParser;
}

public Template parse(Path path) throws IOException {
Expand Down Expand Up @@ -529,6 +544,11 @@ public Consumer<Map<String, Object>> getEnvironmentMapConfigurator() {
return environmentMapConfigurator;
}


public BasicDateParser getDateParser() {
return dateParser;
}

private Path getLocationFromInputStream(InputStream input) {
try {
if (input instanceof FileInputStream) {
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/liqp/filters/Date.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import liqp.LValue;
import liqp.TemplateContext;
import liqp.filters.date.BasicDateParser;
import liqp.filters.date.CustomDateFormatRegistry;
import liqp.filters.date.CustomDateFormatSupport;
import liqp.filters.date.Parser;
Expand Down Expand Up @@ -46,7 +47,7 @@ public Object apply(Object value, TemplateContext context, Object... params) {
// No need to divide this by 1000, the param is expected to be in seconds already!
compatibleDate = ZonedDateTime.ofInstant(Instant.ofEpochMilli(super.asNumber(value).longValue() * 1000), context.getParser().defaultTimeZone);
} else {
compatibleDate = Parser.parse(valAsString, locale, context.getParser().defaultTimeZone);
compatibleDate = context.getDateParser().parse(valAsString, locale, context.getParser().defaultTimeZone);
}
if (compatibleDate == null) {
return value;
Expand Down
102 changes: 102 additions & 0 deletions src/main/java/liqp/filters/date/BasicDateParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package liqp.filters.date;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.time.temporal.TemporalQueries;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import static java.time.temporal.ChronoField.*;

public abstract class BasicDateParser {

private final List<String> cachedPatterns = new ArrayList<>();

protected BasicDateParser() {

}

protected BasicDateParser(List<String> patterns) {
cachedPatterns.addAll(patterns);
}

protected void storePattern(String pattern) {
cachedPatterns.add(pattern);
}

public abstract ZonedDateTime parse(String valAsString, Locale locale, ZoneId timeZone);

protected ZonedDateTime parseUsingCachedPatterns(String str, Locale locale, ZoneId defaultZone) {
for(String pattern : cachedPatterns) {
try {
TemporalAccessor temporalAccessor = parseUsingPattern(str, pattern, locale);
return getZonedDateTimeFromTemporalAccessor(temporalAccessor, defaultZone);
} catch (Exception e) {
// ignore
}
}
// Could not parse the string into a meaningful date, return null.
return null;
}

protected TemporalAccessor parseUsingPattern(String normalized, String pattern, Locale locale) {
DateTimeFormatter timeFormatter = new DateTimeFormatterBuilder()
.parseCaseInsensitive()
.appendPattern(pattern)
.toFormatter(locale);

return timeFormatter.parse(normalized);
}


/**
* Follow ruby rules: if some datetime part is missing,
* the default is taken from `now` with default zone
*/
public static ZonedDateTime getZonedDateTimeFromTemporalAccessor(TemporalAccessor temporal, ZoneId defaultZone) {
if (temporal == null) {
return ZonedDateTime.now(defaultZone);
}
if (temporal instanceof ZonedDateTime) {
return (ZonedDateTime) temporal;
}
if (temporal instanceof Instant) {
return ZonedDateTime.ofInstant((Instant) temporal, defaultZone);
}

ZoneId zoneId = temporal.query(TemporalQueries.zone());
if (zoneId == null) {
LocalDate date = temporal.query(TemporalQueries.localDate());
LocalTime time = temporal.query(TemporalQueries.localTime());

if (date == null) {
date = LocalDate.now(defaultZone);
}
if (time == null) {
time = LocalTime.now(defaultZone);
}
return ZonedDateTime.of(date, time, defaultZone);
} else {
LocalDateTime now = LocalDateTime.now(zoneId);
TemporalField[] copyThese = new TemporalField[]{
YEAR,
MONTH_OF_YEAR,
DAY_OF_MONTH,
HOUR_OF_DAY,
MINUTE_OF_HOUR,
SECOND_OF_MINUTE,
NANO_OF_SECOND
};
for (TemporalField tf: copyThese) {
if (temporal.isSupported(tf)) {
now = now.with(tf, temporal.get(tf));
}
}
return now.atZone(zoneId);
}
}
}
117 changes: 117 additions & 0 deletions src/main/java/liqp/filters/date/FuzzyDateDateParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package liqp.filters.date;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class FuzzyDateDateParser extends BasicDateParser {

@Override
public ZonedDateTime parse(String valAsString, Locale locale, ZoneId defaultZone) {
String normalized = valAsString.toLowerCase();
ZonedDateTime zonedDateTime = parseUsingCachedPatterns(normalized, locale, defaultZone);
if (zonedDateTime != null) {
return zonedDateTime;
}

List<Part> parts = new ArrayList<>();
// we start as one big single unparsed part
DateParseContext ctx = new DateParseContext();
parts.add(new UnparsedPart(0, normalized.length(), normalized));

while (haveUnparsed(parts)) {
parts = parsePart(parts, ctx);
}

String pattern = reconstructPattern(parts);

TemporalAccessor temporalAccessor = parseUsingPattern(normalized, pattern, locale);
if (temporalAccessor == null) {
return null;
}
storePattern(pattern);
return getZonedDateTimeFromTemporalAccessor(temporalAccessor, defaultZone);
}


private String reconstructPattern(List<Part> parts) {
return null;
}

static class DateParseContext {

}

private List<Part> parsePart(List<Part> parts, DateParseContext ctx) {
return new ArrayList<>();
}

private boolean haveUnparsed(List<Part> parts) {
return parts.stream().anyMatch(p -> p.state() == PartState.UNPARSED);
}

private PartItem getPart(String valAsString) {
return null;
}
enum PartState {
UNPARSED,
PARSED,
KNOWN_CONSTANT,
UNRECOGNIZED
}
interface Part {
int start(); // before symbol
int end(); // after symbol
PartState state();
}

static class UnparsedPart implements Part {
final int start;
final int end;
UnparsedPart(int start, int end, String value) {
this.start = start;
this.end = end;
}
@Override
public int start() {
return start;
}
@Override
public int end() {
return end;
}
@Override
public PartState state() {
return PartState.UNPARSED;
}
}

static class PartItem {
final PartKind kind;
final String pattern;
final int start;
final int end;
PartItem(PartKind kind, String pattern, int start, int end) {
this.kind = kind;
this.pattern = pattern;
this.start = start;
this.end = end;
}
}
enum PartKind {
CONSTANT,
YEAR,
MONTH,
DAY,
HOUR,
MINUTE,
SECOND,
MILLISECOND,
MICROSECOND,
NANOSECOND
}

}
Loading

0 comments on commit 4069ad9

Please sign in to comment.