Skip to content

Conversation

@jim-rhoades
Copy link
Contributor

@jim-rhoades jim-rhoades commented Oct 27, 2017

This is the Aztec related work needed to implement adding links to images in posts from its detail editor.

One small detail to note is that in the example app, the linkURL is automatically set to the same thing as the file URL after the image is added in EditorDemoController.

Doing it that way, rather than automatically setting it within ImageAttachment, leaves it up to the developer integrating AztecEditor into their app to decide whether or not they want to automatically link images to themselves.

To test:

  1. run the example app and tap "Empty Editor Demo"
  2. tap the text view to bring up the format bar
  3. tap the "+" button in the lower left corner to add an image
  4. once you've added the image, tap on it to show "Tap for options"
  5. tap the image again, then tap "Media Details"
  6. note that the link URL is shown (change it to something else if you want)
  7. tap "Done"
  8. bring up the format bar again and tap the </> button to show that the link URL is shown within the HTML (with an <a href></a> containing the URL around the img tag)
  9. if you want, edit the URL within the HTML
  10. tap the </> button again to show that the HTML is properly converted back to an NSAttributedString... and that the image displays properly
  11. bring up the image media details again, and observe that the link URL is still set (because it was set after converting back from HTML)

@diegoreymendez
Copy link
Contributor

@jim-rhoades - Can you provide some sample testing steps? You can check out some of our other PRs for examples.

@jim-rhoades
Copy link
Contributor Author

Testing steps added!

Copy link
Contributor

@diegoreymendez diegoreymendez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jim-rhoades - I started with a logic and style check and added a few comments.

Let me know if there's anything.

let childContent = serialize(child, inheriting: childAttributes)
content.append(childContent)
// if the element is of type '.a' and contains a '.img'
if element.isNodeType(.a) && element.children.count == 1 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readability / Maintainability:

From this point onward we have 2 if conditions that could be reduced to a single method call for clarity. Something along the lines of:

if isLinkedImage(element) {
    // calculate content here
}

if let imgElement = element.children.first as? ElementNode, imgElement.isNodeType(.img) {

// get the link URL and assign it to the image attachment
if let linkText = element.stringValueForAttribute(named: "href") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic: If this is false, we're losing content.

We should actually set linkText to "" when href is missing.

// get the link URL and assign it to the image attachment
if let linkText = element.stringValueForAttribute(named: "href") {
let imgAttributes = self.attributes(for: imgElement, inheriting: attributes)
if let attachment = imgAttributes[NSAttachmentAttributeName] as? ImageAttachment,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic: There's a problem here since if this is false, we're losing content.

The truth is that the code we currently have doesn't support this scenario very well, so I'd recommend to deal with the other comments, and once that's done let's discuss this part of the code in Slack to try and provide a nice solution for it.

@jim-rhoades
Copy link
Contributor Author

I've made some changes to address the first two issues you mentioned, which also fixed the problem with one of the tests not passing.

Copy link
Contributor

@diegoreymendez diegoreymendez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice changes. I particularly like that you documented the new method.

I've added a few extra comments to try and improve both readability and maintainability.

Once we're done with these we chat to figure out the remaining logic.

// if the node is of type '.a' with one child of type '.img', retrieve the '.img' node
if let imgElement = linkedImageElementFor(element) {
// get the link URL text
var linkText = ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to avoid having linkText be a variable, you can reduce this and the next three lines into:

let linkText = element.stringValueForAttribute(named: "href") ?? ""

///
/// - Returns: the child '.img' node if there is one
///
fileprivate func linkedImageElementFor(_ element: ElementNode) -> ElementNode? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really minor, but to align better with the rest of our code we'd normally declare this method as:

fileprivate func linkedImageElement(for element: ElementNode) -> ElementNode? {

return nil
}

guard let imgElement = element.children.first as? ElementNode, imgElement.isNodeType(.img) else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can group this up with the above guard, like so:

guard element.isNodeType(.a) && element.children.count == 1,
    let imgElement = element.children.first as? ElementNode,
    imgElement.isNodeType(.img) else {
        return nil
}

content.append(attributedString)
}
}
// if the node is of type '.a' with one child of type '.img', retrieve the '.img' node
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this method since it describes internal details of linkedImageElementFor() that are already documented in the method.

}
// if the node is of type '.a' with one child of type '.img', retrieve the '.img' node
if let imgElement = linkedImageElementFor(element) {
// get the link URL text
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd normally avoid explaining in comments what the code does, since the code itself is readable enough to describe it.

In cases where the code is not easy to read, it normally means some effort can be put towards making it more readable (like you did with the method linkedImageElementFor()).

Copy link
Contributor

@diegoreymendez diegoreymendez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments.

element.updateAttribute(named: key, value: .string(finalValue))
}

// if the image attachment has a linkURL set,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the comments that explain what the code does, since that should be evident by just reading the code.

if let imgElement = linkedImageElement(for: element) {
let linkText = element.stringValueForAttribute(named: "href") ?? ""
let imgAttributes = self.attributes(for: imgElement, inheriting: attributes)
if let attachment = imgAttributes[NSAttachmentAttributeName] as? ImageAttachment,
Copy link
Contributor

@diegoreymendez diegoreymendez Oct 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specific problem we have at this point is that if this condition fails, we're losing content, since the element has no representation in the attributed string.

Upon inspecting the code in more depth I realized both of these conditions should succeed if we're dealing with an image element. If they ever failed it would mean we have broken logic somewhere.

The ideal solution would be to reengineer our formatters, but that'd be too big of a project for the scope of the trial.

In order to avoid a silent failure, we could use something like this:

let attachment = imgAttributes[NSAttachmentAttributeName] as! ImageAttachment
let attributedString = implicitRepresentation(for: imgElement, inheriting: imgAttributes)!

PS: I know it can look counter-intuitive but it's basically a decision between silently ignoring an error situation vs letting it "explode" and become more noticeable (so we can fix it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally I would try to avoid force unwrapping optionals, but I can see how "letting it explode" would be better in this case - to make the failure noticeable. Will make that change momentarily.

@diegoreymendez diegoreymendez added this to the Beta 14 (next stable) milestone Oct 30, 2017
@diegoreymendez diegoreymendez self-assigned this Oct 30, 2017
Copy link
Contributor

@diegoreymendez diegoreymendez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working as described.

We'll wait for the second review before merging.

Copy link
Contributor

@frosty frosty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! I ran through your testing steps and everything worked as expected. I've just left a couple of quite small code style comments. I'll mark the PR as approved, as I don't think they should hold it up, but one or two of them might be nice to quickly address before merging.


if let linkText = attachment.linkURL?.absoluteString {
let hrefValue = Attribute.Value(withString: linkText)
let hrefAttribute = Attribute(name: "href", value: hrefValue)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very familiar with the Aztec codebase, but "href" here feels like something we could use a constant for, instead of using string literals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and I see there's already a HTMLLinkAttribute enum with a constant for that. I'll change it.

}

if let imgElement = linkedImageElement(for: element) {
let linkText = element.stringValueForAttribute(named: "href") ?? ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See earlier comment about "href"


var attachment: ImageAttachment?
var onUpdate: ((ImageAttachment.Alignment, ImageAttachment.Size, URL, String?) -> Void)?
var onUpdate: ((ImageAttachment.Alignment, ImageAttachment.Size, URL, URL?, String?) -> Void)?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This closure is getting a little unwieldy, particularly for the urls and strings. I wonder if we should either add a comment that clarifies what each parameter does, or simply add names to the parameters:

var onUpdate: ((_ alignment: ImageAttachment.Alignment, _ size: ImageAttachment.Size, _ imageURL: URL, _ linkURL: URL?, _ altText: String?) -> Void)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of the code seems to favor making things self-explanatory, rather than adding comments... so I think adding names to the parameters is a good idea. Making that change.

Unfortunately that doesn't make the point where you're using the closure any more readable since it's still:
onUpdate(alignment.toAttachmentAlignment(), size.toAttachmentSize(), url, linkURL, alt)

(because "Function types cannot have argument labels" ? meh)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! But I guess at least in the sample you game, the variables you’re passing in all have explanatory names :)

onUpdate(alignment.toAttachmentAlignment(), size.toAttachmentSize(), url, alt)
var linkURL: URL?
if let linkText = linkURLTextField.text {
linkURL = URL(string: linkText)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could condense this like:

var linkURL = URL(string: linkURLTextField.text ?? "")

If you pass in an empty string, you get back nil.

Copy link
Contributor

@diegoreymendez diegoreymendez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the latest changes and all is fine. Merging.

Nice work @jim-rhoades :)

@diegoreymendez diegoreymendez merged commit a1fa190 into wordpress-mobile:develop Nov 2, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants