index.js 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. /**
  2. * @typedef {import('hast').Element} Element
  3. * @typedef {import('hast').Properties} Properties
  4. */
  5. /**
  6. * @template {string} SimpleSelector
  7. * Selector type.
  8. * @template {string} DefaultTagName
  9. * Default tag name.
  10. * @typedef {(
  11. * SimpleSelector extends ''
  12. * ? DefaultTagName
  13. * : SimpleSelector extends `${infer TagName}.${infer Rest}`
  14. * ? ExtractTagName<TagName, DefaultTagName>
  15. * : SimpleSelector extends `${infer TagName}#${infer Rest}`
  16. * ? ExtractTagName<TagName, DefaultTagName>
  17. * : SimpleSelector extends string
  18. * ? SimpleSelector
  19. * : DefaultTagName
  20. * )} ExtractTagName
  21. * Extract tag name from a simple selector.
  22. */
  23. const search = /[#.]/g
  24. /**
  25. * Create a hast element from a simple CSS selector.
  26. *
  27. * @template {string} Selector
  28. * Type of selector.
  29. * @template {string} [DefaultTagName='div']
  30. * Type of default tag name (default: `'div'`).
  31. * @param {Selector | null | undefined} [selector]
  32. * Simple CSS selector (optional).
  33. *
  34. * Can contain a tag name (`foo`), classes (`.bar`), and an ID (`#baz`).
  35. * Multiple classes are allowed.
  36. * Uses the last ID if multiple IDs are found.
  37. * @param {DefaultTagName | null | undefined} [defaultTagName='div']
  38. * Tag name to use if `selector` does not specify one (default: `'div'`).
  39. * @returns {Element & {tagName: ExtractTagName<Selector, DefaultTagName>}}
  40. * Built element.
  41. */
  42. export function parseSelector(selector, defaultTagName) {
  43. const value = selector || ''
  44. /** @type {Properties} */
  45. const props = {}
  46. let start = 0
  47. /** @type {string | undefined} */
  48. let previous
  49. /** @type {string | undefined} */
  50. let tagName
  51. while (start < value.length) {
  52. search.lastIndex = start
  53. const match = search.exec(value)
  54. const subvalue = value.slice(start, match ? match.index : value.length)
  55. if (subvalue) {
  56. if (!previous) {
  57. tagName = subvalue
  58. } else if (previous === '#') {
  59. props.id = subvalue
  60. } else if (Array.isArray(props.className)) {
  61. props.className.push(subvalue)
  62. } else {
  63. props.className = [subvalue]
  64. }
  65. start += subvalue.length
  66. }
  67. if (match) {
  68. previous = match[0]
  69. start++
  70. }
  71. }
  72. return {
  73. type: 'element',
  74. // @ts-expect-error: tag name is parsed.
  75. tagName: tagName || defaultTagName || 'div',
  76. properties: props,
  77. children: []
  78. }
  79. }