1 |
|
2 | <script>
|
3 |
|
4 | import { has, isString } from 'lodash';
|
5 |
|
6 | const PREFIX = '%{';
|
7 | const SUFFIX = '}';
|
8 | const START_SUFFIX = 'Start';
|
9 | const END_SUFFIX = 'End';
|
10 | const PLACE_HOLDER_REGEX = new RegExp(`(${PREFIX}[a-z]+[\\w-]*[a-z0-9]+${SUFFIX})`, 'gi');
|
11 |
|
12 | function groupPlaceholdersByStartTag(placeholders = {}) {
|
13 | return Object.entries(placeholders).reduce((acc, [slotName, [startTag, endTag]]) => {
|
14 | acc[startTag] = { slotName, endTag };
|
15 | return acc;
|
16 | }, {});
|
17 | }
|
18 |
|
19 | function getPlaceholderDefinition(chunk, placeholdersByStartTag) {
|
20 | const tagName = chunk.slice(PREFIX.length, -SUFFIX.length);
|
21 |
|
22 | if (has(placeholdersByStartTag, tagName)) {
|
23 |
|
24 | return {
|
25 | ...placeholdersByStartTag[tagName],
|
26 | tagName,
|
27 | };
|
28 | }
|
29 |
|
30 | if (tagName.endsWith(START_SUFFIX)) {
|
31 |
|
32 | const slotName = tagName.slice(0, -START_SUFFIX.length);
|
33 |
|
34 | return {
|
35 | slotName,
|
36 | endTag: `${slotName}${END_SUFFIX}`,
|
37 | tagName,
|
38 | };
|
39 | }
|
40 |
|
41 | return {
|
42 | slotName: tagName,
|
43 | endTag: undefined,
|
44 | tagName,
|
45 | };
|
46 | }
|
47 |
|
48 | export default {
|
49 | functional: true,
|
50 | props: {
|
51 | /**
|
52 | * A translated string with named placeholders, e.g., "Written by %{author}".
|
53 | */
|
54 | message: {
|
55 | type: String,
|
56 | required: true,
|
57 | },
|
58 | /**
|
59 | * An object mapping slot names to custom start/end placeholders. Use this
|
60 | * to avoid changing an existing message, and in turn invalidating existing
|
61 | * translations, in the case it uses non-default placeholders.
|
62 | */
|
63 | placeholders: {
|
64 | type: Object,
|
65 | required: false,
|
66 | default: undefined,
|
67 | validator: (value) =>
|
68 | Object.values(value).every(
|
69 | (tagPair) => Array.isArray(tagPair) && tagPair.length === 2 && tagPair.every(isString)
|
70 | ),
|
71 | },
|
72 | },
|
73 | /**
|
74 | * Available slots are determined by the placeholders in the provided
|
75 | * message prop. For example, a message of "Written by %{author}" has
|
76 | * a slot called "author", and its content is used to replace "%{author}"
|
77 | * in the rendered output. When two placeholders indicate a start and an
|
78 | * end region in the message, e.g., "%{linkStart}foo%{linkEnd}", the common
|
79 | * base name can be used as a scoped slot, where the content between the
|
80 | * placeholders is passed via the `content` scoped slot prop.
|
81 | * @slot * (arbitrary)
|
82 | * @binding {string} content The content to place between start and end placeholders.
|
83 | */
|
84 | render(createElement, context) {
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 | let i = 0;
|
92 | const vnodes = [];
|
93 | const slots = context.scopedSlots;
|
94 | const chunks = context.props.message.split(PLACE_HOLDER_REGEX);
|
95 | const placeholdersByStartTag = groupPlaceholdersByStartTag(context.props.placeholders);
|
96 |
|
97 | while (i < chunks.length) {
|
98 | const chunk = chunks[i];
|
99 |
|
100 | i += 1;
|
101 |
|
102 | if (!PLACE_HOLDER_REGEX.test(chunk)) {
|
103 |
|
104 | vnodes.push(chunk);
|
105 | continue;
|
106 | }
|
107 |
|
108 | const { slotName, endTag, tagName } = getPlaceholderDefinition(chunk, placeholdersByStartTag);
|
109 |
|
110 | if (endTag) {
|
111 |
|
112 | const indexOfEnd = chunks.indexOf(`${PREFIX}${endTag}${SUFFIX}`, i);
|
113 | if (indexOfEnd > -1) {
|
114 |
|
115 |
|
116 | const content = chunks.slice(i, indexOfEnd);
|
117 | i = indexOfEnd + 1;
|
118 |
|
119 | if (!has(slots, slotName)) {
|
120 |
|
121 | vnodes.push(chunk, ...content, chunks[indexOfEnd]);
|
122 | continue;
|
123 | }
|
124 |
|
125 |
|
126 | vnodes.push(slots[slotName]({ content: content.join('') }));
|
127 | continue;
|
128 | }
|
129 | }
|
130 |
|
131 |
|
132 | vnodes.push(has(slots, tagName) ? slots[tagName]() : chunk);
|
133 | continue;
|
134 | }
|
135 |
|
136 | return vnodes;
|
137 | },
|
138 | };
|
139 | </script>
|