johnalbin / drupal-twig-extensions Goto Github PK
View Code? Open in Web Editor NEWThis project forked from kalamuna/twig-drupal-filters
JavaScript implementation of Drupal’s Twig extensions
License: Other
This project forked from kalamuna/twig-drupal-filters
JavaScript implementation of Drupal’s Twig extensions
License: Other
The active_theme_path
function should return the active theme path if we add base_path
and active_theme_path
configuration during setup.
See #9 for details.
Drupal-specific Twig tags are not implemented in Twing.
Since the API of Twig.js and Twing differ so much, it is doubtful the same implementation can be shared. See #8 for Twig.js
The official list of tags is in Drupal 9’s code.
trans
, plural
, and endtrans
: These tags are described in Drupal’s “Translation in Twig templates” documentation.The file_url
function should return configured values if we add options to set them during setup.
See #9 for details.
Hey John!
Saw you added a few awesome things here. Noticed you weren't a maintainer of the npm package so I added you there:
https://www.npmjs.com/package/twig-drupal-filters
Anyways, if you're up to move these changes back up to -filters, I'm sure they'd be happy to.
Thanks!
Rob
There's a bunch of logic in TwigExtension::renderVar
that is separate from Drupal's renderer for Render arrays. We should implement that logic converted to JavaScript as best as possible.
{ null|render }} renders to ''
{{ true|render }} renders to '1'
{{ false|render }} renders to ''
{{ 0.0|render }} renders to '0'
{{ ['string1', 'string2']|render }} renders to '' (and prints some errors)
We should also render Symbol data like strings and BigInt data like numbers.
TypeScript docs on authoring and publishing definition files: https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html
The type
argument of the format_date
filter uses Drupal's named date formats.
By default, Drupal core provides these named formats:
short
: m/d/Y - H:i
medium
: D, m/d/Y - H:i
long
: l, F j, Y - H:i
html_datetime
: Y-m-d\TH:i:sO
html_date
: Y-m-d
html_time
: H:i:s
html_yearless_date
: m-d
html_week
: Y-\WW
html_month
: Y-m
html_year
: Y
fallback
: D, m/d/Y - H:i
Users should be able to override these defaults.
The end of Node.js v12 long-term support is May 1, 2022. It's the last LTS node that doesn't support the new ES module support (without an experimental flag).
We are currently distributing this packages as CommonJS files (compiled by Babel). To make distributing a dual CJS/ESM as easy as possible we should follow Approach 2 of “Writing dual packages while avoiding or minimizing hazards”. If we compile our ES .js files to .cjs files and set the default type
to module
, our default export will be a .cjs
file and I'm not sure that Node.js v12 can import a .cjs
file (without the experimental flag). But with its LTS almost over, it is simpler to just drop support for it a little early.
The active_theme
function should return the active theme name if we add a active_theme
configuration during setup.
See #9 for details.
Problem:
Impossible to import drupal functions only:
import functions from 'drupal-twig-extensions/functions/twig';
because export path linked to filters
directory.
https://github.com/JohnAlbin/drupal-twig-extensions/blob/main/package.json#L46
Drupal-specific Twig tags are not implemented in Twig.js.
Since the API of Twig.js and Twing differ so much, it is doubtful the same implementation can be shared. See #7 for Twing.
The official list of tags is in Drupal 9’s code.
trans
, plural
, and endtrans
: These tags are described in Drupal’s “Translation in Twig templates” documentation.Was considering making a dump()
implementation that would output to the console. While dump()
is adequite for simple debugging, leverage the console could speed up development...
Twig.extendFunction('vardumper', function(...args) {
if (args.length === 0) {
console.log(Twig.functions.dump(this.context))
}
else {
console.log(Twig.functions.dump(...args))
}
});
Another option is to output directly to the browser's console too...
let output = Twig.functions.dump(this.context);
return '<script>console.log(' + output + ');</script>';
console.dir()
could be a good alternative to something like kint: https://developer.mozilla.org/en-US/docs/Web/API/console/dir
Since Twing has no mechanism for unregistering extensions, and since attempting to register an extension with the same name as one that's already been registered throws an error, it would be nice to be able to specify extensions that you don't want drupal-twig-extensions
to register.
For example, right now the create_attribute()
function that this package registers isn't useful in Twing, and consequently I'd like to register my own custom create_attribute()
. But because of the limitations mentioned above, I find myself stuck: once this package registers create_attribute()
, I can't unregister it, and I can't supply an override. If I could instruct drupal-twig-extensions
not to register create_attribute()
, I could get around this problem.
I envision something like:
addDrupalExtensions(twigOrTwingEnvironment, { exclude: ['create_attribute'] })
Currently, the placeholder
is just a pass-through filter. We should implement it the way Drupal does.
From the Drupal docs on escapePlaceholder, the placeholder
filter should return:
<em class="placeholder">{{ string|drupal_escape }}</em>
Fortunately, the drupal_escape
filter is already implemented, so this should be easy.
AVA 4.0.0-rc.1 is out now. https://github.com/avajs/ava/releases/tag/v4.0.0-rc.1
We should upgrade our tests.
Because AVA 4 drops support for Babel transpiling of test files, we'll need to use Node.js built-in ES Module support. So this ticket depends on #20 and #22
So after completing #22, we learned that a webpack 4 loader fails to load this package in #43. This is a separate bug from #37.
While this package should work fine with a webpack 5 loader, it's a relatively easy work-around to transpile our ES Module code to CommonJS to get it to work with Webpack 4.
main
, modules
and files
entries in package.jsonThe without
filter fails when used in Twing. The entire object being filtered is replaced with an empty string.
The test in tests/Twing/filters/without.js
reports:
✖ Twing › filters › without › should remove the given element from an object
─
Twing › filters › without › should remove the given element from an object
Difference:
- 'No author: '
+ 'No author: You can only find truth with logic if you have already found truth without it.1874-1936'
› renderTemplateMacro (tests/fixtures/twing-helpers.js:24:5)
Judging by this test.failing()
, I figure this is a known problem, but I didn't see an issue for it: the object returned by a create_attribute()
call doesn't have working methods.
I do have an example of a DrupalAttribute
class with methods that work in Twing (it just adds a compatibility layer between Twing and the drupal-attribute
npm package): https://github.com/MichaelAllenWarner/dte-storybook/blob/drupal-attribute/DrupalAttribute/index.js
Hello, and thanks for the lovely module.
I'm working with this module, drupal-attributes, twing, and storybook... but I'm having the darndest time getting attributes.addClass('foo') to print at all. I'm assuming the problem is how I'm calling drupal-attribute... I found this old comment here: ericmorand/drupal-attribute#12 (comment) that mentions the glue to make it work also exists in this module now.
relevant bits of package.json (initially ran npx sb init -t html
)
"@storybook/html": "^6.4.19",
"drupal-attribute": "^1.0.2",
"drupal-twig-extensions": "^1.0.0-beta.4",
"twing": "^5.1.0",
"twing-loader": "^4.0.0"
main.js
webpackFinal: async config => {
// add twig support to storybook
config.module.rules.push({
test: /\.twig/,
use: [
{
loader: 'twing-loader',
options: {
environmentModulePath: path.resolve(__dirname, 'twing-environment.js'),
},
},
],
include: path.resolve(__dirname, '..', 'components'),
});
return config;
},
twing-environment.js
const { TwingEnvironment, TwingLoaderRelativeFilesystem } = require('twing');
const { addDrupalExtensions } = require('drupal-twig-extensions/twing');
const twing = new TwingEnvironment(
new TwingLoaderRelativeFilesystem()
);
addDrupalExtensions(twing);
module.exports = twing;
block.stories.js
import block from './block.twig';
import drupalAttribute from 'drupal-attribute'
import './block.css';
import './block.js';
export default { title: 'Blocks' };
export const Block = (_, { loaded: { component } }) => component;
Block.args = {
attributes: new drupalAttribute(),
title_attributes: new drupalAttribute(),
plugin_id: "Some plugin",
title_prefix: "",
title_suffix: "",
label: "I'm a block!",
content: "Lorem ipsum dolor sit amet.",
configuration: {
provider: "Some module"
}
}
Block.render = async args => {
return await block({
...Block.args
})
}
block.twig (identical to block.html.twig in drupal)
{%
set classes = [
'block',
'block-' ~ configuration.provider|clean_class,
'block-' ~ plugin_id|clean_class,
]
%}
<div{{ attributes.addClass(classes) }}>
{{ title_prefix }}
{% if label %}
<h2{{ title_attributes }}>{{ label }}</h2>
{% endif %}
{{ title_suffix }}
{% block content %}
{{ content }}
{% endblock %}
</div>
output is:
<div>
<h2>I'm a block!</h2>
Lorem ipsum dolor sit amet.
</div>
If I change how I create the attributes in Block.args
, to use Attribute
from import Attribute from 'drupal-twig-extensions/twing'
instead of drupalAttributes
, I get a constructor error. Without the constructor, nothing happens... I feel like the solution's right there, but I'm missing something obvious. Any pointers or existing documentation I've missed would be greatly appreciated.
Block.args = {
attributes: new Attribute(),
title_attributes: new Attribute(),
Block.args = {
attributes: Attribute,
title_attributes: Attribute,
The clean_id
filter doesn't return unique IDs like it does in Drupal.
Care should be made to ensure that a page load in Storybook doesn't cause a unique ID on a page doesn't change just because the same ID is used on another pge.
The failing tests report:
✖ Twig.js › filters › clean_id › should make a repeated ID unique
✖ Twing › filters › clean_id › should make a repeated ID unique
─
Twing › filters › clean_id › should make a repeated ID unique
tests/Twing/filters/clean_id.js:45
44: actual = await compiledTemplate.render(data);
45: t.not(actual, expected);
46: });
Value is the same as:
'test-unique-id'
› tests/Twing/filters/clean_id.js:45:5
According to the Drupal docs for the without filter:
The string keys of $element to prevent printing. Arguments can include
string keys directly, or arrays of string keys to hide.
Right now, we only handle direct string keys, but not arrays of strings.
e.g., this is a valid way to call without
:
{{ render_array|without( 'key1', [ 'key2', 'key3' ], 'key4' ) }}
Given the locutus package implements many PHP functions, I bet this is easily fixed.
Locutus implementation: https://github.com/locutusjs/locutus/blob/master/src/php/datetime/date.js
PHP docs on date formatting: https://www.php.net/manual/en/datetime.format.php
It's impossible to fully implement Drupal's render system, but we shouldn't discount what our users are capable of.
Whatever renderer->render()
logic we implement, our users might want to do something different. So users should be able to override the default render function that we write with:
addDrupalExtensions(twigOrTwing, {
render: someCustomFunction,
});
We are currently distributing this packages as CommonJS files (compiled by Babel). To make distributing a dual CJS/ESM as easy as possible we should follow Approach 2 of “Writing dual packages while avoiding or minimizing hazards”.
package.json
to add a "type": "module",
.dist
folder to commonjs
to make it obvious which files are which.main
entry to point at dist/index.cjs
.exports
in our package.json
to support CJS and ESM.The current implementation of Attribute uses a JavaScript Map internally, which is an excellent choice.
But the constructor of Attribute just re-uses the Map constructor, so usage is:
const attr = new Attribute([['id', 'val'], ['class', ['one', 'two']]]);
or in a Twig file:
{% set attr = create_attribute([['id', 'val'], ['class', ['one', 'two']]]) %}
See the Map constructor docs:
Parameters
iterable (Optional)
An Array or other iterable object whose elements are key-value pairs. (For example, arrays with two elements, such as[[ 1, 'one' ],[ 2, 'two' ]]
.) Each key-value pair is added to the new Map.
But that doesn't match the syntax we'd use inside Drupal twig files because Twig associative arrays use an object-like syntax:
{% set attr = create_attribute({ id: "val", class: ["one", "two"]}) %}
I've implemented a work-around inside the create_attribute()
Twig function.
The fault is either in:
We either need to provide a patch upstream or fork drupal-attribute
into this project.
Docs for Drupal's Attribute object are here: https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Template%21Attribute.php/class/Attribute/9.1.x
Right now, we are doing a shallow clone (spread operator) of the element it filters. But the PHP version of Drupal's without
filter is doing a clone
of the element which is a deep clone.
This won't cause issues in 99% of the cases, but modifying a child element after |without
is run, will change the original element's child too.
lodash has a cloneDeep()
method.
Tracking this bug: bcoe/c8#318
The clean_class implementation looks as if it was written to cache the results of cleaning the string.
From cab7ef3:
module.exports = function (string) {
var classes = {}
var identifier = String(string)
return (function () {
if (!Object.prototype.hasOwnProperty.call(classes, identifier)) {
classes[identifier] = cleanCssIdentifier(identifier.toLowerCase())
}
return classes[identifier]
})()
}
Unfortunately, the classes
cache is initialized on every call to the clean_class
filter.
We can fix this by moving the cache initialization out of the function and into the surrounding module file. Creating an anonymous function on each call is also superfluous.
We should be running eslint as part of our test suite.
Several of our Twig functions could return configured values if we added options to set them during setup.
With these config options:
date_format
: defaults to the list of Drupal's default date formatsactive_theme
: defaults to stark
, the minimal install profile's default themeactive_theme_path
: defaults to core/themes/[active_theme]
for core themes and themes/custom/[active_theme]
for other active themesstreamWrapper
: defaults to {'public://': 'sites/default/files', 'private://': 'sites/default/private', 'temporary://': 'sites/default/tmp'}
base_url
: defaults to /
host
: defaults to https://example.com
; should include scheme, host, and portrouting
: defaults to { '<front>': '/', '<current>': '<current>', '<none>': '' }
; a list of routes that map to pathsThese functions:
format_date
: formats dates using the patterns in date_format
active_theme
: returns active_theme
nameactive_theme_path
: returns active_theme_path
file_url
: prepends base_url
and uses the replacement pattern in streamWrapper
configpath
: prepends base_url
and uses the replacement pattern in routing
configurl
: prepends the host
and base_url
and uses the replacement pattern in routing
configThe Drupal version of trans
(t
) filter accepts parameters for replacement in the original string. For example:
{{ 'Expected waiting time is @time minutes'|t({ '@time': time }) }}
The trans
filter will replace the @time
characters from the original string with the value of the time
variable.
In the current implementation of this project, trans
is implemented as a pass-through; it returns the original string without change. We should update it to handle replacements for:
@variable
%variable
:variable
From the Drupal docs on how to handle the replacements: https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Component%21Render%21FormattableMarkup.php/function/FormattableMarkup%3A%3AplaceholderFormat/9.2.x
@variable
: When the placeholder replacement value is:
- A string, the replaced value in the returned string will be sanitized using \Drupal\Component\Utility\Html::escape().
- A MarkupInterface object, the replaced value in the returned string will not be sanitized.
A MarkupInterface object cast to a string, the replaced value in the returned string be forcibly sanitized using \Drupal\Component\Utility\Html::escape().Doesn't apply to JS implementations of Twig?Use this placeholder as the default choice for anything displayed on the site, but not within HTML attributes, JavaScript, or CSS. Doing so is a security risk.
%variable
: Use when the replacement value is to be wrapped in<em>
tags. A call like:
{{ '%output_text'|trans({ '%output_text': 'text output here.' }) }}
makes the following HTML code:
<em class="placeholder">text output here.</em>
As with
@variable
, do not use this within HTML attributes, JavaScript, or CSS. Doing so is a security risk.
:variable
: Return value is escaped with \Drupal\Component\Utility\Html::escape() and filtered for dangerous protocols usingUrlHelper::stripDangerousProtocols()
.
This issue depends on #15, "Add placeholder filter implementation".
Currently the render
filter just returns the given input, but it should always return a string.
While implementing Drupal's Render API in JavaScript would be… a very bad idea, we can easily use JS's toString()
prototype method to coerce any input to a string.
Then the developer would be responsible for how the render
filter should convert a JS object into a string. We could even provide a render
configuration option that takes a JS function.
The default render
function would just be: (renderArray) => renderArray.toString()
In PHP Twig, the join
filter can be used to join the values of a PHP object.
{{ { key1: 'value1', key2: 'value2' }|join(', ') }}
{# Outputs: value1, value2 #}
This works fine in Twig.js, but not in Twing. Since we are re-using the Twig.js/Twing join filters for safe_join, this means we inherit the bugs in Twing's join filter.
We already have a test in our test suite that fails for this Twing test case.
Most of the filters and functions are not tested, but should be.
The following filters
need tests:
The following functions
need tests:
We are currently distributing this packages as CommonJS files (compiled by Babel). Since we dropped support for Node.js v12 in #20, Node.js v14 and later support native ES Modules and we don't need to distribute CommonJS or use Babel.
"type": "module"
.main
entry to point at lib/index.js
.exports
in our package.json
to remove the lib
part of the path.Follow-ups:
Currently, the functions
directory is split into shared
, twing
and twig
folders. But it should be organized by the actual Twig function with twig.js
and twing.js
files inside each folder, if needed.
Same goes for the filters
directory.
To support protocol relative URLs, a slash is added after the replacement but it introduces multiple issues.
When the baseUrl is defined to a domain, it adds an unecessary slash.
When the baseUrl is not defined, it adds an unecessary leading slash.
Note that //image/dummy.png
is not a valid protocal-relative URL since there is no domain, a correct URL would be//example.test/image/dummy.png
(Following up on our conversation here.)
I've created a repo demonstrating the problem.
The master branch uses the alpha build of drupal-twig-extensions
and works fine (clone repo, do npm install
, and then npm run storybook
).
The dte-beta
branch uses the beta-3 build of drupal-twig-extensions
and doesn't work. Here is the ESM-ified twing-environment.js
file for that branch, with some comments explaining the situation.
I'm not sure exactly what Storybook and Webpack are doing under the hood here, but so far the only solution I've found is sticking with the alpha build.
drupal-twig-extensions 1.0.0-alpha.6 rendered undefined
to an empty string. 1.0.0-beta.1 renders undefined
to the string undefined
.
Drupal renders PHP null
to an empty string and PHP doesn't have an undefined
, so we should make undefined
render to an empty string.
The path
and url
functions should return configured values if we add options to set them during setup.
See #9 for details.
Twig filters and functions are defined with options. You can see those options in Drupal definition files.
But none of those options are passed to Twig.js or Twing.
Digging into Twig.js, I see it has no concept of options on Twig functions or filters. But Twing does.
Tracking this Twing bug in its join
filter: https://gitlab.com/nightlycommit/twing/-/issues/544
When the join
bug is fixed upstream, we should remove the work-around in filters/safe_join/twing.js
that we added with #44.
The drupal_escape
filter fails when used in Twing. The entire object being filtered is replaced with an empty string.
The test in tests/Twing/filters/drupal_escape.js
reports:
Twing › filters › drupal_escape › should escape ampersands in a string
Difference:
- ''
+ 'Bonnie & Clyde & Luna'
› renderTemplateMacro (tests/fixtures/twing-helpers.js:24:5)
Those functions currently return null, but should return a string.
Trying to use this package with Webpack 4 produces two errors:
exports
entry of package.json
, so it needs the old-school entry points in the project root.??
), it errors out with ModuleParseError: Module parse failed: Unexpected token
.A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.