From 3ecb343750ecc7a3a1c7a897c3ef072cc54247a0 Mon Sep 17 00:00:00 2001 From: catchonme Date: Sun, 5 May 2019 10:49:20 +0800 Subject: [PATCH 1/3] add audio component --- scss/_variables.scss | 72 +++++++-- scss/components/_audio.scss | 143 +++++++++++++++++ scss/themes/cxd.scss | 1 + scss/themes/default.scss | 1 + src/components/icons.tsx | 6 +- src/index.tsx | 1 + src/renderers/Audio.tsx | 301 ++++++++++++++++++++++++++++++++++++ 7 files changed, 512 insertions(+), 13 deletions(-) create mode 100644 scss/components/_audio.scss create mode 100644 src/renderers/Audio.tsx diff --git a/scss/_variables.scss b/scss/_variables.scss index 9e030b0b9..df6f5bbb8 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -1178,15 +1178,63 @@ $Tree-folderIconContent: "\f07b" !default; $Tree-itemText--onChecked-color: $Form-selectValue-color !default; // IconPicker -$IconPicker-tabs-bg: #F0F3F4; -$IconPicker-tab-padding: 0 px2rem(5px); -$IconPicker-tab-height: px2rem(30px); -$IconPicker-tab-lineHeight: px2rem(30px); -$IconPicker-tab-onActive-bg: $white; -$IconPicker-content-maxHeight: px2rem(350px); -$IconPicker-singleVendor-padding: px2rem(5px) 0 px2rem(5px) px2rem(13px); -$IconPicker-multiVendor-padding: px2rem(35px) 0 px2rem(5px) px2rem(13px); -$IconPicker-sugItem-width: px2rem(28px); -$IconPicker-sugItem-height: px2rem(28px); -$IconPicker-sugItem-lineHeight: px2rem(28px); -$IconPicker-selectedIcon-marginRight: px2rem(5px); \ No newline at end of file +$IconPicker-tabs-bg: #F0F3F4 !default; +$IconPicker-tab-padding: 0 px2rem(5px) !default; +$IconPicker-tab-height: px2rem(30px) !default; +$IconPicker-tab-lineHeight: px2rem(30px) !default; +$IconPicker-tab-onActive-bg: $white !default; +$IconPicker-content-maxHeight: px2rem(350px) !default; +$IconPicker-singleVendor-padding: px2rem(5px) 0 px2rem(5px) px2rem(13px) !default; +$IconPicker-multiVendor-padding: px2rem(35px) 0 px2rem(5px) px2rem(13px) !default; +$IconPicker-sugItem-width: px2rem(28px) !default; +$IconPicker-sugItem-height: px2rem(28px) !default; +$IconPicker-sugItem-lineHeight: px2rem(28px) !default; +$IconPicker-selectedIcon-marginRight: px2rem(5px) !default; + +// Audio +$Audio-width: px2rem(300px) !default; +$Audio-height: px2rem(50px) !default; +$Audio-lineHeight: px2rem(50px) !default; +$Audio-border: px2rem(1px) solid #dee2e6 !default; +$Audio-rate-padding: 0 px2rem(5px) !default; +$Audio-rate-width: px2rem(40px) !default; +$Audio-rate-height: px2rem(50px) !default; +$Audio-rate-lineHeight: px2rem(50px) !default; +$Audio-rate-bg: #dee2e6 !default; +$Audio-rateControlItem-padding: px2rem(16px) px2rem(7px) !default; +$Audio-rateControlItem-bg: #dee2e6 !default; +$Audio-rateControlItem-borderRight: px2rem(1px) solid #d3dae0 !default; +$Audio-play-width: px2rem(25px) !default; +$Audio-play-top: px2rem(5px) !default; +$Audio-play-marginLeft: px2rem(10px) !default; +$Audio-times-width: px2rem(80px) !default; +$Audio-times-margin: 0 px2rem(10px) !default; +$Audio-process-width: px2rem(80px) !default; +$Audio-volume-width: px2rem(22px) !default; +$Audio-volume-height: px2rem(22px) !default; +$Audio-volume-lineHeight: px2rem(30px) !default; +$Audio-volume-borderRadius: px2rem(15px) !default; +$Audio-volume-top: px2rem(5px) !default; +$Audio-volume-left: px2rem(10px) !default; +$Audio-volumeControl-width: px2rem(130px) !default; +$Audio-volumeControl-height: px2rem(30px) !default; +$Audio-volumeControl-lineHeight: px2rem(30px) !default; +$Audio-volumeControl-right: px2rem(-6px) !default; +$Audio-volumeControl-top: px2rem(-4px) !default; +$Audio-volumeControl-paddingLeft: px2rem(10px) !default; +$Audio-volumeControl-bg: #E6E7E9 !default; +$Audio-volumeControl-border: px2rem(2px) solid transparent !default; +$Audio-volumeControl-borderRadius: px2rem(10px) !default; +$Audio-input-width: px2rem(80px) !default; +$Audio-volumeControl-input-margin: px2rem(-3px) px2rem(10px) 0 0 !default; +$Audio-track-height: px2rem(6px) !default; +$Audio-track-bg: #d7dbdd !default; +$Audio-track-borderRadius: px2rem(3px) !default; +$Audio-track-border: px2rem(1px) solid transparent !default; +$Audio-thumb-width: px2rem(14px) !default; +$Audio-thumb-height: px2rem(14px) !default; +$Audio-thumb-bg: #606670 !default; +$Audio-thumb-marginTop: px2rem(-5px) !default; +$Audio-svg-width: px2rem(20px) !default; +$Audio-svg-height: px2rem(20px) !default; +$Audio-svg-top: px2rem(4px) !default; \ No newline at end of file diff --git a/scss/components/_audio.scss b/scss/components/_audio.scss new file mode 100644 index 000000000..a9b9b3258 --- /dev/null +++ b/scss/components/_audio.scss @@ -0,0 +1,143 @@ +@mixin input-range { + width: $Audio-input-width; + display: inline-block; + -webkit-appearance: none; + vertical-align: middle; + outline: none; + border: none; + padding: 0; + background: none; + + &::-webkit-slider-runnable-track { + background-color: $Audio-track-bg; + height: $Audio-track-height; + border-radius: $Audio-track-borderRadius; + border: $Audio-track-border; + } + + &::-webkit-slider-thumb { + -webkit-appearance: none !important; + border-radius: 100%; + cursor: pointer; + background: $Audio-thumb-bg; + height: $Audio-thumb-width; + width: $Audio-thumb-height; + margin-top: $Audio-thumb-marginTop; + } +} + +@mixin svg { + width: $Audio-svg-width; + height: $Audio-svg-height; +} + +.#{$ns}Audio-original { + display: none; +} + +.#{$ns}Audio { + width: $Audio-width; + height: $Audio-height; + line-height: $Audio-lineHeight; + border: $Audio-border; + &-rates { + display: inline-block; + position: relative; + + .#{$ns}Audio-rate { + display: block; + padding: $Audio-rate-padding; + width: $Audio-rate-width; + height: $Audio-rate-height; + line-height: $Audio-rate-lineHeight; + text-align: center; + background: $Audio-rate-bg; + cursor: pointer; + } + + .#{$ns}Audio-rateControl { + position: absolute; + top: 0; + left: 0; + z-index: 1; + + .#{$ns}Audio-rateControlItem { + padding: $Audio-rateControlItem-padding; + background: $Audio-rateControlItem-bg; + cursor: pointer; + user-select: none; + border-right: $Audio-rateControlItem-borderRight; + } + } + } + &-play { + display: inline-block; + width: $Audio-play-width; + position: relative; + top: $Audio-play-top; + cursor: pointer; + margin-left: $Audio-play-marginLeft; + + svg { + @include svg(); + } + } + &-times { + display: inline-block; + width: $Audio-times-width; + margin: $Audio-times-margin; + cursor: default; + } + &-process { + display: inline-block; + width: $Audio-process-width; + cursor: pointer; + + input[type=range] { + @include input-range(); + } + } + &-volume { + display: inline-block; + width: $Audio-volume-width; + height: $Audio-volume-height; + line-height: $Audio-volume-lineHeight; + border-radius: $Audio-volume-borderRadius; + position: relative; + top: $Audio-volume-top; + left: $Audio-volume-left; + cursor: pointer; + + svg { + @include svg(); + } + + .#{$ns}Audio-volumeControl { + position: absolute; + right: $Audio-volumeControl-right; + top: $Audio-volumeControl-top; + padding-left: $Audio-volumeControl-paddingLeft; + height: $Audio-volumeControl-height; + line-height: $Audio-volumeControl-lineHeight; + border-radius: $Audio-volumeControl-borderRadius; + background: $Audio-volumeControl-bg; + border: $Audio-volumeControl-border; + width: $Audio-volumeControl-width; + input[type=range] { + margin: $Audio-volumeControl-input-margin; + @include input-range(); + } + + .#{$ns}Audio-volumeControlIcon { + display: inline-block; + cursor: pointer; + } + + svg { + position: relative; + top: $Audio-svg-top; + @include svg(); + } + } + } +} \ No newline at end of file diff --git a/scss/themes/cxd.scss b/scss/themes/cxd.scss index 102bc64f8..c4f1d06fe 100644 --- a/scss/themes/cxd.scss +++ b/scss/themes/cxd.scss @@ -413,6 +413,7 @@ $TagControl-sugTip-color: $primary; @import "../components/remark"; @import "../components/chart"; @import "../components/video"; +@import "../components/audio"; @import "../components/panel"; @import "../components/service"; @import "../components/spinner"; diff --git a/scss/themes/default.scss b/scss/themes/default.scss index c0fc54eb8..0c6b81165 100644 --- a/scss/themes/default.scss +++ b/scss/themes/default.scss @@ -39,6 +39,7 @@ $Form-input-borderColor: #cfdadd; @import "../components/remark"; @import "../components/chart"; @import "../components/video"; +@import "../components/audio"; @import "../components/panel"; @import "../components/service"; @import "../components/spinner"; diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 95566d5b7..b95948fa8 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -2,4 +2,8 @@ import * as React from 'react'; export const closeIcon = (); export const unDoIcon = (); export const reDoIcon = () -export const enterIcon = () \ No newline at end of file +export const enterIcon = () +export const volumeIcon = () +export const muteIcon = () +export const playIcon = () +export const pauseIcon = () \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 01cfc52fb..9b84e2157 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -141,6 +141,7 @@ import './renderers/Chart'; import './renderers/Container'; import './renderers/Service'; import './renderers/Video'; +import './renderers/Audio'; import './renderers/Nav'; import './renderers/Tasks'; import './renderers/Drawer'; diff --git a/src/renderers/Audio.tsx b/src/renderers/Audio.tsx new file mode 100644 index 000000000..8a9c24918 --- /dev/null +++ b/src/renderers/Audio.tsx @@ -0,0 +1,301 @@ +import * as React from 'react'; +import { + Renderer, + RendererProps +} from '../factory'; +import { autobind } from '../utils/helper'; +import { volumeIcon, muteIcon, playIcon, pauseIcon} from '../components/icons'; + +export interface AudioProps extends RendererProps { + className?: string; + src?: string, + autoPlay?: boolean, + loop?: boolean, + rates?: number[] +} + +export interface AudioState { + isReady?: boolean, + muted?: boolean, + playing?: boolean, + played: number, + seeking?: boolean, + volume: number, + prevVolume: number, + loaded?: number, + playbackRate: number, + showHandlePlaybackRate: boolean, + showHandleVolume: boolean +} + +export class Audio extends React.Component { + audio: any; + progressTimeout: any; + + static defaultProps:Partial = { + autoPlay: false, + playbackRate: 1, + loop: false, + rates: [1.0, 2.0, 4.0], + progressInterval: 1000 + }; + + state:AudioState = { + isReady: false, + muted: false, + playing: false, + played: 0, + seeking: false, + volume: 0.8, + prevVolume: 0.8, + loaded: 0, + playbackRate: 1.0, + showHandlePlaybackRate: false, + showHandleVolume: false + } + + componentWillUnmount () { + clearTimeout(this.progressTimeout); + } + + componentDidMount() { + const autoPlay = this.props.autoPlay; + const playing = autoPlay ? true : false; + this.setState({ + playing: playing + }, this.progress); + } + + @autobind + progress() { + clearTimeout(this.progressTimeout); + if (this.props.src && this.audio) { + const currentTime = this.audio.currentTime || 0; + const duration = this.audio.duration; + const played = currentTime / duration; + let playing = this.state.playing; + playing = (played != 1 && playing) ? true : false; + this.setState({ + played, + playing + }); + this.progressTimeout = setTimeout(this.progress, (this.props.progressInterval / this.state.playbackRate)) + } + } + + @autobind + audioRef(audio:any) { + this.audio = audio; + } + + @autobind + load() { + this.setState({ + isReady: true + }); + } + + @autobind + handlePlaybackRate(rate:number) { + this.audio.playbackRate = rate; + this.setState({ + playbackRate: rate, + showHandlePlaybackRate: false + }); + } + + @autobind + handleMute() { + const {muted, prevVolume} = this.state; + const curVolume = !muted ? 0 : prevVolume; + this.audio.muted = !muted; + this.setState({ + muted: !muted, + volume: curVolume + }); + } + + @autobind + handlePlaying() { + let playing = this.state.playing; + playing ? this.audio.pause() : this.audio.play(); + this.setState({ + playing: !playing + }); + } + + @autobind + getCurrentTime() { + if (!this.audio || !this.state.isReady) return 0; + const duration = this.audio.duration; + const played = this.state.played; + return this.formatTime(duration * (played || 0)); + } + + @autobind + getDuration() { + if (!this.audio || !this.state.isReady) return 0; + const { duration, seekable } = this.audio; + // on iOS, live streams return Infinity for the duration + // so instead we use the end of the seekable timerange + if (duration === Infinity && seekable.length > 0) { + return seekable.end(seekable.length - 1) + } + return this.formatTime(duration); + } + + @autobind + onSeekChange(e:any) { + const played = e.target.value; + this.setState({ played: played }); + } + + @autobind + onSeekMouseDown() { + this.setState({ seeking: true }); + } + + @autobind + onSeekMouseUp(e:any) { + if (!this.state.seeking) return; + const played = e.target.value; + const duration = this.audio.duration; + this.audio.currentTime = duration * played; + + const loop = this.props.loop; + let playing = this.state.playing; + playing = (played < 1 || loop) ? playing : false; + this.setState({ + playing: playing, + seeking: false + }); + } + + @autobind + setVolume(e:any) { + const volume = e.target.value; + this.audio.volume = volume; + this.setState({ + volume: volume, + prevVolume: volume + }); + } + + @autobind + formatTime(seconds:number) { + const date = new Date(seconds * 1000); + const hh = date.getUTCHours(); + const mm = date.getUTCMinutes(); + const ss = this.pad(date.getUTCSeconds()); + if (hh) { + return `${hh}:${this.pad(mm)}:${ss}`; + } + return `${mm}:${ss}`; + } + + @autobind + pad(string:number) { + return ('0' + string).slice(-2) + } + + @autobind + toggleHandlePlaybackRate() { + this.setState({ + showHandlePlaybackRate: !this.state.showHandlePlaybackRate + }); + } + + @autobind + toggleHandleVolume(type:boolean) { + this.setState({ + showHandleVolume: type + }); + } + + render() { + const { + className, + src, + autoPlay, + loop, + rates, + classnames: cx + } = this.props; + const { + playing, + played, + volume, + muted, + isReady, + playbackRate, + showHandlePlaybackRate, + showHandleVolume + } = this.state; + + return ( +
+ + {isReady ?
+ {rates ?
+
+ x{playbackRate.toFixed(1)} +
+ {showHandlePlaybackRate ?
+ {rates.map((rate, index) => + this.handlePlaybackRate(rate)}> + x{rate.toFixed(1)} + + )}
: null} +
+ : null } +
+ {playing ? pauseIcon : playIcon} +
+
{this.getCurrentTime()} / {this.getDuration()}
+
+ +
+
this.toggleHandleVolume(true)} + onMouseLeave={() => this.toggleHandleVolume(false)}> + {showHandleVolume ? +
+ +
+ {volume > 0 ? volumeIcon : muteIcon} +
+ : volume > 0 ? volumeIcon : muteIcon} +
+
: null} +
+ ); + } +} + +@Renderer({ + test: /(^|\/)audio/, + name: 'audio' +}) +export class AudioRenderer extends Audio {}; From 8221c638f84db20b675dceddedb4677af1607b3c Mon Sep 17 00:00:00 2001 From: catchonme Date: Sun, 5 May 2019 14:45:12 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BC=98=E5=8C=96=20audio=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/renderers.md | 32 +++++++++++++++++++++++++++----- scss/components/_audio.scss | 5 +++++ src/renderers/Audio.tsx | 23 +++++++++++++---------- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/docs/renderers.md b/docs/renderers.md index b5ec825d0..77f13625c 100644 --- a/docs/renderers.md +++ b/docs/renderers.md @@ -77,7 +77,7 @@ Json 配置最外层是一个 `Page` 渲染器。他主要包含标题,副标 |---|---|---|---| | type | `string` | | `"form"` 指定为 Form 渲染器 | | mode | `string` | `normal` | 表单展示方式,可以是:`normal`、`horizontal` 或者 `inline` [示例](/docs/demo/forms/mode) | -| horizontal | `Object` | `{"left":"col-sm-2", "right":"col-sm-10", "offset":"col-sm-offset-2"}` | 当 mode 为 `horizontal` 时有用,用来控制 label | +| horizontal | `Object` | `{"left":"col-sm-2", "right":"col-sm-10", "offset":"col-sm-offset-2"}` | 当 mode 为 `horizontal` 时有用,用来控制 label | | title | `string` | `"表单"` | Form 的标题 | | submitText | `String` | `"提交"` | 默认的提交按钮名称,如果设置成空,则可以把默认按钮去掉。 | | className | `string` | | 外层 Dom 的类名 | @@ -2925,7 +2925,7 @@ Action 是一种特殊的渲染器,它本身是一个按钮,同时它能触 "link": "/docs/index" } } - ``` + ``` * `url` 当按钮点击后,新窗口打开指定页面。 ```schema:height="200" @@ -2938,7 +2938,7 @@ Action 是一种特殊的渲染器,它本身是一个按钮,同时它能触 "url": "raw:http://www.baidu.com" } } - ``` + ``` * `dialog` 当按钮点击后,弹出一个对话框。 关于 dialog 配置,请查看 [Dialog 模型](#dialog)。 ```schema:height="200" @@ -2964,7 +2964,7 @@ Action 是一种特殊的渲染器,它本身是一个按钮,同时它能触 } } } - ``` + ``` * `drawer` 当按钮点击后,弹出一个抽出式对话框。 关于 drawer 配置,请查看 [Drawer 模型](#drawer)。 @@ -2991,7 +2991,7 @@ Action 是一种特殊的渲染器,它本身是一个按钮,同时它能触 } } } - ``` + ``` ## Dialog @@ -3465,6 +3465,28 @@ CRUD 支持三种模式:`table`、`cards`、`list`,默认为 `table`。 } ``` +## Audio + +音频播放器 + +|属性名 | 类型 | 默认值 | 说明 | +|---|---|---|---| +| type | `string` | `"audio"` | 指定为 audio 渲染器 | +| className | `string` | | 外层 Dom 的类名 | +| inline | `boolean` | true | 是否是内联模式 | +| src | `string` | | 音频地址 | +| loop | `boolean` | false | 是否循环播放 | +| autoPlay | `boolean` | false | 是否自动播放 | +| rates | `array` | `[1.0, 2.0, 4.0]` | 加速播放 | + +```schema:height="500" scope="body" +{ + "type": "audio", + "autoPlay": false, + "src": "" +} +``` + ## Video 视频播放器。 diff --git a/scss/components/_audio.scss b/scss/components/_audio.scss index a9b9b3258..3f5511f20 100644 --- a/scss/components/_audio.scss +++ b/scss/components/_audio.scss @@ -35,11 +35,16 @@ display: none; } +.#{$ns}Audio--inline { + display: inline; +} + .#{$ns}Audio { width: $Audio-width; height: $Audio-height; line-height: $Audio-lineHeight; border: $Audio-border; + display: inline-block; &-rates { display: inline-block; position: relative; diff --git a/src/renderers/Audio.tsx b/src/renderers/Audio.tsx index 8a9c24918..12b53e6e9 100644 --- a/src/renderers/Audio.tsx +++ b/src/renderers/Audio.tsx @@ -8,6 +8,7 @@ import { volumeIcon, muteIcon, playIcon, pauseIcon} from '../components/icons'; export interface AudioProps extends RendererProps { className?: string; + inline?: boolean, src?: string, autoPlay?: boolean, loop?: boolean, @@ -32,7 +33,8 @@ export class Audio extends React.Component { audio: any; progressTimeout: any; - static defaultProps:Partial = { + static defaultProps:Pick = { + inline: true, autoPlay: false, playbackRate: 1, loop: false, @@ -215,6 +217,7 @@ export class Audio extends React.Component { render() { const { className, + inline, src, autoPlay, loop, @@ -233,7 +236,7 @@ export class Audio extends React.Component { } = this.state; return ( -
+
- {isReady ?
- {rates ?
+ {isReady ? (
+ {rates ? (
x{playbackRate.toFixed(1)}
- {showHandlePlaybackRate ?
+ {showHandlePlaybackRate ? (
{rates.map((rate, index) => this.handlePlaybackRate(rate)}> x{rate.toFixed(1)} - )}
: null} -
+ )}
) : null} +
) : null }
{playing ? pauseIcon : playIcon} @@ -277,7 +280,7 @@ export class Audio extends React.Component { onMouseEnter={() => this.toggleHandleVolume(true)} onMouseLeave={() => this.toggleHandleVolume(false)}> {showHandleVolume ? -
+ (
{
{volume > 0 ? volumeIcon : muteIcon} -
+
) : volume > 0 ? volumeIcon : muteIcon}
-
: null} +
) : null}
); } From 9cece2197065004c4c3afe3c27825d95a040af78 Mon Sep 17 00:00:00 2001 From: liaoxuezhi Date: Sun, 5 May 2019 11:58:05 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20resolveVariable=20?= =?UTF-8?q?=E5=B5=8C=E5=A5=97=E7=94=A8=E6=B3=95=E9=97=AE=E9=A2=98=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=94=A8=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/utils/tpl-builtin.test.ts | 20 ++++++++++++++++++-- src/utils/tpl-builtin.ts | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/__tests__/utils/tpl-builtin.test.ts b/__tests__/utils/tpl-builtin.test.ts index 6a081dce6..61061c63e 100644 --- a/__tests__/utils/tpl-builtin.test.ts +++ b/__tests__/utils/tpl-builtin.test.ts @@ -1,7 +1,8 @@ import { prettyBytes, escapeHtml, - formatDuration + formatDuration, + resolveVariableAndFilter } from '../../src/utils/tpl-builtin'; test('tpl-builtin:prettyBytes', () => { @@ -18,5 +19,20 @@ test('tpl-builtin:formatDuration', () => { expect(formatDuration(1)).toEqual('1秒'); expect(formatDuration(61)).toEqual('1分1秒'); expect(formatDuration(233233)).toEqual('3天17时47分13秒'); +}) -}) \ No newline at end of file +test('tpl-bultin:resolveVariableAndFilter', () => { + const data = { + a: 1, + b: '2', + c: { + '1': 'first', + '2': 'second' + } + }; + + expect(resolveVariableAndFilter('${a}', data, '| raw')).toEqual(1); + expect(resolveVariableAndFilter('${b}', data, '| raw')).toEqual('2'); + expect(resolveVariableAndFilter('${c.${a}}', data, '| raw')).toEqual('first'); + expect(resolveVariableAndFilter('${c.${b}}', data, '| raw')).toEqual('second'); +}); \ No newline at end of file diff --git a/src/utils/tpl-builtin.ts b/src/utils/tpl-builtin.ts index ebf7f19e4..a5301bc27 100644 --- a/src/utils/tpl-builtin.ts +++ b/src/utils/tpl-builtin.ts @@ -312,7 +312,7 @@ export const resolveVariableAndFilter = ( return undefined; } - const m = /^(\\)?\$(?:([a-z0-9_.]+)|{([^}{]+)})$/i.exec(path); + const m = /^(\\)?\$(?:([a-z0-9_.]+)|{([\s\S]+)})$/i.exec(path); if (!m) { return undefined;