Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug Fix] @Image directive requires an explicit file extension while images described in Markdown do not(SR-15315) #3

Merged
merged 1 commit into from
Oct 27, 2021

Conversation

Kyle-Ye
Copy link
Collaborator

@Kyle-Ye Kyle-Ye commented Oct 15, 2021

Bug fix: SR-15315

https://bugs.swift.org/browse/SR-15315

Summary

Starting a Local Preview Server and set a breakpoint at ResourceReference's path property

Remove the ".png" to trigger the pause.

And find only 2 API get the path property.
One is inside DocumentationContext.swift

public func resource(with identifier: ResourceReference, trait: DataTraitCollection = .init()) throws -> Data {
guard let bundle = bundle(identifier: identifier.bundleIdentifier),
let assetManager = assetManagers[identifier.bundleIdentifier],
let asset = assetManager.allData(named: identifier.path) else {
throw ContextError.notFound(identifier.url)
}
let resource = asset.data(bestMatching: trait)
return try dataProvider.contentsOfURL(resource.url, in: bundle)
}

The other is inside RenderNodeTranslator.swift

/// Creates a render reference for the given media and registers the reference to include it in the `references` dictionary.
mutating func createAndRegisterRenderReference(forMedia media: ResourceReference?, poster: ResourceReference? = nil, altText: String? = nil, assetContext: DataAsset.Context = .display) -> RenderReferenceIdentifier {
var mediaReference = RenderReferenceIdentifier("")
guard let media = media else { return mediaReference }
let fileExtension = NSString(string: media.path).pathExtension
func resolveAsset() -> DataAsset? {
renderContext?.store.content(
forAssetNamed: media.path, bundleIdentifier: identifier.bundleIdentifier)
?? context.resolveAsset(named: media.path, in: identifier)
}
// Check if media is a supported image.
if DocumentationContext.isFileExtension(fileExtension, supported: .image),
let resolvedImages = resolveAsset()
{
mediaReference = RenderReferenceIdentifier(media.path)
imageReferences[media.path] = ImageReference(
identifier: mediaReference,
// If no alt text has been provided and this image has been registered previously, use the registered alt text.
altText: altText ?? imageReferences[media.path]?.altText,
imageAsset: resolvedImages
)
}
if DocumentationContext.isFileExtension(fileExtension, supported: .video),
let resolvedVideos = resolveAsset()
{
mediaReference = RenderReferenceIdentifier(media.path)
let poster = poster.map { createAndRegisterRenderReference(forMedia: $0) }
videoReferences[media.path] = VideoReference(identifier: mediaReference, altText: altText, videoAsset: resolvedVideos, poster: poster)
}
if assetContext == DataAsset.Context.download, let resolvedDownload = resolveAsset() {
// Create a download reference if possible.
let downloadReference: DownloadReference
do {
mediaReference = RenderReferenceIdentifier(media.path)
let downloadURL = resolvedDownload.variants.first!.value
let downloadData = try context.dataProvider.contentsOfURL(downloadURL, in: bundle)
downloadReference = DownloadReference(identifier: mediaReference,
renderURL: downloadURL,
sha512Checksum: Checksum.sha512(of: downloadData))
} catch {
// It seems this is the way to error out of here.
return mediaReference
}
// Add the file to the download references.
mediaReference = RenderReferenceIdentifier(media.path)
downloadReferences[media.path] = downloadReference
}
return mediaReference
}

The first will call DataAssetManager and will use inner fuzzyKeyIndex to query which have no problem.
The second is where the bug comes from. It need to get the extension of the media to check if is supported.

My 2 potential fix suggestion:

  1. Create some helper method in "DataAssetManager" and "DocumentationContext" to use fuzzyKeyIndex to get the file extension Which seems ugly.
  2. Assume a no-file-extension media must be a supported image and then skip the check. (Which I do in the commit)

Dependencies

NO

Testing

Steps:

  1. Download sample project https://developer.apple.com/documentation/xcode/slothcreator_building_docc_documentation_in_xcode
  2. Remove any @image'source string of ".png" in the tutorial file of the sample project.
  3. Build documentation to verify fix

Checklist

Make sure you check off the following items. If they cannot be completed, provide a reason.

  • Added tests
  • Ran the ./bin/test script and it succeeded
  • Updated documentation if necessary

@Kyle-Ye Kyle-Ye marked this pull request as ready for review October 15, 2021 08:42
@Kyle-Ye Kyle-Ye changed the title Fix SR-15315 [Bugfix] @Image directive requires an explicit file extension while images described in Markdown do not(SR-15315) Oct 15, 2021
@Kyle-Ye Kyle-Ye changed the title [Bugfix] @Image directive requires an explicit file extension while images described in Markdown do not(SR-15315) [Bug Fix] @Image directive requires an explicit file extension while images described in Markdown do not(SR-15315) Oct 16, 2021
Copy link
Member

@franklinsch franklinsch left a comment

Choose a reason for hiding this comment

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

Thanks for these changes! It's looking great. I just have one small suggestion regarding the doc comment. Also, we’ll want to add tests to check the new behavior.

@Kyle-Ye
Copy link
Collaborator Author

Kyle-Ye commented Oct 20, 2021

Rebase to the updated main branch.
Update the doc comment and add the related test.
Any suggestion on the test function? @franklinsch

@franklinsch
Copy link
Member

franklinsch commented Oct 21, 2021

