zammad-tailwind-ltr.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. /**
  3. * @fileoverview Enforce "ltr/rtl" rule, if positioning classes are used
  4. * @author Vladimir Sheremet
  5. */
  6. //------------------------------------------------------------------------------
  7. // Rule Definition
  8. //------------------------------------------------------------------------------
  9. const parsePossibleClassString = (classesList) => {
  10. const counterparts = {
  11. left: 'right',
  12. right: 'left',
  13. pl: 'pr',
  14. pr: 'pl',
  15. ml: 'mr',
  16. mr: 'ml',
  17. rtl: 'ltr',
  18. ltr: 'rtl',
  19. }
  20. Object.entries(counterparts).forEach(([key, value]) => {
  21. counterparts[`-${key}`] = `-${value}`
  22. counterparts[`!${key}`] = `!${value}`
  23. counterparts[`!-${key}`] = `!-${value}`
  24. })
  25. counterparts['translate-x'] = '-translate-x'
  26. counterparts['-translate-x'] = 'translate-x'
  27. counterparts['!translate-x'] = '!-translate-x'
  28. counterparts['!-translate-x'] = '!translate-x'
  29. const classes = classesList.split(' ')
  30. const errors = []
  31. const baseClass = Object.keys(counterparts).join('|')
  32. classes.forEach((className) => {
  33. const match = className.match(new RegExp(`^(${baseClass})-([^\n]+)`))
  34. if (!match) return
  35. const [, prefix, value] = match
  36. const counterpart = `${counterparts[prefix]}-${value}` // pl-2 pr-2 is the same with or without ltr/rtl
  37. if (classes.includes(counterpart)) return
  38. errors.push({
  39. remove: className,
  40. add: [`rtl:${counterpart}`, `ltr:${className}`],
  41. })
  42. })
  43. classes.forEach((className) => {
  44. const match = className.match(
  45. new RegExp(`^(rtl|ltr):(${baseClass})-([^\n]+)`),
  46. )
  47. if (!match) return
  48. const [, dir, prefix, value] = match
  49. if (value === '0') return
  50. const counterpart = `${counterparts[dir]}:${counterparts[prefix]}-${value}`
  51. if (classes.includes(counterpart)) return
  52. errors.push({
  53. remove: null,
  54. add: [counterpart],
  55. })
  56. })
  57. return {
  58. classes,
  59. errors,
  60. }
  61. }
  62. /**
  63. * @type {import('eslint').Rule.RuleModule}
  64. */
  65. module.exports = {
  66. meta: {
  67. type: 'problem',
  68. docs: {
  69. description: 'Enforce "ltr/rtl" rule, if positioning classes are used',
  70. category: 'Layout & Formatting',
  71. recommended: true,
  72. url: null,
  73. },
  74. fixable: 'code',
  75. schema: [],
  76. },
  77. create(context) {
  78. const visitor =
  79. context.parserServices?.defineTemplateBodyVisitor ||
  80. ((obj1, obj2) => ({ ...obj1, ...obj2 }))
  81. const processLiteral = (node, quotes = "'") => {
  82. const content = node.value
  83. if (typeof content !== 'string') return
  84. const { errors, classes } = parsePossibleClassString(content)
  85. if (errors.length) {
  86. context.report({
  87. loc: node.loc,
  88. message:
  89. 'When positioning classes are used, they must be prefixed with ltr/rtl.',
  90. fix(fixer) {
  91. const newClasses = [...classes]
  92. errors.forEach(({ remove, add }) => {
  93. if (remove) {
  94. const index = newClasses.indexOf(remove)
  95. newClasses.splice(index, 1)
  96. }
  97. add.forEach((a) => {
  98. if (!newClasses.includes(a)) {
  99. newClasses.push(a)
  100. }
  101. })
  102. })
  103. return fixer.replaceText(
  104. node,
  105. `${quotes}${newClasses.join(' ')}${quotes}`,
  106. )
  107. },
  108. })
  109. }
  110. }
  111. return visitor(
  112. {
  113. VLiteral: (node) => processLiteral(node, '"'),
  114. Literal: (node) => processLiteral(node, "'"),
  115. },
  116. {
  117. Literal: (node) => processLiteral(node, "'"),
  118. },
  119. )
  120. },
  121. }