diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java
index 31c5005cd53d..1ce4cf240a9c 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java
@@ -426,7 +426,7 @@ public static String replaceMediaFileWithUrlInGutenbergPost(@NonNull String post
.notNullStr(Utils.escapeQuotes(mediaFile.getFileURL()));
MediaUploadCompletionProcessor processor = new MediaUploadCompletionProcessor(localMediaId, mediaFile,
siteUrl);
- postContent = processor.processPost(postContent);
+ postContent = processor.processContent(postContent);
}
return postContent;
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java
index eccb6b37d43c..80129877df69 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessor.java
@@ -1,5 +1,8 @@
package org.wordpress.android.ui.posts.mediauploadcompletionprocessors;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Document.OutputSettings;
@@ -7,7 +10,8 @@
import org.wordpress.android.util.helpers.MediaFile;
import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+
+import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaUploadCompletionProcessorPatterns.PATTERN_BLOCK_CAPTURES;
/**
* Abstract class to be extended for each enumerated {@link MediaBlockType}.
@@ -27,11 +31,12 @@ abstract class BlockProcessor {
String mRemoteId;
String mRemoteUrl;
- private Pattern mBlockPattern;
- private String mHeaderComment;
- private String mBlockContent;
+ private String mBlockName;
+ private JsonObject mJsonAttributes;
+ private Document mBlockContentDocument;
private String mClosingComment;
+
/**
* @param localId The local media id that needs replacement
* @param mediaFile The mediaFile containing the remote id and remote url
@@ -40,51 +45,38 @@ abstract class BlockProcessor {
mLocalId = localId;
mRemoteId = mediaFile.getMediaId();
mRemoteUrl = org.wordpress.android.util.StringUtils.notNullStr(Utils.escapeQuotes(mediaFile.getFileURL()));
- mBlockPattern = Pattern.compile(String.format(getBlockPatternTemplate(), localId), Pattern.DOTALL);
}
- // TODO: consider processing block header JSON in a more robust way (current processing uses RexEx)
- /**
- * @param block The raw block contents of the block to be matched
- * @return A {@link Matcher} to extract block contents and splice the header with a remote id. The matcher has the
- * following capture groups:
- *
- *
- * - Block header before id
- * - The localId (to be replaced)
- * - Block header after id
- * - Block contents
- * - Block closing comment and any following characters
- *
- */
- Matcher getMatcherForBlock(String block) {
- return mBlockPattern.matcher(block);
+ private JsonObject parseJson(String blockJson) {
+ JsonParser parser = new JsonParser();
+ return parser.parse(blockJson).getAsJsonObject();
+ }
+
+ private Document parseHTML(String blockContent) {
+ // create document from block content
+ Document document = Jsoup.parse(blockContent);
+ document.outputSettings(OUTPUT_SETTINGS);
+ return document;
}
- boolean matchAndSpliceBlockHeader(String block) {
- Matcher matcher = getMatcherForBlock(block);
+ private boolean splitBlock(String block) {
+ Matcher captures = PATTERN_BLOCK_CAPTURES.matcher(block);
- boolean matchFound = matcher.find();
+ boolean capturesFound = captures.find();
- if (matchFound) {
- mHeaderComment = new StringBuilder()
- .append(matcher.group(1))
- .append(mRemoteId) // here we substitute remote id in place of the local id
- .append(matcher.group(3))
- .toString();
- mBlockContent = matcher.group(4);
- mClosingComment = matcher.group(5);
+ if (capturesFound) {
+ mBlockName = captures.group(1);
+ mJsonAttributes = parseJson(captures.group(2));
+ mBlockContentDocument = parseHTML(captures.group(3));
+ mClosingComment = captures.group(4);
+ return true;
} else {
- mHeaderComment = null;
- mBlockContent = null;
+ mBlockName = null;
+ mJsonAttributes = null;
+ mBlockContentDocument = null;
mClosingComment = null;
+ return false;
}
-
- return matchFound;
- }
-
- String getHeaderComment() {
- return mHeaderComment;
}
/**
@@ -94,46 +86,28 @@ String getHeaderComment() {
* @param block The raw block contents
* @return A string containing content with ids and urls replaced
*/
- String processBlock(String block) {
- if (matchAndSpliceBlockHeader(block)) {
- // create document from block content
- Document document = Jsoup.parse(mBlockContent);
- document.outputSettings(OUTPUT_SETTINGS);
-
- if (processBlockContentDocument(document)) {
- // return injected block
- return new StringBuilder()
- .append(getHeaderComment())
- .append(document.body().html()) // parser output
- .append(mClosingComment)
- .toString();
- }
- }
-
- // leave block unchanged
- return block;
- }
-
- /**
- * All concrete implementations must implement this method to return a regex pattern template for the particular
- * block type.
- *
- * The pattern template should contain a format specifier for the local id that needs to be matched and
- * replaced in the block header, and the format specifier should be within its own capture group, e.g. `(%1$s)`.
- *
- * The pattern template should result in a matcher with the following capture groups:
- *
- *
- * - Block header before id
- * - The format specifier for the local id (to be replaced by the local id when generating the pattern)
- * - Block header after id
- * - Block contents
- * - Block closing comment and any following characters
- *
- *
- * @return String with the regex pattern template
- */
- abstract String getBlockPatternTemplate();
+ String processBlock(String block) {
+ if (splitBlock(block)) {
+ if (processBlockJsonAttributes(mJsonAttributes)) {
+ if (processBlockContentDocument(mBlockContentDocument)) {
+ // return injected block
+ return new StringBuilder()
+ .append("\n")
+ .append(mBlockContentDocument.body().html()) // HTML parser output
+ .append(mClosingComment)
+ .toString();
+ }
+ } else {
+ return processInnerBlock(block); // delegate to inner blocks if needed
+ }
+ }
+ // leave block unchanged
+ return block;
+ }
/**
* All concrete implementations must implement this method for the particular block type. The document represents
@@ -146,4 +120,33 @@ String processBlock(String block) {
* @return A boolean value indicating whether or not the block contents should be replaced
*/
abstract boolean processBlockContentDocument(Document document);
+
+ /**
+ * All concrete implementations must implement this method for the particular block type. The jsonAttributes object
+ * is a {@link JsonObject} parsed from the block header attributes. This object can be used to check for a match,
+ * and can be directly mutated if necessary.
+ *
+ * This method should return true to indicate success. Returning false will result in the block contents being
+ * unmodified.
+ *
+ * @param jsonAttributes the attributes object used to check for a match with the local id, and mutated if necessary
+ * @return
+ */
+ abstract boolean processBlockJsonAttributes(JsonObject jsonAttributes);
+
+ /**
+ * This method can be optionally overriden by concrete implementations to delegate further processing via recursion
+ * when {@link BlockProcessor#processBlockJsonAttributes(JsonObject)} returns false (i.e. the block did not match
+ * the local id being replaced). This is useful for implementing mutual recursion with
+ * {@link MediaUploadCompletionProcessor#processContent(String)} for block types that have media-containing blocks
+ * within their inner content.
+ *
+ * The default implementation provided is a NOOP that leaves the content of the block unchanged.
+ *
+ * @param block The raw block contents
+ * @return A string containing content with ids and urls replaced
+ */
+ String processInnerBlock(String block) {
+ return block;
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessorFactory.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessorFactory.java
index 1c0d8e60528b..b33568c3c850 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessorFactory.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/BlockProcessorFactory.java
@@ -5,19 +5,22 @@
import java.util.HashMap;
import java.util.Map;
+import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaBlockType.COVER;
import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaBlockType.GALLERY;
import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaBlockType.IMAGE;
import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaBlockType.MEDIA_TEXT;
import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaBlockType.VIDEO;
class BlockProcessorFactory {
+ private final MediaUploadCompletionProcessor mMediaUploadCompletionProcessor;
private final Map mMediaBlockTypeBlockProcessorMap;
/**
* This factory initializes block processors for all media block types and provides a method to retrieve a block
* processor instance for a given block type.
*/
- BlockProcessorFactory() {
+ BlockProcessorFactory(MediaUploadCompletionProcessor mediaUploadCompletionProcessor) {
+ mMediaUploadCompletionProcessor = mediaUploadCompletionProcessor;
mMediaBlockTypeBlockProcessorMap = new HashMap<>();
}
@@ -32,6 +35,8 @@ BlockProcessorFactory init(String localId, MediaFile mediaFile, String siteUrl)
mMediaBlockTypeBlockProcessorMap.put(VIDEO, new VideoBlockProcessor(localId, mediaFile));
mMediaBlockTypeBlockProcessorMap.put(MEDIA_TEXT, new MediaTextBlockProcessor(localId, mediaFile));
mMediaBlockTypeBlockProcessorMap.put(GALLERY, new GalleryBlockProcessor(localId, mediaFile, siteUrl));
+ mMediaBlockTypeBlockProcessorMap.put(COVER, new CoverBlockProcessor(localId, mediaFile,
+ mMediaUploadCompletionProcessor));
return this;
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java
new file mode 100644
index 000000000000..f5cc3e18b6bc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessor.java
@@ -0,0 +1,79 @@
+package org.wordpress.android.ui.posts.mediauploadcompletionprocessors;
+
+import com.google.gson.JsonObject;
+
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.wordpress.android.util.helpers.MediaFile;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CoverBlockProcessor extends BlockProcessor {
+ /**
+ * Template pattern used to match and splice cover inner blocks
+ */
+ private static final Pattern PATTERN_COVER_INNER = Pattern.compile(new StringBuilder()
+ .append("(^.*?\\s*)")
+ .append("(.*)") // inner block contents
+ .append("(\\s*
\\s*\\s*.*)").toString(), Pattern.DOTALL);
+
+ /**
+ * Pattern to match background-image url in cover block html content
+ */
+ private static final Pattern PATTERN_BACKGROUND_IMAGE_URL = Pattern.compile(
+ "background-image:\\s*url\\([^\\)]+\\)");
+
+ private final MediaUploadCompletionProcessor mMediaUploadCompletionProcessor;
+
+ public CoverBlockProcessor(String localId, MediaFile mediaFile,
+ MediaUploadCompletionProcessor mediaUploadCompletionProcessor) {
+ super(localId, mediaFile);
+ mMediaUploadCompletionProcessor = mediaUploadCompletionProcessor;
+ }
+
+ @Override String processInnerBlock(String block) {
+ Matcher innerMatcher = PATTERN_COVER_INNER.matcher(block);
+ boolean innerCapturesFound = innerMatcher.find();
+
+ // process inner contents recursively
+ if (innerCapturesFound) {
+ String innerProcessed = mMediaUploadCompletionProcessor.processContent(innerMatcher.group(2)); //
+ return new StringBuilder()
+ .append(innerMatcher.group(1))
+ .append(innerProcessed)
+ .append(innerMatcher.group(3))
+ .toString();
+ }
+
+ return block;
+ }
+
+ @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) {
+ if (jsonAttributes.get("id").getAsInt() == Integer.parseInt(mLocalId, 10)) {
+ jsonAttributes.addProperty("id", Integer.parseInt(mRemoteId, 10));
+ jsonAttributes.addProperty("url", mRemoteUrl);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override boolean processBlockContentDocument(Document document) {
+ // select cover block div
+ Element targetDiv = document.select(".wp-block-cover").first();
+
+ // if a match is found, proceed with replacement
+ if (targetDiv != null) {
+ // replace background-image url in style attribute
+ String style = PATTERN_BACKGROUND_IMAGE_URL.matcher(targetDiv.attr("style"))
+ .replaceFirst(String.format("background-image:url(%1$s)", mRemoteUrl));
+ targetDiv.attr("style", style);
+
+ // return injected block
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java
index 4d5c25a9c226..59d7f8363036 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/GalleryBlockProcessor.java
@@ -1,38 +1,17 @@
package org.wordpress.android.ui.posts.mediauploadcompletionprocessors;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.wordpress.android.util.helpers.MediaFile;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
public class GalleryBlockProcessor extends BlockProcessor {
- /**
- * Template pattern used to match and splice gallery blocks
- */
- private static final String PATTERN_TEMPLATE_GALLERY = "(\n?)" // rest of header
- + "(.*)" // block contents
- + "(\n?)"; // closing comment
-
-
- /**
- * A {@link Pattern} to match and capture gallery linkTo property from block header
- *
- *
- * - Block header before linkTo property
- * - The linkTo property
- * - Block header after linkTo property
- *
- */
- public static final Pattern PATTERN_GALLERY_LINK_TO = Pattern.compile("(\n?)"); // rest of header
-
-
private String mAttachmentPageUrl;
+ private String mLinkTo;
/**
* Query selector for selecting the img element from gallery which needs processing
@@ -49,10 +28,6 @@ public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl
mAttachmentPageUrl = mediaFile.getAttachmentPageURL(siteUrl);
}
- @Override String getBlockPatternTemplate() {
- return PATTERN_TEMPLATE_GALLERY;
- }
-
@Override boolean processBlockContentDocument(Document document) {
// select image element with our local id
Element targetImg = document.select(mGalleryImageQuerySelector).first();
@@ -69,15 +44,10 @@ public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl
targetImg.removeClass("wp-image-" + mLocalId);
targetImg.addClass("wp-image-" + mRemoteId);
- // check for linkTo property
- Matcher linkToMatcher = PATTERN_GALLERY_LINK_TO.matcher(getHeaderComment());
-
// set parent anchor href if necessary
Element parent = targetImg.parent();
- if (parent != null && parent.is("a") && linkToMatcher.find()) {
- String linkToValue = linkToMatcher.group(2);
-
- switch (linkToValue) {
+ if (parent != null && parent.is("a") && mLinkTo != null) {
+ switch (mLinkTo) {
case "media":
parent.attr("href", mRemoteUrl);
break;
@@ -95,4 +65,19 @@ public GalleryBlockProcessor(String localId, MediaFile mediaFile, String siteUrl
return false;
}
+
+ @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) {
+ JsonArray ids = jsonAttributes.getAsJsonArray("ids");
+ JsonElement linkTo = jsonAttributes.get("linkTo");
+ if (linkTo != null) {
+ mLinkTo = linkTo.getAsString();
+ }
+ for (int i = 0; i < ids.size(); i++) {
+ if (ids.get(i).getAsString().equals(mLocalId)) {
+ ids.set(i, new JsonPrimitive(Integer.parseInt(mRemoteId, 10)));
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java
index 650a92ca148a..fc2dd197060f 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/ImageBlockProcessor.java
@@ -1,27 +1,16 @@
package org.wordpress.android.ui.posts.mediauploadcompletionprocessors;
+import com.google.gson.JsonObject;
+
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.wordpress.android.util.helpers.MediaFile;
public class ImageBlockProcessor extends BlockProcessor {
- /**
- * Template pattern used to match and splice image blocks
- */
- private static final String PATTERN_TEMPLATE_IMAGE = "(\n?)" // rest of header
- + "(.*)" // block contents
- + "(\n?)"; // closing comment
-
public ImageBlockProcessor(String localId, MediaFile mediaFile) {
super(localId, mediaFile);
}
- @Override String getBlockPatternTemplate() {
- return PATTERN_TEMPLATE_IMAGE;
- }
-
@Override boolean processBlockContentDocument(Document document) {
// select image element with our local id
Element targetImg = document.select("img").first();
@@ -40,4 +29,13 @@ public ImageBlockProcessor(String localId, MediaFile mediaFile) {
return false;
}
+
+ @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) {
+ if (jsonAttributes.get("id").getAsString().equals(mLocalId)) {
+ jsonAttributes.addProperty("id", Integer.parseInt(mRemoteId));
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaBlockType.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaBlockType.java
index fd3fa6a1de10..7de0bf67305b 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaBlockType.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaBlockType.java
@@ -3,6 +3,8 @@
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -12,7 +14,26 @@ enum MediaBlockType {
IMAGE("image"),
VIDEO("video"),
MEDIA_TEXT("media-text"),
- GALLERY("gallery");
+ GALLERY("gallery"),
+ COVER("cover");
+
+ private static final Map MAP = new HashMap<>();
+ private static final String MATCHING_GROUP;
+ private static final Pattern PATTERN_MEDIA_BLOCK_TYPES;
+
+ static {
+ for (MediaBlockType type : values()) {
+ MAP.put(type.mName, type);
+ }
+
+ MATCHING_GROUP = StringUtils.join(Arrays.asList(MediaBlockType.values()), "|");
+
+ PATTERN_MEDIA_BLOCK_TYPES = Pattern.compile(new StringBuilder()
+ .append(PATTERN_BLOCK_PREFIX)
+ .append(MATCHING_GROUP)
+ .append(")")
+ .toString());
+ }
private final String mName;
@@ -25,12 +46,7 @@ public String toString() {
}
static MediaBlockType fromString(String blockType) {
- for (MediaBlockType mediaBlockType : MediaBlockType.values()) {
- if (mediaBlockType.mName.equals(blockType)) {
- return mediaBlockType;
- }
- }
- return null;
+ return MAP.get(blockType);
}
/**
@@ -38,7 +54,7 @@ static MediaBlockType fromString(String blockType) {
* regex capturing group pattern)
*/
static String getMatchingGroup() {
- return StringUtils.join(Arrays.asList(MediaBlockType.values()), "|");
+ return MATCHING_GROUP;
}
/**
@@ -48,12 +64,7 @@ static String getMatchingGroup() {
* @return The media block type or null if no match is found
*/
static MediaBlockType detectBlockType(String block) {
- final Pattern pattern = Pattern.compile(new StringBuilder()
- .append(PATTERN_BLOCK_PREFIX)
- .append(MediaBlockType.getMatchingGroup())
- .append(")")
- .toString());
- Matcher matcher = pattern.matcher(block);
+ Matcher matcher = PATTERN_MEDIA_BLOCK_TYPES.matcher(block);
if (matcher.find()) {
return MediaBlockType.fromString(matcher.group(1));
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java
index 588f630ddd02..558eaa12536d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaTextBlockProcessor.java
@@ -1,27 +1,16 @@
package org.wordpress.android.ui.posts.mediauploadcompletionprocessors;
+import com.google.gson.JsonObject;
+
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.wordpress.android.util.helpers.MediaFile;
public class MediaTextBlockProcessor extends BlockProcessor {
- /**
- * Template pattern used to match and splice media-text blocks
- */
- private static final String PATTERN_TEMPLATE_MEDIA_TEXT = "(\n?)" // rest of header
- + "(.*)" // block contents
- + "(\n?)"; // closing comment
-
public MediaTextBlockProcessor(String localId, MediaFile mediaFile) {
super(localId, mediaFile);
}
- @Override String getBlockPatternTemplate() {
- return PATTERN_TEMPLATE_MEDIA_TEXT;
- }
-
@Override boolean processBlockContentDocument(Document document) {
// select image element with our local id
Element targetImg = document.select("img").first();
@@ -53,4 +42,13 @@ public MediaTextBlockProcessor(String localId, MediaFile mediaFile) {
return false;
}
+
+ @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) {
+ if (jsonAttributes.get("mediaId").getAsString().equals(mLocalId)) {
+ jsonAttributes.addProperty("mediaId", Integer.parseInt(mRemoteId));
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessor.java
index f741dc7a820d..bd75608a9e24 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessor.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessor.java
@@ -3,8 +3,10 @@
import org.wordpress.android.util.helpers.MediaFile;
import java.util.regex.Matcher;
+import java.util.regex.Pattern;
-import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaUploadCompletionProcessorPatterns.PATTERN_BLOCK;
+import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaUploadCompletionProcessorPatterns.PATTERN_BLOCK_HEADER;
+import static org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaUploadCompletionProcessorPatterns.PATTERN_TEMPLATE_BLOCK_BOUNDARY;
public class MediaUploadCompletionProcessor {
private final BlockProcessorFactory mBlockProcessorFactory;
@@ -18,34 +20,49 @@ public class MediaUploadCompletionProcessor {
* @param siteUrl The site url - used to generate the attachmentPage url
*/
public MediaUploadCompletionProcessor(String localId, MediaFile mediaFile, String siteUrl) {
- mBlockProcessorFactory = new BlockProcessorFactory()
+ mBlockProcessorFactory = new BlockProcessorFactory(this)
.init(localId, mediaFile, siteUrl);
}
/**
- * Processes a post to replace the local ids and local urls of media with remote ids and remote urls. This matches
- * media-containing blocks and delegates further processing to {@link #processBlock(String)}
+ * Processes content to replace the local ids and local urls of media with remote ids and remote urls. This method
+ * delineates block boundaries for media-containing blocks and delegates further processing via itself and / or
+ * {@link #processBlock(String)}, via direct and mutual recursion, respectively.
*
- * @param postContent The post content to be processed
- * @return A string containing the processed post, or the original content if no match was found
+ * @param content The content to be processed
+ * @return A string containing the processed content, or the original content if no match was found
*/
- public String processPost(String postContent) {
- Matcher matcher = PATTERN_BLOCK.matcher(postContent);
- StringBuilder result = new StringBuilder();
+ public String processContent(String content) {
+ Matcher headerMatcher = PATTERN_BLOCK_HEADER.matcher(content);
- int position = 0;
+ int positionBlockStart, positionBlockEnd = content.length();
- while (matcher.find()) {
- result.append(postContent.substring(position, matcher.start()));
- result.append(processBlock(matcher.group()));
- position = matcher.end();
- }
+ if (headerMatcher.find()) {
+ positionBlockStart = headerMatcher.start();
+ String blockType = headerMatcher.group(1);
+ Matcher blockBoundaryMatcher = Pattern.compile(String.format(PATTERN_TEMPLATE_BLOCK_BOUNDARY, blockType),
+ Pattern.DOTALL).matcher(content.substring(headerMatcher.end()));
- result.append(postContent.substring(position));
+ int nestLevel = 1;
- return result.toString();
- }
+ while (0 < nestLevel && blockBoundaryMatcher.find()) {
+ if (blockBoundaryMatcher.group(1).equals("/")) {
+ positionBlockEnd = headerMatcher.end() + blockBoundaryMatcher.end();
+ nestLevel--;
+ } else {
+ nestLevel++;
+ }
+ }
+ return new StringBuilder()
+ .append(content.substring(0, positionBlockStart))
+ .append(processBlock(content.substring(positionBlockStart, positionBlockEnd)))
+ .append(processContent(content.substring(positionBlockEnd)))
+ .toString();
+ } else {
+ return content;
+ }
+ }
/**
* Processes a media block returning a raw content replacement string
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatterns.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatterns.java
index 157730a9aad5..643fada1f527 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatterns.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatterns.java
@@ -3,17 +3,40 @@
import java.util.regex.Pattern;
public class MediaUploadCompletionProcessorPatterns {
- // TODO: these patterns can be DRYed up after implementing JSON handling for the block header
public static final String PATTERN_BLOCK_PREFIX = "";
/**
- * A {@link Pattern} to match Gutenberg media-containing blocks with a capture group for the block type
+ * A {@link Pattern} to match headers for Gutenberg media-containing blocks with a capture group for the block type
*/
- public static final Pattern PATTERN_BLOCK = Pattern.compile(new StringBuilder()
+ public static final Pattern PATTERN_BLOCK_HEADER = Pattern.compile(new StringBuilder()
.append(PATTERN_BLOCK_PREFIX)
.append(MediaBlockType.getMatchingGroup())
- .append(").*?")
- .append(PATTERN_BLOCK_SUFFIX)
+ .append(").*? -->\n?")
+ .toString(), Pattern.DOTALL);
+
+ /**
+ * A pattern template to match the block boundaries of a specific Gutenberg block type with a capture group to
+ * identify the match as either the beginning or end of a block: group(1) == "/" for the end of a block
+ */
+ public static final String PATTERN_TEMPLATE_BLOCK_BOUNDARY = "\n?";
+
+ /**
+ * A {@link Pattern} to match Gutenberg media-containing blocks with the following capture groups:
+ *
+ *
+ * - Block type
+ * - Block json attributes
+ * - Block html content
+ * - Block closing comment and any following characters
+ *
+ *
+ */
+ public static final Pattern PATTERN_BLOCK_CAPTURES = Pattern.compile(new StringBuilder()
+ .append(PATTERN_BLOCK_PREFIX) // start-of-group: block type
+ .append(MediaBlockType.getMatchingGroup())
+ .append(")") // end-of-group: block type
+ .append(" (\\{.*?\\}) -->\n?") // group: block header json
+ .append("(.*)") // group: html content
+ .append("(.*)") // group: closing-comment (name must match group 1: block type)
.toString(), Pattern.DOTALL);
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java
index 1f188f253fef..ae91d130dbdf 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/VideoBlockProcessor.java
@@ -1,27 +1,16 @@
package org.wordpress.android.ui.posts.mediauploadcompletionprocessors;
+import com.google.gson.JsonObject;
+
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.wordpress.android.util.helpers.MediaFile;
public class VideoBlockProcessor extends BlockProcessor {
- /**
- * Template pattern used to match and splice video blocks
- */
- private static final String PATTERN_TEMPLATE_VIDEO = "(\n?)" // rest of header
- + "(.*)" // block contents
- + "(\n?)"; // closing comment
-
public VideoBlockProcessor(String localId, MediaFile mediaFile) {
super(localId, mediaFile);
}
- @Override String getBlockPatternTemplate() {
- return PATTERN_TEMPLATE_VIDEO;
- }
-
@Override boolean processBlockContentDocument(Document document) {
// select video element with our local id
Element targetVideo = document.select("video").first();
@@ -37,4 +26,13 @@ public VideoBlockProcessor(String localId, MediaFile mediaFile) {
return false;
}
+
+ @Override boolean processBlockJsonAttributes(JsonObject jsonAttributes) {
+ if (jsonAttributes.get("id").getAsString().equals(mLocalId)) {
+ jsonAttributes.addProperty("id", Integer.parseInt(mRemoteId));
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessorTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessorTest.kt
new file mode 100644
index 000000000000..c67dfbcab422
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/CoverBlockProcessorTest.kt
@@ -0,0 +1,63 @@
+package org.wordpress.android.ui.posts.mediauploadcompletionprocessors
+
+import com.nhaarman.mockitokotlin2.any
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.whenever
+import org.assertj.core.api.Assertions
+import org.junit.Test
+import org.junit.Before
+
+import org.wordpress.android.util.helpers.MediaFile
+
+class CoverBlockProcessorTest {
+ private val mediaFile: MediaFile = mock()
+ private val mediaUploadCompletionProcessor: MediaUploadCompletionProcessor = mock()
+ private lateinit var processor: CoverBlockProcessor
+
+ @Before
+ fun before() {
+ whenever(mediaFile.mediaId).thenReturn(TestContent.remoteMediaId)
+ whenever(mediaFile.fileURL).thenReturn(TestContent.remoteImageUrl)
+ processor = CoverBlockProcessor(TestContent.localMediaId, mediaFile, mediaUploadCompletionProcessor)
+ }
+
+ @Test
+ fun `processBlock replaces temporary local id and url for cover block`() {
+ val processedBlock = processor.processBlock(TestContent.oldCoverBlock)
+ Assertions.assertThat(processedBlock).isEqualTo(TestContent.newCoverBlock)
+ }
+
+ @Test
+ fun `processBlock works with nested image blocks`() {
+ val processedBlock = processor.processBlock(TestContent.oldCoverBlockWithNestedImageBlock)
+ Assertions.assertThat(processedBlock).isEqualTo(TestContent.newCoverBlockWithNestedImageBlock)
+ }
+
+ @Test
+ fun `processBlock does not process inner nested cover blocks`() {
+ whenever(mediaUploadCompletionProcessor.processContent(any())).thenReturn(TestContent.oldCoverBlock + "\n ")
+ val processedBlock = processor.processBlock(TestContent.oldCoverBlockWithNestedCoverBlockInner)
+ Assertions.assertThat(processedBlock).isEqualTo(TestContent.oldCoverBlockWithNestedCoverBlockInner)
+ }
+
+ @Test
+ fun `processBlock works with outer nested cover blocks`() {
+ whenever(mediaFile.mediaId).thenReturn(TestContent.remoteMediaId2)
+ whenever(mediaFile.fileURL).thenReturn(TestContent.remoteImageUrl2)
+ processor = CoverBlockProcessor(TestContent.localMediaId2, mediaFile, mediaUploadCompletionProcessor)
+ val processedBlock = processor.processBlock(TestContent.oldCoverBlockWithNestedCoverBlockOuter)
+ Assertions.assertThat(processedBlock).isEqualTo(TestContent.newCoverBlockWithNestedCoverBlockOuter)
+ }
+
+ @Test
+ fun `processBlock works with different inline style order`() {
+ val processedBlock = processor.processBlock(TestContent.oldCoverBlockDifferentStyleOrder)
+ Assertions.assertThat(processedBlock).isEqualTo(TestContent.newCoverBlockDifferentStyleOrder)
+ }
+
+ @Test
+ fun `processBlock works with a space in inline styles`() {
+ val processedBlock = processor.processBlock(TestContent.oldCoverBlockStyleOrderWithSpace)
+ Assertions.assertThat(processedBlock).isEqualTo(TestContent.newCoverBlockStyleOrderWithoutSpace)
+ }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatternsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatternsTest.kt
new file mode 100644
index 000000000000..87f2b2c407c4
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorPatternsTest.kt
@@ -0,0 +1,51 @@
+package org.wordpress.android.ui.posts.mediauploadcompletionprocessors
+
+import org.assertj.core.api.Assertions
+import org.junit.Test
+import org.wordpress.android.ui.posts.mediauploadcompletionprocessors.MediaUploadCompletionProcessorPatterns.PATTERN_BLOCK_CAPTURES
+
+const val blockType = "image"
+const val blockJson = """{"url":"file:///image.png","id":123,someObject:{a:1,b:2},someArray:[1,2,3]}"""
+const val blockHTML = """
+"""
+const val blockHeader = """"""
+const val blockClosingComment = """"""
+const val rawBlock = """$blockHeader
+$blockHTML
+$blockClosingComment"""
+const val nestedRawBlock = """$blockHeader
+
+$blockClosingComment
+"""
+
+class MediaUploadCompletionProcessorPatternsTest {
+ @Test
+ fun `PATTERN_BLOCK_CAPTURES captures the block type`() {
+ val matcher = PATTERN_BLOCK_CAPTURES.matcher(rawBlock)
+ val outcome = matcher.find()
+ Assertions.assertThat(outcome).isEqualTo(true)
+ Assertions.assertThat(matcher.group(1)).isEqualTo(blockType)
+ }
+ @Test
+ fun `PATTERN_BLOCK_CAPTURES captures the block header json`() {
+ val matcher = PATTERN_BLOCK_CAPTURES.matcher(rawBlock)
+ Assertions.assertThat(matcher.find()).isEqualTo(true)
+ Assertions.assertThat(matcher.group(2)).isEqualTo(blockJson)
+ }
+ @Test
+ fun `PATTERN_BLOCK_CAPTURES captures the block content`() {
+ val matcher = PATTERN_BLOCK_CAPTURES.matcher(rawBlock)
+ Assertions.assertThat(matcher.find()).isEqualTo(true)
+ Assertions.assertThat(matcher.group(3)).isEqualTo(blockHTML + "\n")
+ }
+}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorTest.kt
index fd6b52b30389..5d8a29bfd706 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/MediaUploadCompletionProcessorTest.kt
@@ -26,7 +26,7 @@ class MediaUploadCompletionProcessorTest {
@Test
fun `processPost splices id and url for an image block`() {
- val blocks = processor.processPost(TestContent.oldPostImage)
+ val blocks = processor.processContent(TestContent.oldPostImage)
Assertions.assertThat(blocks).isEqualTo(TestContent.newPostImage)
}
@@ -34,19 +34,52 @@ class MediaUploadCompletionProcessorTest {
fun `processPost splices id and url for a video block`() {
whenever(mediaFile.fileURL).thenReturn(TestContent.remoteVideoUrl)
processor = MediaUploadCompletionProcessor(TestContent.localMediaId, mediaFile, TestContent.siteUrl)
- val blocks = processor.processPost(TestContent.oldPostVideo)
+ val blocks = processor.processContent(TestContent.oldPostVideo)
Assertions.assertThat(blocks).isEqualTo(TestContent.newPostVideo)
}
@Test
fun `processPost splices id and url for a media-text block`() {
- val blocks = processor.processPost(TestContent.oldPostMediaText)
+ val blocks = processor.processContent(TestContent.oldPostMediaText)
Assertions.assertThat(blocks).isEqualTo(TestContent.newPostMediaText)
}
@Test
fun `processPost splices id and url for a gallery block`() {
- val blocks = processor.processPost(TestContent.oldPostGallery)
+ val blocks = processor.processContent(TestContent.oldPostGallery)
Assertions.assertThat(blocks).isEqualTo(TestContent.newPostGallery)
}
+
+ @Test
+ fun `processPost splices id and url for a cover block`() {
+ val blocks = processor.processContent(TestContent.oldPostCover)
+ Assertions.assertThat(blocks).isEqualTo(TestContent.newPostCover)
+ }
+
+ @Test
+ fun `processPost works for nested inner cover blocks`() {
+ val blocks = processor.processContent(TestContent.oldCoverBlockWithNestedCoverBlockInner)
+ Assertions.assertThat(blocks).isEqualTo(TestContent.newCoverBlockWithNestedCoverBlockInner)
+ }
+
+ @Test
+ fun `processPost works for nested outer cover blocks`() {
+ whenever(mediaFile.mediaId).thenReturn(TestContent.remoteMediaId2)
+ whenever(mediaFile.fileURL).thenReturn(TestContent.remoteImageUrl2)
+ processor = MediaUploadCompletionProcessor(TestContent.localMediaId2, mediaFile, TestContent.siteUrl)
+ val blocks = processor.processContent(TestContent.oldCoverBlockWithNestedCoverBlockOuter)
+ Assertions.assertThat(blocks).isEqualTo(TestContent.newCoverBlockWithNestedCoverBlockOuter)
+ }
+
+ @Test
+ fun `processPost works for image blocks nested within a cover block`() {
+ val processedContent = processor.processContent(TestContent.oldImageBlockNestedInCoverBlock)
+ Assertions.assertThat(processedContent).isEqualTo(TestContent.newImageBlockNestedInCoverBlock)
+ }
+
+ @Test
+ fun `processPost leaves malformed cover block unchanged`() {
+ val processedContent = processor.processContent(TestContent.malformedCoverBlock)
+ Assertions.assertThat(processedContent).isEqualTo(TestContent.malformedCoverBlock)
+ }
}
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/TestContent.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/TestContent.kt
index 2a6a02dd9147..985bcc976ea8 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/TestContent.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/mediauploadcompletionprocessors/TestContent.kt
@@ -5,18 +5,21 @@ package org.wordpress.android.ui.posts.mediauploadcompletionprocessors
object TestContent {
const val siteUrl = "https://wordpress.org"
private const val localImageUrl = "file://Screenshot-1-1.png"
+ private const val localImageUrl2 = "file://Screenshot-1-2.png"
const val remoteImageUrl = "https://onetwoonetwothisisjustatesthome.files.wordpress.com/2019/11/pexels-photo-1671668.jpg"
+ const val remoteImageUrl2 = "https://onetwoonetwothisisjustatesthome.files.wordpress.com/2019/12/img_20191202_094944-19.jpg"
private const val remoteImageUrlBlogLink = "http://onetwoonetwothisisjustatest.home.blog/pexels-photo-1671668/"
private const val remoteImageUrlWithSize = "https://onetwoonetwothisisjustatesthome.files.wordpress.com/2019/11/pexels-photo-1671668.jpg?w=1024"
- private const val remoteImageUrl2 = "https://onetwoonetwothisisjustatesthome.files.wordpress.com/2019/12/img_20191202_094944-19.jpg"
private const val remoteImageUrl2BlogLink = "http://onetwoonetwothisisjustatest.home.blog/?attachment_id=369"
private const val remoteImageUrl2WithSize = "https://onetwoonetwothisisjustatesthome.files.wordpress.com/2019/12/img_20191202_094944-19.jpg?w=768"
private const val localVideoUrl = "file://local-video.mov"
const val remoteVideoUrl = "https://videos.files.wordpress.com/qeJFeNa2/macintosh-plus-floral-shoppe-02-e383aae382b5e38395e383a9e383b3e382af420-e78fbee4bba3e381aee382b3e383b3e38394e383a5e383bc-1_hd.mp4"
const val localMediaId = "112"
+ const val localMediaId2 = "113"
private const val collidingPrefixMediaId = "${localMediaId}42"
private const val collidingSuffixMediaId = "42${localMediaId}"
const val remoteMediaId = "97629"
+ const val remoteMediaId2 = "97630"
const val attachmentPageUrl = "https://wordpress.org?p=${remoteMediaId}"
const val oldImageBlock = """
@@ -357,6 +360,143 @@ object TestContent {
+"""
+
+ const val oldCoverBlock = """
+
+
+"""
+
+ const val newCoverBlock = """
+
+
+"""
+ const val oldCoverBlockWithNestedImageBlock = """
+
+
+"""
+ const val newCoverBlockWithNestedImageBlock = """
+
+
+"""
+ const val oldCoverBlockWithNestedCoverBlockOuter = """
+
+
+"""
+ const val newCoverBlockWithNestedCoverBlockOuter = """
+
+
+"""
+ const val oldCoverBlockWithNestedCoverBlockInner = """
+
+
+"""
+ const val newCoverBlockWithNestedCoverBlockInner = """
+
+
+"""
+ const val oldImageBlockNestedInCoverBlock = """
+
+
+"""
+ const val newImageBlockNestedInCoverBlock = """
+
+
+"""
+ const val malformedCoverBlock = """
+
+"""
+ const val oldCoverBlockDifferentStyleOrder = """
+
+
+"""
+
+ const val newCoverBlockDifferentStyleOrder = """
+
+
+"""
+ const val oldCoverBlockStyleOrderWithSpace = """
+
+
+"""
+
+ const val newCoverBlockStyleOrderWithoutSpace = """
+
+
"""
const val oldPostImage = paragraphBlock + oldImageBlock + newVideoBlock + newMediaTextBlock + newGalleryBlock
@@ -367,4 +507,6 @@ object TestContent {
const val newPostMediaText = paragraphBlock + newImageBlock + newVideoBlock + newMediaTextBlock + newGalleryBlock
const val oldPostGallery = paragraphBlock + newImageBlock + newVideoBlock + newMediaTextBlock + oldGalleryBlock
const val newPostGallery = paragraphBlock + newImageBlock + newVideoBlock + newMediaTextBlock + newGalleryBlock
+ const val oldPostCover = paragraphBlock + newImageBlock + oldCoverBlock + newMediaTextBlock + oldGalleryBlock
+ const val newPostCover = paragraphBlock + newImageBlock + newCoverBlock + newMediaTextBlock + newGalleryBlock
}
diff --git a/libs/gutenberg-mobile b/libs/gutenberg-mobile
index 6906be56ace3..6d7a9952ea96 160000
--- a/libs/gutenberg-mobile
+++ b/libs/gutenberg-mobile
@@ -1 +1 @@
-Subproject commit 6906be56ace31186ded1df0b65b9ea34fe2aaf34
+Subproject commit 6d7a9952ea964eb9d3eb79f41463e2465fba6585