jsocol/bleach — Issue #56: Pass links through (a) callback(s)
Author: jsocol
Created: 2012-02-17 22:22:32+00:00
State: open
Labels: linkify

An interesting idea from @aehlke in pull req #43 is to pass links through a callback or callbacks that accept and return (possibly among other things) a dict of attributes.

I think this has a lot of potential to simplify the `linkify` API and address the issues that `delinkify` aimed to, but in a better way.

Total straw man API might be:

```python
def linkify(text, callback):
# ... snip ...

def some_callback(text, attributes=None, new=False): # This signature would be specified in docs.
# Do processing, including things like url and text munging that are currently callbacks.
# Create a link.
return {'href': href, ...} # Return a dict, everything's OK.
# Or don't create a link/remove the link.
return None # Existing links removed, new links not created.
```

The obvious question that arises is what to do with multiple callbacks. It'd be great to make these callback functions small, atomic, etc, so something along the lines of Django context processors seems obvious:

```python
def linkify(text, callbacks=None):
if callbacks is None:
callbacks = []
elsif callable(callbacks):
callbacks = [callbacks]
# ... snip ...
# Found a link!
attrs = {'some': 'dict', ...}
for cb in callbacks:
a = cb(link_text, attrs, new)
if a is None:
# Break out of this somehow, just replace everything with the text.
attrs.update(a)
# Render the link with the attributes in attrs
```

Pros of this approach:

* Generalizes everything `linkify` currently does. `filter_url` and `filter_text` are already callbacks, so they could easily be updated to return dicts. Adding or setting attributes like `rel` and `target` is trivial, etc.
* Is extremely flexible.
* It's easy to create a bunch of default callbacks for people to use.
* It can handle what `delinkify` was meant to do--just write a callback that only allows relative links or links from certain domains.
* It can handle the `skip_existing_links` idea from pull req #43.

Cons, and some suggestions:

* Adding new callbacks requires repeating all the old ones. Hopefully this is solvable (e.g. `linkify(text, DEFAULT_CALLBACKS + [my_callback])` or something).
* The order of the callbacks is critical and might introduce subtle bugs for people? (They should have tests.)
* It's flexible but more complicated for developers.
* How to deal with the link text? (Maybe add it to the dict as `_text`? Something that can never be an HTML attribute.)
* As described above, wouldn't ever remove attributes unless their was some sort of whitelist/blacklist filter callback. (Maybe allow setting `'some_attr': None` to remove it? Or make sure you return the whole `attr` dict, unlike Django context processors.)