Thanks for adding the test! It tests the behavior of the API in DocumentationContext , but it doesn’t cover that RenderNodeTranslator actually calls it with the correct parameters, i.e., the test would still pass if we weren’t specifying the type argument to this function in RenderNodeTranslator. Does that make sense?

I’d recommend implementing another test in RenderNodeTranslatorTests to verify similar assertions of your existing test. Thanks so much for doing this work and iterating on it, I really appreciate it!

@Kyle-Ye
Copy link
Collaborator Author

Kyle-Ye commented Oct 22, 2021

Thanks for adding the test! It tests the behavior of the API in DocumentationContext , but it doesn’t cover that RenderNodeTranslator actually calls it with the correct parameters, i.e., the test would still pass if we weren’t specifying the type argument to this function in RenderNodeTranslator. Does that make sense?

I’d recommend implementing another test in RenderNodeTranslatorTests to verify similar assertions of your existing test. Thanks so much for doing this work and iterating on it, I really appreciate it!

Did not find a file named RenderNodeTranslatorTests.swift and add the test into SemaToRenderNodeTests.swift

XCTAssertEqual(translator.imageReferences.count, 3)
XCTAssertNotNil(translator.imageReferences["intro.png"])
XCTAssertNotNil(translator.imageReferences["introposter"])
XCTAssertNotNil(translator.imageReferences["introposter.png"])
Copy link
Member

Choose a reason for hiding this comment

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

I think we should be asserting on the RenderNode node here, similar to other tests, rather than the state of the translator. I don't think it's expected for us to have separate image references for introposter and introposter.png, since they refer to the same asset, right? I think we should use always use the full file name with extension as the key.

Copy link
Collaborator Author

@Kyle-Ye Kyle-Ye Oct 22, 2021

Choose a reason for hiding this comment

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

The RenderNode seem to be always true, do you mean we assert the renderNode's references property or something else?

let renderNode = translator.visit(technology) as! RenderNode
guard let introPosterReference = renderNode.references["introposter.png"] as? ImageReference else {
    XCTFail("Missing intro poster reference")
    return
}

I don't think it's expected for us to have separate image references for introposter and introposter.png, since they refer to the same asset, right?

Yes, they refer to the same asset. But due a implementation detail, they have different key, will try to solve the wrong behavior in the coming commit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fix the test issue, but for the second problem I think we need the resolveAsset(named name: String, in parent: ResolvedTopicReference, ofType type: AssetType? = nil) method to not only return a DataAsset? but also the queried "full media name"/"file extension" String from the inner bestKey method. Is that OK with you?

/// Finds the best matching storage key for a given asset name.
/// `name` is one of the following formats:
/// - "image" - asset name without extension
/// - "image.png" - asset name including extension
func bestKey(forAssetName name: String) -> String? {
    guard !storage.keys.contains(name) else { return name }
    
    // Try the fuzzy index
    return fuzzyKeyIndex[name]
}

Screen Shot 2021-10-22 at 20 54 12

Copy link
Member

Choose a reason for hiding this comment

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

Hmm interesting. If we create a new function in the context, it would need to find the correct DataAssetManager to query again. Maybe it would be reasonable to add a property in DataAsset (which resolveAsset returns) that tracks the identifier that should be used when referring to this asset. That way, we resolve the asset and get its identifier in one call.

Something like this in DataAsset:

/// The identifier of the asset.
///
/// This is typically the base name of the asset and its extension, for example "`image.png`".
public var identifier: String

We'd deprecate the existing DataAsset initializer and create a new one that takes an identifier, and update usages of the DataAsset initializer to use the new one that takes an identifier.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it is a way to achieve it. But can we wrap and expose the bestKey method like the updated PR? So we'll get the fullname of the asset. Which do you prefer?

Copy link
Member

Choose a reason for hiding this comment

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

We could expose the bestKey functionality (which I think we would name differently in DocumentationContext) but I think starting to store the identifiers in DataAsset instead makes sense long term. That way, clients don't need to query the context twice (once to resolve, another to get the identifier). I think it's expected that you'd get a canonical identifier for it as part of the resolution process.

Interested in hearing your thoughts though!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I think from long term we need such identifier property in DataAsset, I'm currently trying to implement it. But encounter some problems here.

Sure, we can use dataAsset.identifier here to indicate the name.
Screen Shot 2021-10-22 at 21 58 09

But in some case the DataAsset(identifier:) seem hard to use/fit the situation.
Screen Shot 2021-10-22 at 21 59 45

And sometimes it causes duplication.
Screen Shot 2021-10-22 at 22 00 34

Also after the trying, I change my mind and think DataAsset actually don't fit to add a identifier property. Since it is an abstract of "Asset" and should not contain the key.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

And we can view and discuss the WIP change on the branch here if needed

Copy link
Member

Choose a reason for hiding this comment

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

I think the changes you're showing the screenshot make sense—for the VideoReference decoder implementation, the identifier we should use is the identifier value declared a few lines above, since that's the value we're encoding. Regarding the duplication, I'm not too concerned about that, it turns out we're also using the identifier as the key of a dictionary for fast lookups.

@Kyle-Ye
Copy link
Collaborator Author

Kyle-Ye commented Oct 27, 2021

Update with the "nit" suggestion

@franklinsch
Copy link
Member

@swift-ci please test

Copy link
Member

@franklinsch franklinsch left a comment

Choose a reason for hiding this comment

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

Looks great, thank you @Kyle-Ye !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants