Skip to content

Commit

Permalink
Extracted HTML card visibility rendering to utility function
Browse files Browse the repository at this point in the history
ref https://linear.app/ghost/issue/PLG-332

- extracted visibility rendering logic and adapted to be more generalised for use in other cards
  • Loading branch information
kevinansfield committed Feb 5, 2025
1 parent 222a942 commit 9ab1269
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 38 deletions.
42 changes: 5 additions & 37 deletions packages/kg-default-nodes/lib/nodes/html/html-renderer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {addCreateDocumentOption} from '../../utils/add-create-document-option';
import {renderEmptyContainer} from '../../utils/render-empty-container';
import {ALL_MEMBERS_SEGMENT, usesOldVisibilityFormat, migrateOldVisibilityFormat, NO_MEMBERS_SEGMENT} from '../../utils/visibility';
import {renderWithVisibility} from '../../utils/visibility';

export function renderHtmlNode(node, options = {}) {
addCreateDocumentOption(options);
Expand All @@ -17,43 +17,11 @@ export function renderHtmlNode(node, options = {}) {
const textarea = document.createElement('textarea');
textarea.value = wrappedHtml;

if (!options.feature?.contentVisibility || !node.visibility) {
// `type: 'value'` will render the value of the textarea element
return {element: textarea, type: 'value'};
}

const visibility = node.visibility;
const oldVisibilityFormat = usesOldVisibilityFormat(visibility);

if (oldVisibilityFormat) {
migrateOldVisibilityFormat(visibility);
}

if (options.target === 'email') {
if (visibility.email.memberSegment === NO_MEMBERS_SEGMENT) {
return renderEmptyContainer(document);
}

if (visibility.email.memberSegment === ALL_MEMBERS_SEGMENT) {
return {element: textarea, type: 'value'};
}

const container = document.createElement('div');
container.innerHTML = wrappedHtml;
container.setAttribute('data-gh-segment', visibility.email.memberSegment);
return {element: container, type: 'html'};
}

if (visibility.web.nonMember === false && visibility.web.memberSegment === NO_MEMBERS_SEGMENT) {
return renderEmptyContainer(document);
}

// If there are restrictions on web visibility, wrap the HTML in a gated block comment
if (visibility.web.nonMember !== true || visibility.web.memberSegment !== ALL_MEMBERS_SEGMENT) {
const {nonMember, memberSegment} = visibility.web;
const visibilityWrappedHtml = `\n<!--kg-gated-block:begin nonMember:${nonMember} memberSegment:"${memberSegment}" -->${textarea.value}<!--kg-gated-block:end-->\n`;
textarea.value = visibilityWrappedHtml;
if (options.feature?.contentVisibility || node.visibility) {
const renderOutput = {element: textarea, type: 'value'};
return renderWithVisibility(renderOutput, node.visibility, options);
}

// `type: 'value'` will render the value of the textarea element
return {element: textarea, type: 'value'};
}
72 changes: 72 additions & 0 deletions packages/kg-default-nodes/lib/utils/visibility.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {renderEmptyContainer} from './render-empty-container';

export const ALL_MEMBERS_SEGMENT = 'status:free,status:-free';
export const NO_MEMBERS_SEGMENT = '';

Expand Down Expand Up @@ -34,3 +36,73 @@ export function migrateOldVisibilityFormat(visibility) {
visibility.email.memberSegment = NO_MEMBERS_SEGMENT;
}
}

export function renderWithVisibility(originalRenderOutput, visibility, options) {
const document = originalRenderOutput.element.ownerDocument;
const content = _getRenderContent(originalRenderOutput);

if (usesOldVisibilityFormat(visibility)) {
migrateOldVisibilityFormat(visibility);
}

if (options.target === 'email') {
if (visibility.email.memberSegment === NO_MEMBERS_SEGMENT) {
return renderEmptyContainer(document);
}

if (visibility.email.memberSegment === ALL_MEMBERS_SEGMENT) {
return originalRenderOutput;
}

return _renderWithEmailVisibility(document, content, visibility.email);
}

const isNotVisibleOnWeb =
visibility.web.nonMember === false &&
visibility.web.memberSegment === NO_MEMBERS_SEGMENT;

if (isNotVisibleOnWeb) {
return renderEmptyContainer(document);
}

const hasWebVisibilityRestrictions =
visibility.web.nonMember !== true ||
visibility.web.memberSegment !== ALL_MEMBERS_SEGMENT;

if (hasWebVisibilityRestrictions) {
return _renderWithWebVisibility(document, content, visibility.web);
}

return originalRenderOutput;
}

/* Private functions -------------------------------------------------------- */

function _getRenderContent({element, type}) {
if (type === 'inner') {
return element.innerHTML;
} else if (type === 'value') {
if ('value' in element) {
return element.value;
}
return '';
} else {
return element.outerHTML;
}
}

function _renderWithEmailVisibility(document, content, emailVisibility) {
const {memberSegment} = emailVisibility;
const container = document.createElement('div');
container.innerHTML = content;
container.setAttribute('data-gh-segment', memberSegment);
return {element: container, type: 'html'};
}

function _renderWithWebVisibility(document, content, webVisibility) {
const {nonMember, memberSegment} = webVisibility;
const wrappedContent = `\n<!--kg-gated-block:begin nonMember:${nonMember} memberSegment:"${memberSegment}" -->${content}<!--kg-gated-block:end-->\n`;
const textarea = document.createElement('textarea');
textarea.value = wrappedContent;
return {element: textarea, type: 'value'};
}
143 changes: 142 additions & 1 deletion packages/kg-default-nodes/test/utils/visibility.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {JSDOM} = require('jsdom');
const {utils} = require('../../');
const {usesOldVisibilityFormat, migrateOldVisibilityFormat} = utils.visibility;
const {usesOldVisibilityFormat, migrateOldVisibilityFormat, renderWithVisibility, buildDefaultVisibility} = utils.visibility;

describe('Utils: visibility', function () {
describe('usesOldVisibilityFormat', function () {
Expand Down Expand Up @@ -57,4 +58,144 @@ describe('Utils: visibility', function () {
visibility.segment.should.eql('status:free');
});
});

describe('renderWithVisibility', function () {
let document;

before(function () {
document = (new JSDOM()).window.document;
});

function buildVisibility(visibility) {
return {...buildDefaultVisibility(), ...visibility};
}

function runRender(html, visibility, target) {
const visibilityWithDefaults = buildVisibility(visibility);

const p = document.createElement('p');
p.innerHTML = html;

const originalOutput = {
element: p,
type: 'html'
};
return renderWithVisibility(originalOutput, visibilityWithDefaults, {target});
}

describe('email target', function () {
it('returns empty container when membersSegment === no members', function () {
const visibility = {email: {memberSegment: ''}};
const result = runRender('testing', visibility, 'email');

result.element.tagName.should.equal('SPAN');
result.element.innerHTML.should.equal('');
result.type.should.equal('inner');
});

it('returns original output when membersSegment === all members', function () {
const visibility = {email: {memberSegment: 'status:free,status:-free'}};
const result = runRender('testing', visibility, 'email');

result.element.tagName.should.eql('P');
result.element.innerHTML.should.equal('testing');
result.type.should.equal('html');
});

it('wraps original output when emailing a specific segment', function () {
const visibility = {email: {memberSegment: 'status:free'}};
const result = runRender('testing', visibility, 'email');

result.element.tagName.should.eql('DIV');
result.element.dataset.ghSegment.should.equal('status:free');
result.element.innerHTML.should.equal('<p>testing</p>');
result.type.should.equal('html');
});
});

describe('web target', function () {
it('returns original output when no restrictions placed on web visibility', function () {
const visibility = {web: {nonMember: true, memberSegment: 'status:free,status:-free'}};
const result = runRender('testing', visibility, 'web');

result.element.tagName.should.eql('P');
result.element.innerHTML.should.equal('testing');
result.type.should.equal('html');
});

it('adds wrapping comments when anonymous is gated', function () {
const visibility = {web: {nonMember: false, memberSegment: 'status:free,status:-free'}};
const result = runRender('testing', visibility, 'web');

result.element.tagName.should.equal('TEXTAREA');
result.element.value.should.equal('\n<!--kg-gated-block:begin nonMember:false memberSegment:"status:free,status:-free" --><p>testing</p><!--kg-gated-block:end-->\n');
});

it('adds wrapping comments when member segment is gated', function () {
const visibility = {web: {nonMember: true, memberSegment: 'status:free'}};
const result = runRender('testing', visibility, 'web');

result.element.tagName.should.equal('TEXTAREA');
result.element.value.should.equal('\n<!--kg-gated-block:begin nonMember:true memberSegment:"status:free" --><p>testing</p><!--kg-gated-block:end-->\n');
});
});

it('handles no render type', function () {
const visibility = buildVisibility({web: {nonMember: true, memberSegment: 'status:free'}});
const p = document.createElement('p');
p.innerHTML = 'testing';
const originalOutput = {element: p};

const result = renderWithVisibility(originalOutput, buildVisibility(visibility), {target: 'web'});

result.element.tagName.should.equal('TEXTAREA');
result.element.value.should.equal('\n<!--kg-gated-block:begin nonMember:true memberSegment:"status:free" --><p>testing</p><!--kg-gated-block:end-->\n');
});

it('handles inner render type', function () {
const visibility = buildVisibility({web: {nonMember: true, memberSegment: 'status:free'}});
const div = document.createElement('div');
div.innerHTML = '<!--comment test--><span>testing</span>';
const originalOutput = {element: div, type: 'inner'};

const result = renderWithVisibility(originalOutput, buildVisibility(visibility), {target: 'web'});

result.element.tagName.should.equal('TEXTAREA');
result.element.value.should.equal('\n<!--kg-gated-block:begin nonMember:true memberSegment:"status:free" --><!--comment test--><span>testing</span><!--kg-gated-block:end-->\n');
});

it('handles value render type', function () {
const visibility = buildVisibility({web: {nonMember: true, memberSegment: 'status:free'}});
const input = document.createElement('input');
input.value = '<!--comment test--><span>testing</span>';
const originalOutput = {element: input, type: 'value'};

const result = renderWithVisibility(originalOutput, buildVisibility(visibility), {target: 'web'});

result.element.tagName.should.equal('TEXTAREA');
result.element.value.should.equal('\n<!--kg-gated-block:begin nonMember:true memberSegment:"status:free" --><!--comment test--><span>testing</span><!--kg-gated-block:end-->\n');
});

it('handles old beta visibility format', function () {
const visibility = {
showOnWeb: true,
showOnEmail: true,
segment: 'status:free'
};

const p = document.createElement('p');
p.innerHTML = 'testing';

const originalOutput = {
element: p,
type: 'html'
};
const result = renderWithVisibility(originalOutput, visibility, {target: 'email'});

result.element.tagName.should.eql('DIV');
result.element.dataset.ghSegment.should.equal('status:free');
result.element.innerHTML.should.equal('<p>testing</p>');
result.type.should.equal('html');
});
});
});

0 comments on commit 9ab1269

Please sign in to comment.