Skip to content

Commit

Permalink
Unified Token Formatting and Rendering (#364)
Browse files Browse the repository at this point in the history
- New toFormatted* token methods that accept options
- New render() token method to return final HTML tag intermediate representation
- New Options implementation to support token rendering
- Removed Options#resolve() method in favour of render
- Update interfaces to use new unified rendering
  • Loading branch information
Nick Frasser authored Nov 24, 2021
1 parent 6ba92e4 commit fc2e7d0
Show file tree
Hide file tree
Showing 15 changed files with 439 additions and 340 deletions.
78 changes: 30 additions & 48 deletions packages/linkify-element/src/linkify-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,52 +29,13 @@ function tokensToNodes(tokens, opts, doc) {
const result = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.t === 'nl' && opts.nl2br) {
if (token.t === 'nl' && opts.get('nl2br')) {
result.push(doc.createElement('br'));
continue;
} else if (!token.isLink || !opts.check(token)) {
result.push(doc.createTextNode(token.toString()));
continue;
} else {
result.push(opts.render(token));
}

const {
formatted,
formattedHref,
tagName,
className,
target,
rel,
events,
attributes,
} = opts.resolve(token);

// Build the link
const link = doc.createElement(tagName);
link.setAttribute('href', formattedHref);

if (className) { link.setAttribute('class', className); }
if (target) { link.setAttribute('target', target); }
if (rel) { link.setAttribute('rel', rel); }

// Build up additional attributes
if (attributes) {
for (const attr in attributes) {
link.setAttribute(attr, attributes[attr]);
}
}

if (events) {
for (const event in events) {
if (link.addEventListener) {
link.addEventListener(event, events[event]);
} else if (link.attachEvent) {
link.attachEvent('on' + event, events[event]);
}
}
}

link.appendChild(doc.createTextNode(formatted));
result.push(link);
}

return result;
Expand All @@ -83,7 +44,7 @@ function tokensToNodes(tokens, opts, doc) {
/**
* Requires document.createElement
* @param {HTMLElement} element
* @param {Object} opts
* @param {Options} opts
* @param {Document} doc
* @returns {HTMLElement}
*/
Expand All @@ -94,10 +55,8 @@ function linkifyElementHelper(element, opts, doc) {
throw new Error(`Cannot linkify ${element} - Invalid DOM Node type`);
}

const { ignoreTags } = opts;

// Is this element already a link?
if (element.tagName === 'A' || ignoreTags.indexOf(element.tagName) >= 0) {
if (element.tagName === 'A' || opts.ignoreTags.indexOf(element.tagName) >= 0) {
// No need to linkify
return element;
}
Expand Down Expand Up @@ -138,6 +97,27 @@ function linkifyElementHelper(element, opts, doc) {
return element;
}

/**
* @param {Document} doc The document implementaiton
*/
function getDefaultRender(doc) {
return ({ tagName, attributes, content, eventListeners }) => {
const link = doc.createElement(tagName);
for (const attr in attributes) {
link.setAttribute(attr, attributes[attr]);
}

if (eventListeners && link.addEventListener) {
for (const event in eventListeners) {
link.addEventListener(event, eventListeners[event]);
}
}

link.appendChild(doc.createTextNode(content));
return link;
};
}

/**
* Recursively traverse the given DOM node, find all links in the text and
* convert them to anchor tags.
Expand All @@ -160,10 +140,12 @@ export default function linkifyElement(element, opts, doc = null) {
);
}

opts = new Options(opts);
opts = new Options(opts, getDefaultRender(doc));
return linkifyElementHelper(element, opts, doc);
}

// Maintain reference to the recursive helper to cache option-normalization
linkifyElement.helper = linkifyElementHelper;
linkifyElement.normalize = (opts) => new Options(opts);
linkifyElement.getDefaultRender = getDefaultRender;
linkifyElement.normalize = (opts, doc) => new Options(opts, getDefaultRender(doc));

2 changes: 1 addition & 1 deletion packages/linkify-jquery/src/linkify-jquery.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function apply($, doc = false) {
}

function jqLinkify(opts) {
opts = linkifyElement.normalize(opts);
opts = linkifyElement.normalize(opts, doc);
return this.each(function () {
linkifyElement.helper(this, opts, doc);
});
Expand Down
68 changes: 24 additions & 44 deletions packages/linkify-react/src/linkify-react.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,26 @@ import { tokenize, Options } from 'linkifyjs';
* Given a string, converts to an array of valid React components
* (which may include strings)
* @param {string} str
* @param {any} opts
* @param {Options} opts
* @returns {React.ReactNodeArray}
*/
function stringToElements(str, opts) {
function stringToElements(str, opts, parentElementId) {

const tokens = tokenize(str);
const elements = [];
let linkId = 0;
let nlId = 0;

for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];

if (token.t === 'nl' && opts.nl2br) {
elements.push(React.createElement('br', {key: `linkified-${++linkId}`}));
continue;
if (token.t === 'nl' && opts.get('nl2br')) {
elements.push(React.createElement('br', { key: `__linkify-el-${parentElementId}-nl-${nlId++}` }));
} else if (!token.isLink || !opts.check(token)) {
// Regular text
elements.push(token.toString());
continue;
}

const {
formatted,
formattedHref,
tagName,
className,
target,
rel,
attributes
} = opts.resolve(token);

const props = { key: `linkified-${++linkId}`, href: formattedHref };

if (className) { props.className = className; }
if (target) { props.target = target; }
if (rel) { props.rel = rel; }

// Build up additional attributes
// Support for events via attributes hash
if (attributes) {
for (var attr in attributes) {
props[attr] = attributes[attr];
}
} else {
elements.push(opts.render(token));
}

elements.push(React.createElement(tagName, props, formatted));
}

return elements;
Expand All @@ -61,7 +35,7 @@ function stringToElements(str, opts) {
* @template P
* @template {string | React.JSXElementConstructor<P>} T
* @param {React.ReactElement<P, T>} element
* @param {Object} opts
* @param {Options} opts
* @param {number} elementId
* @returns {React.ReactElement<P, T>}
*/
Expand All @@ -76,16 +50,15 @@ function linkifyReactElement(element, opts, elementId = 0) {
React.Children.forEach(element.props.children, (child) => {
if (typeof child === 'string') {
// ensure that we always generate unique element IDs for keys
elementId = elementId + 1;
children.push(...stringToElements(child, opts));
children.push.apply(children, stringToElements(child, opts, elementId));
} else if (React.isValidElement(child)) {
if (typeof child.type === 'string'
&& opts.ignoreTags.indexOf(child.type.toUpperCase()) >= 0
) {
// Don't linkify this element
children.push(child);
} else {
children.push(linkifyReactElement(child, opts, ++elementId));
children.push(linkifyReactElement(child, opts, elementId + 1));
}
} else {
// Unknown element type, just push
Expand All @@ -94,11 +67,7 @@ function linkifyReactElement(element, opts, elementId = 0) {
});

// Set a default unique key, copy over remaining props
const newProps = { key: `linkified-element-${elementId}` };
for (const prop in element.props) {
newProps[prop] = element.props[prop];
}

const newProps = Object.assign({ key: `__linkify-el-${elementId}` }, element.props);
return React.cloneElement(element, newProps, children);
}

Expand All @@ -110,14 +79,25 @@ function linkifyReactElement(element, opts, elementId = 0) {
*/
const Linkify = (props) => {
// Copy over all non-linkify-specific props
const newProps = { key: 'linkified-element-wrapper' };
let linkId = 0;

const defaultLinkRender = ({ tagName, attributes, content }) => {
attributes.key = `__linkify-lnk-${++linkId}`;
if (attributes.class) {
attributes.className = attributes.class;
delete attributes.class;
}
return React.createElement(tagName, attributes, content);
};

const newProps = { key: '__linkify-wrapper' };
for (const prop in props) {
if (prop !== 'options' && prop !== 'tagName' && prop !== 'children') {
newProps[prop] = props[prop];
}
}

const opts = new Options(props.options);
const opts = new Options(props.options, defaultLinkRender);
const tagName = props.tagName || React.Fragment || 'span';
const children = props.children;
const element = React.createElement(tagName, newProps, children);
Expand Down
Loading

0 comments on commit fc2e7d0

Please sign in to comment.