diff --git a/eo-parser/src/main/java/org/eolang/parser/StUnhex.java b/eo-parser/src/main/java/org/eolang/parser/StUnhex.java index 78c1e55410..3e7fbe0ae5 100644 --- a/eo-parser/src/main/java/org/eolang/parser/StUnhex.java +++ b/eo-parser/src/main/java/org/eolang/parser/StUnhex.java @@ -24,65 +24,117 @@ package org.eolang.parser; import com.yegor256.xsline.Shift; -import com.yegor256.xsline.StEndless; +import com.yegor256.xsline.StClasspath; import com.yegor256.xsline.StEnvelope; import com.yegor256.xsline.StSequence; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import org.apache.commons.text.StringEscapeUtils; +import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.xembly.Directive; import org.xembly.Directives; /** - * This {@link Shift} turns hex data inside XMIR + * This {@link Shift} turns hex data inside XMIR. * into EO-printable data. * * @since 0.29.0 */ final class StUnhex extends StEnvelope { + /** + * Unexing via {@link com.github.lombrozo.xnav.Xnav}. + */ + static final Shift XNAV = new StSequence( + StUnhex.class.getSimpleName(), + new StXnav( + StUnhex.elements("number"), + xnav -> { + final double number = StUnhex.buffer( + StUnhex.undash(xnav.element("o").text().orElse("")) + ).getDouble(); + final Node node = xnav.node(); + if (Double.isNaN(number) || Double.isInfinite(number)) { + ((Element) node).setAttribute("skip", ""); + } else { + node.setTextContent(StUnhex.number(number)); + } + } + ), + new StXnav( + StUnhex.elements("string"), + xnav -> xnav.node().setTextContent( + String.format( + "\"%s\"", + StringEscapeUtils.escapeJava( + new String( + StUnhex.buffer( + StUnhex.undash(xnav.element("o").text().orElse("")) + ).array(), + StandardCharsets.UTF_8 + ) + ) + ) + ) + ) + ); /** - * Ctor. + * Unhexing via {@link com.jcabi.xml.XMLDocument#xpath(String)}. */ - StUnhex() { - super( - new StEndless( - new StSequence( - StUnhex.class.getSimpleName(), - new StXPath( - StUnhex.xpath("number"), - xml -> { - final double number = StUnhex.buffer( + static final Shift XPATH = new StSequence( + StUnhex.class.getSimpleName(), + new StXPath( + StUnhex.elements("number"), + xml -> { + final double number = StUnhex.buffer( + StUnhex.undash(xml.xpath("./o/text()").get(0)) + ).getDouble(); + final Iterable dirs; + if (Double.isNaN(number) || Double.isInfinite(number)) { + dirs = new Directives().attr("skip", ""); + } else { + dirs = new Directives().set(StUnhex.number(number)); + } + return dirs; + } + ), + new StXPath( + StUnhex.elements("string"), + xml -> new Directives().set( + String.format( + "\"%s\"", + StringEscapeUtils.escapeJava( + new String( + StUnhex.buffer( StUnhex.undash(xml.xpath("./o/text()").get(0)) - ).getDouble(); - final Iterable dirs; - if (Double.isNaN(number) || Double.isInfinite(number)) { - dirs = new Directives().attr("skip", ""); - } else { - dirs = StUnhex.append(StUnhex.number(number)); - } - return dirs; - } - ), - new StXPath( - StUnhex.xpath("string"), - xml -> StUnhex.append( - String.format( - "\"%s\"", - StringEscapeUtils.escapeJava( - new String( - StUnhex.buffer( - StUnhex.undash(xml.xpath("./o/text()").get(0)) - ).array(), - StandardCharsets.UTF_8 - ) - ) - ) + ).array(), + StandardCharsets.UTF_8 ) ) ) ) - ); + ) + ); + + /** + * Unhexing via XSL. + */ + static final Shift XSL = new StClasspath("/org/eolang/parser/print/unhex-data.xsl"); + + /** + * Ctor. + */ + StUnhex() { + this(StUnhex.XNAV); + } + + /** + * Base ctor. + * @param origin Original shift + */ + StUnhex(final Shift origin) { + super(origin); } /** @@ -142,23 +194,14 @@ private static String undash(final String txt) { } /** - * Make XPath. + * Find elements by XPath for given type. * @param type The type to match * @return XPath */ - private static String xpath(final String type) { + private static String elements(final String type) { return String.format( - "(//o[@base='Q.org.eolang.%1$s' and(not(@skip)) and o[1][@base='Q.org.eolang.bytes' and not(o) and string-length(normalize-space(text()))>0]])[1]", + "//o[@base='Q.org.eolang.%1$s' and(not(@skip)) and o[1][@base='Q.org.eolang.bytes' and not(o) and string-length(normalize-space(text()))>0]]", type ); } - - /** - * Append Xemply instructions. - * @param after Value after - * @return Dirs - */ - private static Iterable append(final String after) { - return new Directives().set(after); - } } diff --git a/eo-parser/src/main/java/org/eolang/parser/StXPath.java b/eo-parser/src/main/java/org/eolang/parser/StXPath.java index adfa6111b9..516e50863b 100644 --- a/eo-parser/src/main/java/org/eolang/parser/StXPath.java +++ b/eo-parser/src/main/java/org/eolang/parser/StXPath.java @@ -39,6 +39,10 @@ * @since 0.29.0 */ public final class StXPath implements Shift { + /** + * Xpath for finding first element. + */ + private static final String INDEXED = "(%s)[1]"; /** * XPath to search for. @@ -68,22 +72,21 @@ public String uid() { @Override public XML apply(final int position, final XML xml) { final List nodes = xml.nodes(this.xpath); - if (nodes.size() > 1) { - throw new IllegalArgumentException( - String.format( - "XPath '%s' returned too many elements (%d)", - this.xpath, nodes.size() - ) + final XML doc; + if (nodes.isEmpty()) { + doc = xml; + } else { + final Directives dirs = new Directives(); + for (final XML node : nodes) { + dirs.xpath(String.format(StXPath.INDEXED, this.xpath)) + .strict(1) + .append(this.fun.apply(node)); + } + doc = new XMLDocument( + new Xembler(dirs).applyQuietly(xml.inner()) ); } - final Directives dirs = new Directives(); - if (!nodes.isEmpty()) { - dirs.xpath(this.xpath); - dirs.append(this.fun.apply(nodes.get(0))); - } - return new XMLDocument( - new Xembler(dirs).applyQuietly(xml.deepCopy()) - ); + return doc; } } diff --git a/eo-parser/src/main/java/org/eolang/parser/StXnav.java b/eo-parser/src/main/java/org/eolang/parser/StXnav.java new file mode 100644 index 0000000000..971f373594 --- /dev/null +++ b/eo-parser/src/main/java/org/eolang/parser/StXnav.java @@ -0,0 +1,71 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2025 Objectionary.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.eolang.parser; + +import com.github.lombrozo.xnav.Xnav; +import com.jcabi.xml.XML; +import com.jcabi.xml.XMLDocument; +import com.yegor256.xsline.Shift; +import java.util.function.Consumer; +import org.w3c.dom.Node; + +/** + * This {@link Shift} finds all XPath matches and replaces them + * with what a function suggests using {@link com.github.lombrozo.xnav.Xnav}. + * + * @since 0.53.0 + */ +public final class StXnav implements Shift { + /** + * XPath to search for. + */ + private final String xpath; + + /** + * The mapping function. + */ + private final Consumer fun; + + /** + * Ctor. + * @param path The XPath + * @param func The function + */ + public StXnav(final String path, final Consumer func) { + this.xpath = path; + this.fun = func; + } + + @Override + public String uid() { + return this.getClass().getSimpleName(); + } + + @Override + public XML apply(final int position, final XML xml) { + final Node dom = xml.inner(); + new Xnav(dom).path(this.xpath).forEach(this.fun); + return new XMLDocument(dom); + } +} diff --git a/eo-parser/src/main/java/org/eolang/parser/Xmir.java b/eo-parser/src/main/java/org/eolang/parser/Xmir.java index 44f12149fc..67bdfa1413 100644 --- a/eo-parser/src/main/java/org/eolang/parser/Xmir.java +++ b/eo-parser/src/main/java/org/eolang/parser/Xmir.java @@ -31,8 +31,6 @@ import com.yegor256.xsline.StEndless; import com.yegor256.xsline.TrClasspath; import com.yegor256.xsline.TrDefault; -import com.yegor256.xsline.TrJoined; -import com.yegor256.xsline.Train; import com.yegor256.xsline.Xsline; import java.util.Collection; import java.util.List; @@ -58,22 +56,40 @@ */ @SuppressWarnings("PMD.TooManyMethods") public final class Xmir implements XML { + /** + * Unhex transformation. + */ + private static final Shift UNHEX = new StUnhex(); + /** * Train of transformations that prepare XMIR for conversion to EO. */ - private static final Train FOR_EO = new TrFull( - new TrJoined<>( + private static final Xsline FOR_EO = new Xsline( + new TrFull( new TrDefault<>( new StEndless( new StClasspath("/org/eolang/parser/print/tuples-to-stars.xsl") + ), + new StClasspath("/org/eolang/parser/print/dataized-to-const.xsl"), + Xmir.UNHEX, + new StClasspath("/org/eolang/parser/print/wrap-data.xsl"), + new StClasspath("/org/eolang/parser/print/to-eo.xsl") + ) + ) + ); + + /** + * Train of transformations that prepare XMIR for conversion to EO. + */ + private static final Xsline FOR_PHI = new Xsline( + new TrFull( + new TrDefault<>( + Xmir.UNHEX, + new StClasspath( + "/org/eolang/parser/phi/to-phi.xsl", + String.format("conservative %b", false) ) - ), - new TrClasspath<>( - "/org/eolang/parser/print/dataized-to-const.xsl", - "/org/eolang/parser/print/unhex-data.xsl", - "/org/eolang/parser/print/wrap-data.xsl", - "/org/eolang/parser/print/to-eo.xsl" - ).back() + ) ) ); @@ -154,7 +170,7 @@ public String toEO() { * @return PHI representation as {@link String} */ public String toPhi() { - return this.toPhi(false); + return this.converted(Xmir.FOR_PHI, "phi"); } /** @@ -164,12 +180,14 @@ public String toPhi() { */ public String toPhi(final boolean conservative) { return this.converted( - new TrFull( - new TrDefault<>( - new StClasspath("/org/eolang/parser/print/unhex-data.xsl"), - new StClasspath( - "/org/eolang/parser/phi/to-phi.xsl", - String.format("conservative %b", conservative) + new Xsline( + new TrFull( + new TrDefault<>( + Xmir.UNHEX, + new StClasspath( + "/org/eolang/parser/phi/to-phi.xsl", + String.format("conservative %b", conservative) + ) ) ) ), @@ -183,8 +201,10 @@ public String toPhi(final boolean conservative) { */ public String toSaltyPhi() { return this.converted( - new TrFull( - new TrClasspath<>("/org/eolang/parser/phi/to-salty-phi.xsl").back() + new Xsline( + new TrFull( + new TrClasspath<>("/org/eolang/parser/phi/to-salty-phi.xsl").back() + ) ), "phi" ); @@ -196,8 +216,8 @@ public String toSaltyPhi() { * @param node XML node name * @return XMIR in other representation as {@link String}. */ - private String converted(final Train train, final String node) { - final XML xmir = new Xsline(train).pass(this.xml); + private String converted(final Xsline train, final String node) { + final XML xmir = train.pass(this.xml); Logger.debug(this, "XMIR after converting to %s:\n%s", node, xmir); return new Xnav(xmir.inner()) .element("program") diff --git a/eo-parser/src/main/resources/org/eolang/parser/print/unhex-data.xsl b/eo-parser/src/main/resources/org/eolang/parser/print/unhex-data.xsl index dca8ec8ee7..44a842fa6a 100644 --- a/eo-parser/src/main/resources/org/eolang/parser/print/unhex-data.xsl +++ b/eo-parser/src/main/resources/org/eolang/parser/print/unhex-data.xsl @@ -39,7 +39,7 @@ SOFTWARE. - + diff --git a/eo-parser/src/test/java/org/eolang/parser/StUnhexTest.java b/eo-parser/src/test/java/org/eolang/parser/StUnhexTest.java index 90d8b21dfa..6c009ac4a1 100644 --- a/eo-parser/src/test/java/org/eolang/parser/StUnhexTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/StUnhexTest.java @@ -25,9 +25,14 @@ import com.jcabi.matchers.XhtmlMatchers; import com.jcabi.xml.XMLDocument; +import com.yegor256.xsline.Shift; import com.yegor256.xsline.Xsline; +import java.util.stream.Stream; import org.hamcrest.MatcherAssert; -import org.junit.jupiter.api.Test; +import org.hamcrest.Matchers; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; /** * Test case for {@link StUnhex}. @@ -36,39 +41,45 @@ */ final class StUnhexTest { - @Test - void convertsIntFromHexToEo() { + @ParameterizedTest + @MethodSource("shifts") + void convertsIntFromHexToEo(final Shift shift) { MatcherAssert.assertThat( EoIndentLexerTest.TO_ADD_MESSAGE, - new Xsline(new StUnhex()).pass( + new Xsline(new StUnhex(shift)).pass( new XMLDocument( "

43-70-2E-4F-30-46-73-2E

" ) ), - XhtmlMatchers.hasXPaths("//o[text()='72872276393407200']") + Matchers.anyOf( + XhtmlMatchers.hasXPath("//o[text()='72872276393407200']"), + XhtmlMatchers.hasXPath("//o[text()='7.28722763934072e16']") + ) ); } - @Test - void convertsMaxIntFromHexToEo() { + @ParameterizedTest + @MethodSource("shifts") + void convertsMaxIntFromHexToEo(final Shift shift) { MatcherAssert.assertThat( EoIndentLexerTest.TO_ADD_MESSAGE, - new Xsline(new StUnhex()).pass( + new Xsline(new StUnhex(shift)).pass( new XMLDocument( "

FF-FF-FF-FF-FF-FF-FF-FF

" ) ), - XhtmlMatchers.hasXPaths( - "//o[@base='Q.org.eolang.number' and @skip and o[@base='Q.org.eolang.bytes' and text()!='']]" + XhtmlMatchers.hasXPath( + "//o[@base='Q.org.eolang.number' and o[@base='Q.org.eolang.bytes' and text()!='']]" ) ); } - @Test - void convertsStringFromHexToEo() { + @ParameterizedTest + @MethodSource("shifts") + void convertsStringFromHexToEo(final Shift shift) { MatcherAssert.assertThat( "String bytes must be converted to human readable string, but they didn't", - new Xsline(new StUnhex()).pass( + new Xsline(new StUnhex(shift)).pass( new XMLDocument( String.join( "", @@ -84,61 +95,71 @@ void convertsStringFromHexToEo() { ); } - @Test - void convertsEmptyStringFromHexToEo() { + @ParameterizedTest + @MethodSource("shifts") + void convertsEmptyStringFromHexToEo(final Shift shift) { MatcherAssert.assertThat( EoIndentLexerTest.TO_ADD_MESSAGE, - new Xsline(new StUnhex()).pass( + new Xsline(new StUnhex(shift)).pass( new XMLDocument( "

" ) ), - XhtmlMatchers.hasXPaths( - "//o[empty(text())]" - ) + XhtmlMatchers.hasXPath("//o[empty(text())]") ); } - @Test - void convertsStringWithDoubleSpacesFromHexToEo() { + @ParameterizedTest + @MethodSource("shifts") + void convertsStringWithDoubleSpacesFromHexToEo(final Shift shift) { MatcherAssert.assertThat( EoIndentLexerTest.TO_ADD_MESSAGE, - new Xsline(new StUnhex()).pass( + new Xsline(new StUnhex(shift)).pass( new XMLDocument( "7A-0A-20-20-79-0A-20-78" ) ), - XhtmlMatchers.hasXPaths( - "//o[text()='\"z\\n y\\n x\"']" - ) + XhtmlMatchers.hasXPath("//o[text()='\"z\\n y\\n x\"']") ); } - @Test - void convertsNegativeZeroFromHexToEo() { + @ParameterizedTest + @MethodSource("shifts") + void convertsNegativeZeroFromHexToEo(final Shift shift) { MatcherAssert.assertThat( EoIndentLexerTest.TO_ADD_MESSAGE, - new Xsline(new StUnhex()).pass( + new Xsline(new StUnhex(shift)).pass( new XMLDocument( "80-00-00-00-00-00-00-00" ) ), - XhtmlMatchers.hasXPaths("//o[text()='-0']") + XhtmlMatchers.hasXPath("//o[text()='-0']") ); } - @Test - void convertsFloatFromHexToEo() { + @ParameterizedTest + @MethodSource("shifts") + void convertsFloatFromHexToEo(final Shift shift) { MatcherAssert.assertThat( EoIndentLexerTest.TO_ADD_MESSAGE, - new Xsline(new StUnhex()).pass( + new Xsline(new StUnhex(shift)).pass( new XMLDocument( "

41-42-43-67-AE-CD-3E-FD

" ) ), - XhtmlMatchers.hasXPaths( - "//o[text()='2393807.3656386123']" + Matchers.anyOf( + XhtmlMatchers.hasXPath("//o[text()='2393807.3656386123']"), + XhtmlMatchers.hasXPath("//o[text()='2.3938073656386123e6']") ) ); } + + @SuppressWarnings("PMD.UnusedPrivateMethod") + private static Stream shifts() { + return Stream.of( + Arguments.of(StUnhex.XNAV), + Arguments.of(StUnhex.XPATH), + Arguments.of(StUnhex.XSL) + ); + } } diff --git a/eo-parser/src/test/java/org/eolang/parser/StXPathTest.java b/eo-parser/src/test/java/org/eolang/parser/StXPathTest.java index 5dcf18fdc3..f43d145ce2 100644 --- a/eo-parser/src/test/java/org/eolang/parser/StXPathTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/StXPathTest.java @@ -45,7 +45,7 @@ void modifiesSimpleNode() { new Xsline( new StEndless( new StXPath( - "(//x[@a and not(@b)])[1]", + "//x[@a and not(@b)]", xml -> new Directives().attr( "b", xml.xpath("text()").get(0) ) diff --git a/eo-parser/src/test/resources/org/eolang/parser/phi-packs/numbers-with-exponent.yaml b/eo-parser/src/test/resources/org/eolang/parser/phi-packs/numbers-with-exponent.yaml index a93c2202b4..c76e53b632 100644 --- a/eo-parser/src/test/resources/org/eolang/parser/phi-packs/numbers-with-exponent.yaml +++ b/eo-parser/src/test/resources/org/eolang/parser/phi-packs/numbers-with-exponent.yaml @@ -27,7 +27,7 @@ input: | sweet: |- {⟦ foo ↦ ⟦ - x ↦ 4.294967295e9 + x ↦ 4294967295 ⟧ ⟧} salty: |-