PageSourceSearch

https://mix.my/themes/custom/astro_radix/src/js/utils/generate-slug.js?tluj44

js mix.my collected 2026-10-02 14:07:10 UTC 9,014 bytes, 308 lines download raw bytes

1// utils/slugUtil.js
2(function (root, factory) {
3  if (typeof define === 'function' && define.amd) {
4    define([], factory);
5  } else if (typeof module === 'object' && module.exports) {
6    module.exports = factory();
7  } else {
8    root.slugUtil = factory();
9  }
10}(typeof self !== 'undefined' ? self : this, function () {
11  'use strict';
12
13  /**
14   * @typedef {Object} PathautoSettings
15   * @property {string[]|string} [ignore_words=[]] // Accepts array or comma-separated string
16   * @property {Object.<string,('remove'|'replace'|0|1|string|number)>} [punctuation={}]
17   * @property {number} [max_length=128]
18   * @property {string} [separator='-']
19   * @property {boolean} [lowercase=true]
20   * @property {boolean} [transliterate=false]
21   */
22
23  /**
24   * @typedef {Object} GenerateOptions
25   * @property {PathautoSettings} [settings]   // Per-call override of config
26   */
27
28  // Internal configuration state (optional, set via configure()).
29  let internalConfig = null;
30
31  // ---- Helpers ----
32
33  /**
34   * Escape a string for safe usage inside RegExp.
35   * @param {string} s
36   * @returns {string}
37   */
38  function escapeRegExp(s) {
39    return (s || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
40  }
41
42  /**
43   * Minimal transliteration using Unicode NFD stripping combining diacritics.
44   * Note: For full Pathauto parity, use a comprehensive map or a dedicated lib.
45   * @param {string} s
46   * @returns {string}
47   */
48  function lightTransliterate(s) {
49    if (!s) return s;
50    try {
51      return s.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
52    } catch (e) {
53      // Older browsers without normalize()
54      return s;
55    }
56  }
57
58  /**
59   * Normalize ignore_words coming from Drupal: supports array or comma-separated string.
60   * @param {unknown} v
61   * @returns {string[]}
62   */
63  function normalizeIgnoreWords(v) {
64    if (Array.isArray(v)) {
65      return v
66        .flatMap(item => String(item ?? '').split(','))
67        .map(s => s.trim())
68        .filter(Boolean);
69    }
70    if (typeof v === 'string') {
71      return v
72        .split(',')
73        .map(s => s.trim())
74        .filter(Boolean);
75    }
76    return [];
77  }
78
79  // Pathauto-style punctuation name → literal char(s)
80  /** @type {Record<string, string|string[]>} */
81  const PATHAUTO_PUNCT_MAP = {
82    // brackets & parens
83    'left-square-bracket': '[',
84    'right-square-bracket': ']',
85    'left-parenthesis': '(',
86    'right-parenthesis': ')',
87    'left-curly-brace': '{',
88    'right-curly-brace': '}',
89
90    // dashes / hyphen
91    'hyphen': '-',
92    'en-dash': '–',
93    'em-dash': '—',
94
95    // quotes
96    'apostrophe': "'",
97    'single-quote': "'",
98    'double-quote': '"',
99    'quotation-mark': '"',
100
101    // slashes & backslash
102    'slash': '/',
103    'backslash': '\\',
104
105    // punctuation common
106    'colon': ':',
107    'semicolon': ';',
108    'comma': ',',
109    'period': '.',
110    'dot': '.',
111    'underscore': '_',
112    'question-mark': '?',
113    'exclamation-mark': '!',
114    'asterisk': '*',
115    'plus': '+',
116    'equals': '=',
117    'hash': '#',
118    'percent': '%',
119    'caret': '^',
120    'pipe': '|',
121    'tilde': '~',
122    'grave-accent': '`',
123    'ampersand': '&',
124    'at-sign': '@',
125
126    // extras you may see
127    'angle-left': '<',
128    'angle-right': '>',
129    'ellipsis': '…',
130  };
131
132  /**
133   * Translate a Pathauto-style punctuation map into { [char]: 'remove'|'replace' }.
134   * Accepts char keys as-is; converts named keys via PATHAUTO_PUNCT_MAP.
135   * 0/'0'/'remove' => 'remove'
136   * 1/'1'/'replace' => 'replace'
137   * @param {Record<string, 'remove'|'replace'|0|1|string|number>} src
138   * @returns {Record<string, 'remove'|'replace'>}
139   */
140  function normalizePunctuationMap(src) {
141    const out = {};
142    for (const [key, action] of Object.entries(src || {})) {
143      const normalized =
144        action === 'remove' || action === 0 || action === '0'
145          ? 'remove'
146          : action === 'replace' || action === 1 || action === '1'
147          ? 'replace'
148          : null;
149
150      if (!normalized) continue;
151
152      const mapped = PATHAUTO_PUNCT_MAP[key];
153      if (mapped) {
154        if (Array.isArray(mapped)) {
155          for (const ch of mapped) out[ch] = normalized;
156        } else {
157          out[mapped] = normalized;
158        }
159      } else {
160        // Assume key is a literal char (or multi-char token).
161        if (key) out[key] = normalized;
162      }
163    }
164    return out;
165  }
166
167  /**
168   * Resolve effective configuration in this order:
169   *  1) Per-call `override`
170   *  2) Internal configured state (set via configure())
171   *  3) drupalSettings.mymodule.pathauto (if available on window)
172   *  4) Safe defaults
173   * @param {PathautoSettings} [override]
174   * @returns {Required<PathautoSettings>}
175   */
176  function resolveConfig(override) {
177    // Pull from `drupalSettings` if present (browser, Drupal)
178    let fromDrupalSettings = null;
179    try {
180      // eslint-disable-next-line no-undef
181      if (typeof drupalSettings !== 'undefined' && drupalSettings?.pathauto) {
182        // eslint-disable-next-line no-undef
183        fromDrupalSettings = drupalSettings.pathauto;
184      }
185      console.log(fromDrupalSettings);
186    } catch (e) {
187      // Ignore environment without drupalSettings.
188    }
189
190    const base = override || internalConfig || fromDrupalSettings || {};
191
192    return /** @type {Required<PathautoSettings>} */ ({
193      ignore_words: normalizeIgnoreWords(base.ignore_words),
194      punctuation: base && typeof base.punctuation === 'object' && base.punctuation !== null ? base.punctuation : {},
195      max_length: Number.isFinite(base.max_length) ? base.max_length : 128,
196      separator: typeof base.separator === 'string' && base.separator.length ? base.separator : '-',
197      lowercase: typeof base.lowercase === 'boolean' ? base.lowercase : true,
198      transliterate: !!base.transliterate,
199    });
200  }
201
202  /**
203   * Configure global/internal settings once (or multiple times).
204   * @param {PathautoSettings} settings
205   */
206  function configure(settings) {
207    internalConfig = resolveConfig(settings);
208  }
209
210  /**
211   * Return the currently effective config (no override).
212   * @returns {Required<PathautoSettings>}
213   */
214  function getConfig() {
215    return resolveConfig();
216  }
217
218  /**
219   * Generate a Pathauto-like slug from id + name.
220   * - Handles ignore words (array or comma-separated string)
221   * - Punctuation rules ('remove'/'replace' or 0/1)
222   * - Lowercasing (optional)
223   * - Separator collapsing/trim
224   * - Max length with graceful cutback to last separator
225   * - Light transliteration (diacritics stripping) when enabled
226   *
227   * @param {string|number} id
228   * @param {string} name
229   * @param {GenerateOptions} [options]
230   * @returns {string}
231   */
232  function generateSlug(id, name, options) {
233    // NOTE: per-call overrides come from options.settings
234    const cfg = resolveConfig(options?.settings);
235
236    const separator = cfg.separator;
237    const lowercase = cfg.lowercase;
238    const maxLen = cfg.max_length;
239
240    let slug = `${id ?? ''} ${name ?? ''}`.trim();
241
242    if (cfg.transliterate) {
243      slug = lightTransliterate(slug);
244    }
245
246    // A) Remove ignore words (replace with space to preserve boundaries).
247    if (cfg.ignore_words.length) {
248      const words = cfg.ignore_words
249        .map(w => (w == null ? '' : String(w).trim().toLowerCase()))
250        .filter(Boolean);
251
252      if (words.length) {
253        const escaped = words.map(escapeRegExp);
254        const re = new RegExp(`\\b(${escaped.join('|')})\\b`, 'gi');
255        slug = slug.replace(re, ' ');
256      }
257    }
258
259    // B) Punctuation rules: 'remove'/'replace' or 0/1
260    const punct = normalizePunctuationMap(cfg.punctuation);
261    if (punct && typeof punct === 'object') {
262      Object.entries(punct).forEach(([char, action]) => {
263        if (!char) return;
264        const rx = new RegExp(escapeRegExp(char), 'g');
265        if (action === 'remove') {
266          slug = slug.replace(rx, '');
267        } else if (action === 'replace') {
268          // Replace with space; separators handled later.
269          slug = slug.replace(rx, ' ');
270        }
271      });
272    }
273
274    // C) Lowercase if configured.
275    if (lowercase) {
276      slug = slug.toLowerCase();
277    }
278
279    // Safety net: turn any other stray symbols into spaces (keep digits/letters/_/- and spaces).
280    slug = slug.replace(/[^0-9A-Za-z\s_-]+/g, ' ');
281
282    // D) Normalize whitespace/underscores -> separator, collapse and trim.
283    const sepEsc = escapeRegExp(separator);
284    slug = slug
285      .trim()
286      .replace(/[_\s]+/g, separator)
287      .replace(new RegExp(`${sepEsc}+`, 'g'), separator)
288      .replace(new RegExp(`^${sepEsc}+|${sepEsc}+$`, 'g'), '');
289
290    // E) Enforce max length and optionally cut back to the last separator.
291    if (slug.length > maxLen) {
292      slug = slug.substring(0, maxLen);
293      const lastSep = slug.lastIndexOf(separator);
294      if (lastSep > Math.floor(maxLen / 2)) {
295        slug = slug.substring(0, lastSep);
296      }
297    }
298
299    return slug;
300  }
301
302  // Public API
303  return {
304    configure,
305    getConfig,
306    generateSlug,
307  };
308}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.