PrepareComponentsHook.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import { IPluginNode } from "../../Plugin";
  2. import { Hook } from "../Hook";
  3. import { TagNode } from "../nodes/TagNode";
  4. import { ScriptParser } from "../hooks/component/ScriptParser";
  5. import { AstNode } from "../nodes/AstNode";
  6. import { PupperCompiler } from "../../Compiler";
  7. import { CompilerNode } from "../../../model/core/nodes/CompilerNode";
  8. export const DefaultExportSymbol = Symbol("ExportedComponent");
  9. interface IImplementation {
  10. name: string;
  11. parameters: {
  12. name: string;
  13. initializer?: string;
  14. }[]
  15. body: string;
  16. }
  17. export interface IComponent {
  18. name: string | symbol;
  19. implementation: {
  20. methods?: IImplementation[];
  21. when?: IImplementation[];
  22. listeners?: (IImplementation & {
  23. covers: string[]
  24. })[];
  25. },
  26. template: string;
  27. script?: string;
  28. style?: string;
  29. data?: string;
  30. setupScript?: string;
  31. exported?: boolean;
  32. }
  33. export class PrepareComponents extends Hook {
  34. /**
  35. * The imports that will later be putted into the template header
  36. */
  37. protected components: Record<string | symbol, IComponent> = {};
  38. protected exportedData: Record<string, string> = {};
  39. public beforeStart(code: string) {
  40. this.plugin.sharedData.components = {};
  41. this.components = this.plugin.sharedData.components;
  42. const lines = code.replace(/\r\n/g, "\n").split(/\n/g);
  43. const identation = this.plugin.detectIdentation();
  44. const startWithRegExp = new RegExp("^" + identation + "(?!" + identation + ")");
  45. const paramsRegExp = /^.+?\((?<params>.*?)\)$/gm;
  46. const singleParamRegExp = /([^?=,]+)(=([^,]*))?/g;
  47. for(let index = 0; index < lines.length; index++) {
  48. let line = lines[index];
  49. if (line.startsWith("data") && !line.trimEnd().endsWith(".")) {
  50. lines[index] = line.trimEnd() + ".";
  51. continue;
  52. }
  53. if (!line.startsWith("implementation")) {
  54. continue;
  55. }
  56. index++;
  57. // Retrieve all lines until a non-identation was found
  58. do {
  59. line = lines[index];
  60. if (line === undefined) {
  61. break;
  62. }
  63. // Ignore empty lines
  64. if (line.length === 0) {
  65. index++;
  66. continue;
  67. }
  68. // If the line starts with one identation level
  69. // but doesn't end with a dot and isn't a comment
  70. if (line.match(startWithRegExp) && !line.trim().endsWith(".") && !line.trimStart().startsWith("//")) {
  71. // Append a dot at the end of it
  72. lines[index] = line.trimEnd() + ".";
  73. let identifier = line.trimStart();
  74. // If it's a "when"
  75. if (identifier.startsWith("when")) {
  76. // Replace it with the internal "p-when"
  77. identifier = identifier.replace("when", "event-when");
  78. } else
  79. // If it's not an event or a listener
  80. if (!identifier.startsWith("event") && !identifier.startsWith("listener")) {
  81. // Assume it's a method then
  82. identifier = identifier.replace(identifier, "method" + identifier);
  83. }
  84. // Try matching params against the identifier
  85. const matchedParams = paramsRegExp.exec(identifier);
  86. // If matched
  87. if (matchedParams) {
  88. // Extract all single params
  89. const singleParams = matchedParams.groups.params.matchAll(singleParamRegExp);
  90. // Iterate over all params
  91. for(let param of singleParams) {
  92. // If it doesn't have a initializer
  93. if (param[2] === undefined) {
  94. // Strictly add an initializer to it
  95. identifier = identifier.replace(param[0], param[0] + " = undefined");
  96. }
  97. }
  98. }
  99. // Replace the identifier with the new one
  100. lines[index] = lines[index].replace(line.trimStart(), identifier);
  101. }
  102. index++;
  103. } while(line.length === 0 || line.startsWith(identation));
  104. }
  105. return lines.join("\n");
  106. }
  107. public parse(nodes: IPluginNode[]) {
  108. for(let node of nodes) {
  109. // Ignore components that aren't in the root
  110. if (!(node?.parent instanceof AstNode)) {
  111. continue;
  112. }
  113. // Parse as a component
  114. const component = this.parseComponentNode(node.parent);
  115. break;
  116. }
  117. return nodes;
  118. }
  119. public afterCompile(code: string) {
  120. const exportedComponent = this.components[DefaultExportSymbol];
  121. // Check if has any exported components
  122. if (exportedComponent) {
  123. // Parse the script
  124. const parsedScript = new ScriptParser(
  125. exportedComponent,
  126. this.plugin.getCompilerOptions().fileName,
  127. this.components,
  128. this.plugin
  129. ).parse();
  130. code = `${parsedScript}\n`;
  131. if (exportedComponent.style) {
  132. code += `\n${exportedComponent.style}\n`;
  133. }
  134. }
  135. return code;
  136. }
  137. public parseComponentNode(node: AstNode | TagNode) {
  138. const isRootComponent = node instanceof AstNode;
  139. const name = !isRootComponent ? node.getAttribute("name")?.replace(/"/g, "") : DefaultExportSymbol;
  140. const implementation = node.findFirstChildByTagName("implementation");
  141. const template = node.findFirstChildByTagName("template");
  142. const script = node.findFirstChildByTagName("script");
  143. const style = node.findFirstChildByTagName("style");
  144. const data = node.findFirstChildByTagName("data");
  145. // If no script and no template tag was found
  146. if (!script && !template) {
  147. throw this.compiler.makeParseError("Components must have at least a script tag or a template tag.", {
  148. line: node.getLine() || 1,
  149. column: node.getColumn()
  150. });
  151. }
  152. /**
  153. * Create the component
  154. */
  155. const component: IComponent = {
  156. name,
  157. implementation: {
  158. methods: [],
  159. when: [],
  160. listeners: []
  161. },
  162. template: null,
  163. script: null,
  164. style: null,
  165. data: null,
  166. exported: isRootComponent
  167. };
  168. // Save the component
  169. this.components[component.name] = component;
  170. // If the component is not exported and has no name
  171. if (!component.exported && !name) {
  172. throw this.compiler.makeParseError("Scoped components must have a name.", {
  173. line: node.getLine() || 1,
  174. column: node.getColumn()
  175. });
  176. }
  177. // If the component has no name
  178. if (!name) {
  179. // Assume it's the default export
  180. component.name = DefaultExportSymbol;
  181. }
  182. // If has a script
  183. if (script) {
  184. component.script = this.consumeChildrenAsString(script);
  185. }
  186. // If has a style
  187. if (style) {
  188. console.log(style);
  189. }
  190. // If has data
  191. if (data) {
  192. component.data = this.consumeChildrenAsString(data);
  193. }
  194. // If has methods
  195. if (implementation) {
  196. component.implementation = this.consumeAsImplementation(implementation);
  197. }
  198. // If has a template
  199. // ATTENTION: templates needs to be parsed after everything as already parsed.
  200. if (template) {
  201. let lines = this.plugin.compiler.contents.split("\n");
  202. const nextNodeAfterTemplate = template.getNextNode();
  203. lines = lines.slice(
  204. template.getLine(),
  205. nextNodeAfterTemplate ? nextNodeAfterTemplate.getLine() - 1 : (node.hasNext() ? node.getNextNode().getLine() - 1 : lines.length)
  206. );
  207. // Detect identation
  208. const identation = this.plugin.detectIdentation();
  209. const contents = lines
  210. // Replace the first identation
  211. .map((line) => line.replace(identation, ""))
  212. .join("\n");
  213. const compiler = new PupperCompiler(this.plugin.options);
  214. compiler.setSharedData(this.plugin.sharedData);
  215. const templateAsString = compiler.compileTemplate(contents);
  216. component.template = templateAsString;
  217. }
  218. return component;
  219. }
  220. /**
  221. * Consumes all children nodes from a tag node into a component implementation.
  222. * @param node The node to be consumed.
  223. * @returns
  224. */
  225. protected consumeAsImplementation(node: TagNode) {
  226. const implementations: IComponent["implementation"] = {
  227. methods: [],
  228. when: [],
  229. listeners: []
  230. };
  231. // Iterate over all children
  232. node.getChildren().forEach((child) => {
  233. // Ignore comments
  234. if (child.isType("Comment")) {
  235. return;
  236. }
  237. // If it's not a tag
  238. if (!(child instanceof TagNode)) {
  239. throw this.plugin.compiler.makeParseError("The implementation tag should only contain methods and events, found a " + child.getType() + ".", {
  240. line: child.getLine(),
  241. column: child.getColumn()
  242. });
  243. }
  244. // If it isn't an event or method
  245. if (!["method", "event", "event-when", "listener"].includes(child.getName())) {
  246. throw this.plugin.compiler.makeParseError("The implementation tag should only contain methods, found an invalid tag " + child.getName() + ".", {
  247. line: child.getLine(),
  248. column: child.getColumn()
  249. });
  250. }
  251. switch(child.getName()) {
  252. // If it's a "method"
  253. case "method":
  254. // Add it to the methods
  255. implementations.methods.push({
  256. name: child.getId(),
  257. parameters: child.getRawAttributes().filter((attr) => attr.name !== "class" && attr.name !== "id").map((attr) => ({
  258. name: attr.name,
  259. initializer: attr.val === "undefined" ? undefined : String(attr.val)
  260. })),
  261. body: this.consumeChildrenAsString(child)
  262. });
  263. break;
  264. // If it's a "when"
  265. case "event-when":
  266. // Add it to the when implementations
  267. implementations.when.push({
  268. name: child.getId(),
  269. parameters: child.getAttributes().map((attr) => ({
  270. name: attr.name,
  271. initializer: attr.val
  272. })),
  273. body: this.consumeChildrenAsString(child)
  274. });
  275. break;
  276. // If it's a "listener"
  277. case "listener":
  278. // Add it to the listeners implementations
  279. implementations.listeners.push({
  280. // Listeners has the prefix "$$p_" to prevent conflicts.
  281. name: "$$p_" + child.getId(),
  282. parameters: child.getAttributes().filter((attr) => attr.name !== "class" && attr.name !== "id").map((attr) => ({
  283. name: attr.name,
  284. initializer: attr.val
  285. })),
  286. body: this.consumeChildrenAsString(child),
  287. covers: child.getClasses()
  288. });
  289. break;
  290. }
  291. });
  292. return implementations;
  293. }
  294. /**
  295. * Consumes all children nodes values from a node into a string.
  296. * @param node The node to be consumed.
  297. * @returns
  298. */
  299. protected consumeChildrenAsString(node: CompilerNode) {
  300. this.plugin.parseChildren(node);
  301. return node.getChildren().map((child) => child.getProp("val")).join("").trimEnd();
  302. }
  303. };