Skip to content

Commit 6ef22c8

Browse files
committed
Fix percent processing (again)
1 parent 7610035 commit 6ef22c8

File tree

8 files changed

+315
-76
lines changed

8 files changed

+315
-76
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,7 @@ gradle-app.setting
124124
### Gradle Patch ###
125125
**/build/
126126

127-
# End of https://www.gitignore.io/api/java,gradle,intellij+iml
127+
# End of https://www.gitignore.io/api/java,gradle,intellij+iml
128+
129+
# Output directory for tests
130+
/output

CHANGELOG.md

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- No security issues fixed!
2121
-->
2222

23-
## [Unreleased]
24-
### Added
25-
- No new features!
26-
### Changed
27-
- No changed features!
28-
### Deprecated
29-
- No deprecated features!
30-
### Removed
31-
- No removed features!
23+
## [1.4.1] - 2020-12-28
3224
### Fixed
33-
- No fixed issues!
34-
### Security
35-
- No security issues fixed!
25+
- Fix percent symbols not being properly escaped (again) by processing the XML file line by line.
3626

3727
## [1.4.0] - 2020-12-25
3828
### Added
@@ -233,7 +223,8 @@ res_dir_path -> resDirPath
233223
### Added
234224
- Initial release.
235225

236-
[Unreleased]: https://github.com/bq/poeditor-android-gradle-plugin/compare/1.4.0...HEAD
226+
[Unreleased]: https://github.com/bq/poeditor-android-gradle-plugin/compare/1.4.1...HEAD
227+
[1.4.1]: https://github.com/bq/poeditor-android-gradle-plugin/compare/1.4.0...1.4.1
237228
[1.4.0]: https://github.com/bq/poeditor-android-gradle-plugin/compare/1.3.1...1.4.0
238229
[1.3.1]: https://github.com/bq/poeditor-android-gradle-plugin/compare/1.3.0...1.3.1
239230
[1.3.0]: https://github.com/bq/poeditor-android-gradle-plugin/compare/1.2.0...1.3.0

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ buildscript {
1818
maven { url 'https://jitpack.io' }
1919
}
2020
dependencies {
21-
classpath 'com.github.bq:poeditor-android-gradle-plugin:1.4.0'
21+
classpath 'com.github.bq:poeditor-android-gradle-plugin:1.4.1'
2222
}
2323
}
2424
```
@@ -33,7 +33,7 @@ buildscript {
3333
maven("https://jitpack.io")
3434
}
3535
dependencies {
36-
classpath("com.github.bq:poeditor-android-gradle-plugin:1.4.0")
36+
classpath("com.github.bq:poeditor-android-gradle-plugin:1.4.1")
3737
}
3838
}
3939
```

