parser.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. 'use strict';
  2. // match1 - section, match2 - optional full inheritance part, match3 - inherited section
  3. const REGEXP_SECTION = /^\s*\[\s*([^:]*?)\s*(:\s*(.+?)\s*)?\]\s*$/;
  4. const REGEXP_COMMENT = /^;.*/;
  5. const REGEXP_SINGLE_LINE = /^\s*(.*?)\s*?=\s*?(\S.*?)$/;
  6. const REGEXP_MULTI_LINE = /^\s*(.*?)\s*?=\s*?"(.*?)$/;
  7. const REGEXP_NOT_ESCAPED_MULTI_LINE_END = /^(.*?)\\"$/;
  8. const REGEXP_MULTI_LINE_END = /^(.*?)"$/;
  9. const REGEXP_ARRAY = /^(.*?)\[\]$/;
  10. const STATUS_OK = 0;
  11. const STATUS_INVALID = 1;
  12. const defaults = {
  13. ignore_invalid: true,
  14. keep_quotes: false,
  15. nested_section_names: false,
  16. keep_zero_prefix: false,
  17. oninvalid: () => true,
  18. filters: [],
  19. constants: {},
  20. };
  21. const REGEXP_IGNORE_KEYS = /__proto__|constructor|prototype/;
  22. class Parser {
  23. constructor(options = {}) {
  24. this.options = Object.assign({}, defaults, options);
  25. this.handlers = [
  26. this.handleMultiLineStart,
  27. this.handleMultiLineEnd,
  28. this.handleMultiLineAppend,
  29. this.handleComment,
  30. this.handleSection,
  31. this.handleSingleLine,
  32. ];
  33. }
  34. parse(lines) {
  35. const ctx = {
  36. ini: {},
  37. current: {},
  38. multiLineKeys: false,
  39. multiLineValue: '',
  40. };
  41. for (let line of lines) {
  42. for (let handler of this.handlers) {
  43. const stop = handler.call(this, ctx, line);
  44. if (stop) {
  45. break;
  46. }
  47. }
  48. }
  49. return ctx.ini;
  50. }
  51. isSection(line) {
  52. return line.match(REGEXP_SECTION);
  53. }
  54. getSection(line) {
  55. return line.match(REGEXP_SECTION)[1];
  56. }
  57. getParentSection(line) {
  58. return line.match(REGEXP_SECTION)[3];
  59. }
  60. isInheritedSection(line) {
  61. return !!line.match(REGEXP_SECTION)[3];
  62. }
  63. isComment(line) {
  64. return line.match(REGEXP_COMMENT);
  65. }
  66. isSingleLine(line) {
  67. const result = line.match(REGEXP_SINGLE_LINE);
  68. if (!result) {
  69. return false;
  70. }
  71. const check = result[2].match(/"/g);
  72. return !check || check.length % 2 === 0;
  73. }
  74. isMultiLine(line) {
  75. const result = line.match(REGEXP_MULTI_LINE);
  76. if (!result) {
  77. return false;
  78. }
  79. const check = result[2].match(/"/g);
  80. return !check || check.length % 2 === 0;
  81. }
  82. isMultiLineEnd(line) {
  83. return line.match(REGEXP_MULTI_LINE_END) && !line.match(REGEXP_NOT_ESCAPED_MULTI_LINE_END);
  84. }
  85. isArray(line) {
  86. return line.match(REGEXP_ARRAY);
  87. }
  88. assignValue(element, keys, value) {
  89. value = this.applyFilter(value);
  90. let current = element;
  91. let previous = element;
  92. let array = false;
  93. let key;
  94. if (keys.some((key) => REGEXP_IGNORE_KEYS.test(key))) {
  95. return;
  96. }
  97. for (key of keys) {
  98. if (this.isArray(key)) {
  99. key = this.getArrayKey(key);
  100. array = true;
  101. }
  102. if (current[key] == null) {
  103. current[key] = array ? [] : {};
  104. }
  105. previous = current;
  106. current = current[key];
  107. }
  108. if (array) {
  109. current.push(value);
  110. } else {
  111. previous[key] = value;
  112. }
  113. return element;
  114. }
  115. applyFilter(value) {
  116. for (let filter of this.options.filters) {
  117. value = filter(value, this.options);
  118. }
  119. return value;
  120. }
  121. getKeyValue(line) {
  122. const result = line.match(REGEXP_SINGLE_LINE);
  123. if (!result) {
  124. throw new Error();
  125. }
  126. let [, key, value] = result;
  127. if (!this.options.keep_quotes) {
  128. value = value.replace(/^\s*?"(.*?)"\s*?$/, '$1');
  129. }
  130. return { key, value, status: STATUS_OK };
  131. }
  132. getMultiKeyValue(line) {
  133. const result = line.match(REGEXP_MULTI_LINE);
  134. if (!result) {
  135. throw new Error();
  136. }
  137. let [, key, value] = result;
  138. if (this.options.keep_quotes) {
  139. value = '"' + value;
  140. }
  141. return { key, value };
  142. }
  143. getMultiLineEndValue(line) {
  144. const result = line.match(REGEXP_MULTI_LINE_END);
  145. if (!result) {
  146. throw new Error();
  147. }
  148. let [, value] = result;
  149. if (this.options.keep_quotes) {
  150. value = value + '"';
  151. }
  152. return { value, status: STATUS_OK };
  153. }
  154. getArrayKey(line) {
  155. const result = line.match(REGEXP_ARRAY);
  156. return result[1];
  157. }
  158. handleMultiLineStart(ctx, line) {
  159. if (!this.isMultiLine(line.trim())) {
  160. return false;
  161. }
  162. const { key, value } = this.getMultiKeyValue(line);
  163. const keys = key.split('.');
  164. ctx.multiLineKeys = keys;
  165. ctx.multiLineValue = value;
  166. return true;
  167. }
  168. handleMultiLineEnd(ctx, line) {
  169. if (!ctx.multiLineKeys || !this.isMultiLineEnd(line.trim())) {
  170. return false;
  171. }
  172. const { value, status } = this.getMultiLineEndValue(line);
  173. // abort on false of onerror callback if we meet an invalid line
  174. if (status === STATUS_INVALID && !this.options.oninvalid(line)) {
  175. return;
  176. }
  177. // ignore whole multiline on invalid
  178. if (status === STATUS_INVALID && this.options.ignore_invalid) {
  179. ctx.multiLineKeys = false;
  180. ctx.multiLineValue = '';
  181. return true;
  182. }
  183. ctx.multiLineValue += '\n' + value;
  184. this.assignValue(ctx.current, ctx.multiLineKeys, ctx.multiLineValue);
  185. ctx.multiLineKeys = false;
  186. ctx.multiLineValue = '';
  187. return true;
  188. }
  189. handleMultiLineAppend(ctx, line) {
  190. if (!ctx.multiLineKeys || this.isMultiLineEnd(line.trim())) {
  191. return false;
  192. }
  193. ctx.multiLineValue += '\n' + line;
  194. return true;
  195. }
  196. handleComment(ctx, line) {
  197. return this.isComment(line.trim());
  198. }
  199. handleSection(ctx, line) {
  200. line = line.trim();
  201. if (!this.isSection(line)) {
  202. return false;
  203. }
  204. const section = this.getSection(line);
  205. if (REGEXP_IGNORE_KEYS.test(section)) {
  206. return false;
  207. }
  208. this.createSection(ctx, section);
  209. if (this.isInheritedSection(line)) {
  210. const parentSection = this.getParentSection(line);
  211. ctx.current = Object.assign(
  212. ctx.current,
  213. JSON.parse(JSON.stringify(ctx.ini[parentSection])),
  214. );
  215. }
  216. return true;
  217. }
  218. handleSingleLine(ctx, line) {
  219. line = line.trim();
  220. if (!this.isSingleLine(line)) {
  221. return false;
  222. }
  223. const { key, value, status } = this.getKeyValue(line);
  224. // abort on false of onerror callback if we meet an invalid line
  225. if (status === STATUS_INVALID && !this.options.oninvalid(line)) {
  226. throw new Error('Abort');
  227. }
  228. // skip entry
  229. if (status === STATUS_INVALID && !this.options.ignore_invalid) {
  230. return true;
  231. }
  232. const keys = key.split('.');
  233. this.assignValue(ctx.current, keys, value);
  234. return true;
  235. }
  236. createSection(ctx, section) {
  237. const sections = (this.options.nested_section_names ? section.split('.') : [section]).map(
  238. (name) => name.trim(),
  239. );
  240. ctx.current = sections.reduce((ini, name) => {
  241. if (typeof ini[name] === 'undefined') {
  242. ini[name] = {};
  243. }
  244. return ini[name];
  245. }, ctx.ini);
  246. }
  247. }
  248. module.exports = Parser;