Giter Site home page Giter Site logo

johnalbin / drupal-twig-extensions Goto Github PK

View Code? Open in Web Editor NEW

This project forked from kalamuna/twig-drupal-filters

6.0 6.0 3.0 559 KB

JavaScript implementation of Drupal’s Twig extensions

License: Other

JavaScript 100.00%
drupal twig twig-extension twig-filter twig-functions twig-tag twigjs twing

drupal-twig-extensions's People

Contributors

fafnirical avatar greenkeeper[bot] avatar iberdinsky-skilld avatar jedihe avatar johnalbin avatar robloach avatar skippednote avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

drupal-twig-extensions's Issues

render filter should implement logic of TwigExtension::renderVar

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.

Add setup option for date formats

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.

Drop support for Node.js v12

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.

`dump()` to console?

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

Feature idea: option for opting out of specified extensions

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'] })

Add placeholder filter implementation

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.

Add dual support for CommonJS and ES Modules

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.

  • add Babel transpiling back
  • configure Babel to write to .cjs files
  • figure out how to get require()s to have their ".js" extensions re-written to ".cjs"
  • follow the "isolate state" pattern at https://nodejs.org/api/packages.html#approach-2-isolate-state
  • add conditional exports to package.json
  • update main, modules and files entries in package.json

Filter "without" fails in Twing

The 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)

In Twing, the object returned by `create_attribute()` doesn't have working methods

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

Support request ? for drupal attributes

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,

Filter "clean_id" should return unique IDs

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

Add setup options for render function

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,
});

Add dual support for CommonJS and ES Modules

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”.

  • Update package.json to add a "type": "module",.
  • Use Babel to transpile our ES .js files to .cjs files.
  • Rename our dist folder to commonjs to make it obvious which files are which.
  • Update our main entry to point at dist/index.cjs.
  • Add exports in our package.json to support CJS and ESM.
  • Drop support for Node.js v12. #20

Constructor of Attribute doesn't match Drupal core

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:

  • the Attribute constructor
  • the JS Twig implementation that treats the object-like Twig syntax like an object.

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

"without" filter should make deep clone of element

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.

Fix caching of clean_class filter results

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.

Meta: Add setup options for Drupal's paths, theme name and date formats

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 formats
  • active_theme: defaults to stark, the minimal install profile's default theme
  • active_theme_path: defaults to core/themes/[active_theme] for core themes and themes/custom/[active_theme] for other active themes
  • streamWrapper: 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 port
  • routing: defaults to { '<front>': '/', '<current>': '<current>', '<none>': '' }; a list of routes that map to paths

These functions:

  • format_date: formats dates using the patterns in date_format
  • active_theme: returns active_theme name
  • active_theme_path: returns active_theme_path
  • file_url: prepends base_url and uses the replacement pattern in streamWrapper config
  • path: prepends base_url and uses the replacement pattern in routing config
  • url: prepends the host and base_url and uses the replacement pattern in routing config

Add placeholder replacement to translation filter

The 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 using UrlHelper::stripDangerousProtocols().

This issue depends on #15, "Add placeholder filter implementation".

Render filter should return string

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()

safe_join filter in Twing should join object values

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.

Meta: missing test coverage

Most of the filters and functions are not tested, but should be.

The following filters need tests:

  • drupal_escape
  • format_date
  • placeholder
  • render
  • safe_join
  • t
  • trans

The following functions need tests:

  • active_theme
  • active_theme_path
  • attach_library
  • create_attribute
  • file_url
  • path
  • render_var
  • url

Convert JavaScript to native ES Modules

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.

  • Update package.json to add a "type": "module".
  • Convert import statements from file-extension-less references (supported by Node.js' CommonJS conventions) to fully-specified filename references supported by ES Modules.
  • Remove Babel.
  • Update our main entry to point at lib/index.js.
  • Add exports in our package.json to remove the lib part of the path.

Follow-ups:

Reorganize lib files

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.

Remove file_url replacement trailing slash

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.

  • Base Url: /
  • URL: stream://image/dummy.png
  • URL replaced: //image/dummy.png

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

Beta build not working in Storybook with Webpack 5

(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.

Render filter should convert null/undefined to empty string

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.

Add options to filter and function definitions

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.

Filter "drupal_escape" fails in Twing

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 &amp; Clyde &amp; Luna'

  › renderTemplateMacro (tests/fixtures/twing-helpers.js:24:5)

Enable Webpack 4 compatibility

Trying to use this package with Webpack 4 produces two errors:

  • It doesn't read the exports entry of package.json, so it needs the old-school entry points in the project root.
  • When it sees a nullish coalescing operator (??), it errors out with ModuleParseError: Module parse failed: Unexpected token.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.