src/main/kotlin/com/bq/poeditor/gradle/Main.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ package com.bq.poeditor.gradle
2323
fun main() {
2424
val apiToken = "your_api_token"
2525
val projectId = 1234567890
26-
val resDirPath = "your_res_dir_path"
26+
val resDirPath = "output"
2727
val defaultLanguage = "en"
2828

2929
PoEditorStringsImporter.importPoEditorStrings(apiToken, projectId, defaultLanguage, resDirPath)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2020 BQ
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.bq.poeditor.gradle.ktx
18+
19+
import org.w3c.dom.Document
20+
import org.w3c.dom.bootstrap.DOMImplementationRegistry
21+
import org.w3c.dom.ls.DOMImplementationLS
22+
import java.io.StringWriter
23+
import java.lang.Boolean
24+
import javax.xml.parsers.DocumentBuilderFactory
25+
26+
private val DEFAULT_ENCODING = Charsets.UTF_8
27+
28+
/**
29+
* Converts an XML string to a proper [Document].
30+
*/
31+
fun String.toDocument(): Document =
32+
DocumentBuilderFactory.newInstance()
33+
.newDocumentBuilder()
34+
.parse(this.byteInputStream(DEFAULT_ENCODING))
35+
36+
/**
37+
* Convers a [Document] into a formatted [String].
38+
*/
39+
fun Document.dumpToString(): String {
40+
val registry = DOMImplementationRegistry.newInstance()
41+
val impl = registry.getDOMImplementation("LS") as DOMImplementationLS
42+
val output = impl.createLSOutput().apply { encoding = "UTF-8" }
43+
val serializer = impl.createLSSerializer()
44+
45+
val writer = StringWriter()
46+
output.characterStream = writer
47+
48+
serializer.domConfig.setParameter("format-pretty-print",
49+
Boolean.TRUE)
50+
serializer.domConfig.setParameter("xml-declaration", true)
51+
52+
serializer.write(this, output)
53+
54+
return writer.toString()
55+
}

src/main/kotlin/com/bq/poeditor/gradle/xml/AndroidXmlWriter.kt

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@
1616

1717
package com.bq.poeditor.gradle.xml
1818

19+
import com.bq.poeditor.gradle.ktx.dumpToString
1920
import com.bq.poeditor.gradle.utils.TABLET_REGEX_STRING
2021
import com.bq.poeditor.gradle.utils.TABLET_RES_FOLDER_SUFFIX
2122
import com.bq.poeditor.gradle.utils.logger
2223
import com.bq.poeditor.gradle.utils.createValuesModifierFromLangCode
2324
import org.w3c.dom.Document
24-
import org.w3c.dom.bootstrap.DOMImplementationRegistry
25-
import org.w3c.dom.ls.DOMImplementationLS
2625
import java.io.File
27-
import java.io.StringWriter
2826
import java.lang.IllegalStateException
2927

3028
/**
@@ -70,19 +68,6 @@ class AndroidXmlWriter {
7068
}
7169
}
7270

73-
val registry = DOMImplementationRegistry.newInstance()
74-
val impl = registry.getDOMImplementation("LS") as DOMImplementationLS
75-
val output = impl.createLSOutput().apply { encoding = "UTF-8" }
76-
val serializer = impl.createLSSerializer()
77-
78-
val writer = StringWriter()
79-
output.characterStream = writer
80-
81-
serializer.domConfig.setParameter("format-pretty-print",
82-
java.lang.Boolean.TRUE)
83-
serializer.domConfig.setParameter("xml-declaration", true)
84-
85-
serializer.write(document, output)
86-
File(stringsFolderFile, "strings.xml").writeText(writer.toString())
71+
File(stringsFolderFile, "strings.xml").writeText(document.dumpToString())
8772
}
8873
}

src/main/kotlin/com/bq/poeditor/gradle/xml/XmlPostProcessor.kt

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.bq.poeditor.gradle.xml
1818

19+
import com.bq.poeditor.gradle.ktx.dumpToString
20+
import com.bq.poeditor.gradle.ktx.toDocument
1921
import com.bq.poeditor.gradle.utils.ALL_REGEX_STRING
2022
import org.w3c.dom.Document
2123
import org.w3c.dom.Element
@@ -31,6 +33,13 @@ class XmlPostProcessor {
3133
companion object {
3234
private val DEFAULT_ENCODING = Charsets.UTF_8
3335
private val VARIABLE_REGEX = Regex("""\{\d?\{(.*?)\}\}""")
36+
37+
private const val TAG_RESOURCES = "resources"
38+
private const val TAG_STRING = "string"
39+
private const val TAG_PLURALS = "plurals"
40+
private const val TAG_ITEM = "item"
41+
42+
private const val ATTR_NAME = "name"
3443
}
3544

3645
/**
@@ -40,17 +49,29 @@ class XmlPostProcessor {
4049
* - Format variables and texts to conform to Android strings.xml format
4150
* - Split to multiple XML files depending on regex matching
4251
*/
43-
fun postProcessTranslationXml(translationXmlString: String,
52+
fun postProcessTranslationXml(translationFileXmlString: String,
4453
fileSplitRegexStringList: List<String>): Map<String, Document> =
45-
splitTranslationXml(formatTranslationXml(translationXmlString), fileSplitRegexStringList)
54+
splitTranslationXml(formatTranslationXml(translationFileXmlString), fileSplitRegexStringList)
55+
56+
/**
57+
* Formats a given translations XML string to conform to Android strings.xml format.
58+
*/
59+
fun formatTranslationXml(translationFileXmlString: String): String {
60+
// Parse line by line by traversing the original file using DOM
61+
val translationFileXmlDocument = translationFileXmlString.toDocument()
62+
63+
formatTranslationXmlDocument(translationFileXmlDocument, translationFileXmlDocument.childNodes)
64+
65+
return translationFileXmlDocument.dumpToString()
66+
}
4667

4768
/**
48-
* Format variables and texts to conform to Android strings.xml format.
69+
* Formats a given string to conform to Android strings.xml format.
4970
*/
50-
fun formatTranslationXml(translationXmlString: String): String {
51-
// We need to check for variables to see if we have to escape percent symbols: if we find variables, we have to
52-
// escape them
53-
val containsVariables = translationXmlString.contains(VARIABLE_REGEX)
71+
fun formatTranslationString(translationString: String): String {
72+
// We need to check for variables to see if we have to escape percent symbols: if we find variables, we have
73+
// to escape them
74+
val containsVariables = translationString.contains(VARIABLE_REGEX)
5475

5576
val placeholderTransform: (MatchResult) -> CharSequence = { matchResult ->
5677
// TODO: if the string has multiple variables but any of them has no order number,
@@ -64,7 +85,7 @@ class XmlPostProcessor {
6485
}
6586
}
6687

67-
return translationXmlString
88+
return translationString
6889
// Replace % with %% if variables are found
6990
.let { if (containsVariables) it.replace("%", "%%") else it }
7091
// Replace &lt; with < and &gt; with >
@@ -98,9 +119,9 @@ class XmlPostProcessor {
98119
nodes.forEach { node ->
99120
node.parentNode.removeChild(node)
100121
val copiedNode = (node.cloneNode(true) as Element).apply {
101-
val name = getAttribute("name")
122+
val name = getAttribute(ATTR_NAME)
102123
val nameWithoutRegex = regex.find(name)?.groups?.get(1)?.value ?: ""
103-
setAttribute("name", nameWithoutRegex)
124+
setAttribute(ATTR_NAME, nameWithoutRegex)
104125
}
105126
xmlRecords.adoptNode(copiedNode)
106127
xmlRecords.firstChild.appendChild(copiedNode)
@@ -113,25 +134,65 @@ class XmlPostProcessor {
113134
.plus(ALL_REGEX_STRING to translationFileRecords)
114135
}
115136

137+
private fun formatTranslationXmlDocument(document: Document, nodeList: NodeList, rootNode: Node? = null) {
138+
for (i in 0 until nodeList.length) {
139+
if (nodeList.item(i).nodeType == Node.ELEMENT_NODE) {
140+
val nodeElement = nodeList.item(i) as Element
141+
when (nodeElement.tagName) {
142+
TAG_RESOURCES -> {
143+
// Main node, traverse its children
144+
formatTranslationXmlDocument(document, nodeElement.childNodes, nodeElement)
145+
}
146+
TAG_PLURALS -> {
147+
// Plurals node, process its children
148+
formatTranslationXmlDocument(document, nodeElement.childNodes, nodeElement)
149+
}
150+
TAG_STRING -> {
151+
// String node, apply transformation to the content
152+
processTextAndReplaceNodeContent(document, nodeElement, rootNode)
153+
}
154+
TAG_ITEM -> {
155+
// Plurals item node, apply transformation to the content
156+
processTextAndReplaceNodeContent(document, nodeElement, rootNode)
157+
}
158+
}
159+
}
160+
}
161+
}
162+
163+
private fun processTextAndReplaceNodeContent(document: Document, nodeElement: Element, rootNode: Node?) {
164+
val content = nodeElement.textContent
165+
val processedContent = formatTranslationString(content)
166+
val copiedNodeElement = (nodeElement.cloneNode(true) as Element).apply {
167+
textContent = processedContent
168+
}
169+
document.adoptNode(copiedNodeElement)
170+
rootNode?.replaceChild(copiedNodeElement, nodeElement)
171+
}
172+
116173
private fun extractMatchingNodes(nodeList: NodeList, regexString: String): List<Node> {
117174
val matchedNodes = mutableListOf<Node>()
118175
val regex = Regex(regexString)
119176

120177
for (i in 0 until nodeList.length) {
121178
if (nodeList.item(i).nodeType == Node.ELEMENT_NODE) {
122179
val nodeElement = nodeList.item(i) as Element
123-
when {
124-
// Main XML node, process children
125-
nodeElement.tagName == "resources" -> {
180+
when (nodeElement.tagName) {
181+
TAG_RESOURCES -> {
182+
// Main XML node, process children
126183
matchedNodes.addAll(extractMatchingNodes(nodeElement.childNodes, regexString))
127184
}
128-
// String node, add if name matches regex
129-
nodeElement.tagName == "string" && nodeElement.getAttribute("name").matches(regex) -> {
130-
matchedNodes.add(nodeElement)
185+
TAG_STRING -> {
186+
// String node, add node if name matches regex
187+
if (nodeElement.getAttribute(ATTR_NAME).matches(regex)) {
188+
matchedNodes.add(nodeElement)
189+
}
131190
}
132-
// Plurals node, add node and children if name matches regex
133-
nodeElement.tagName == "plurals" && nodeElement.getAttribute("name").matches(regex) -> {
134-
matchedNodes.add(nodeElement)
191+
TAG_PLURALS -> {
192+
// Plurals node, add node and children if name matches regex
193+
if (nodeElement.getAttribute(ATTR_NAME).matches(regex)) {
194+
matchedNodes.add(nodeElement)
195+
}
135196
}
136197
}
137198
}

0 commit comments

Comments
 (0)