no-unescaped-entities.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. /**
  2. * @fileoverview HTML special characters should be escaped.
  3. * @author Patrick Hayes
  4. */
  5. 'use strict';
  6. const docsUrl = require('../util/docsUrl');
  7. const jsxUtil = require('../util/jsx');
  8. const report = require('../util/report');
  9. // ------------------------------------------------------------------------------
  10. // Rule Definition
  11. // ------------------------------------------------------------------------------
  12. // NOTE: '<' and '{' are also problematic characters, but they do not need
  13. // to be included here because it is a syntax error when these characters are
  14. // included accidentally.
  15. const DEFAULTS = [{
  16. char: '>',
  17. alternatives: ['&gt;'],
  18. }, {
  19. char: '"',
  20. alternatives: ['&quot;', '&ldquo;', '&#34;', '&rdquo;'],
  21. }, {
  22. char: '\'',
  23. alternatives: ['&apos;', '&lsquo;', '&#39;', '&rsquo;'],
  24. }, {
  25. char: '}',
  26. alternatives: ['&#125;'],
  27. }];
  28. const messages = {
  29. unescapedEntity: 'HTML entity, `{{entity}}` , must be escaped.',
  30. unescapedEntityAlts: '`{{entity}}` can be escaped with {{alts}}.',
  31. };
  32. module.exports = {
  33. meta: {
  34. docs: {
  35. description: 'Disallow unescaped HTML entities from appearing in markup',
  36. category: 'Possible Errors',
  37. recommended: true,
  38. url: docsUrl('no-unescaped-entities'),
  39. },
  40. messages,
  41. schema: [{
  42. type: 'object',
  43. properties: {
  44. forbid: {
  45. type: 'array',
  46. items: {
  47. anyOf: [{
  48. type: 'string',
  49. }, {
  50. type: 'object',
  51. properties: {
  52. char: {
  53. type: 'string',
  54. },
  55. alternatives: {
  56. type: 'array',
  57. uniqueItems: true,
  58. items: {
  59. type: 'string',
  60. },
  61. },
  62. },
  63. }],
  64. },
  65. },
  66. },
  67. additionalProperties: false,
  68. }],
  69. },
  70. create(context) {
  71. function reportInvalidEntity(node) {
  72. const configuration = context.options[0] || {};
  73. const entities = configuration.forbid || DEFAULTS;
  74. // HTML entities are already escaped in node.value (as well as node.raw),
  75. // so pull the raw text from context.getSourceCode()
  76. for (let i = node.loc.start.line; i <= node.loc.end.line; i++) {
  77. let rawLine = context.getSourceCode().lines[i - 1];
  78. let start = 0;
  79. let end = rawLine.length;
  80. if (i === node.loc.start.line) {
  81. start = node.loc.start.column;
  82. }
  83. if (i === node.loc.end.line) {
  84. end = node.loc.end.column;
  85. }
  86. rawLine = rawLine.slice(start, end);
  87. for (let j = 0; j < entities.length; j++) {
  88. for (let index = 0; index < rawLine.length; index++) {
  89. const c = rawLine[index];
  90. if (typeof entities[j] === 'string') {
  91. if (c === entities[j]) {
  92. report(context, messages.unescapedEntity, 'unescapedEntity', {
  93. node,
  94. loc: { line: i, column: start + index },
  95. data: {
  96. entity: entities[j],
  97. },
  98. });
  99. }
  100. } else if (c === entities[j].char) {
  101. report(context, messages.unescapedEntityAlts, 'unescapedEntityAlts', {
  102. node,
  103. loc: { line: i, column: start + index },
  104. data: {
  105. entity: entities[j].char,
  106. alts: entities[j].alternatives.map((alt) => `\`${alt}\``).join(', '),
  107. },
  108. });
  109. }
  110. }
  111. }
  112. }
  113. }
  114. return {
  115. 'Literal, JSXText'(node) {
  116. if (jsxUtil.isJSX(node.parent)) {
  117. reportInvalidEntity(node);
  118. }
  119. },
  120. };
  121. },
  122. };