compileResources<\/code>: Stores the selective compile data following a change to the code base.<\/li>\n<\/ol>\nprojectCode<\/h3>\n
The projectCode<\/code> object allows us to quickly retrieve pieces of Sass code. We then combine these pieces into a single string for compilation.<\/p>\n\nfiles<\/code>: With Microthemer, this stores the code added to the full code view described earlier. With an npm implementation, files<\/code>would relate to actual .sass or .scss system files.<\/li>\nfolders<\/code>: Microthemer\u2019s UI folders that contain segmented UI selectors.<\/li>\nindex<\/code>: The order of a folder, or a selector within a folder.<\/li>\nitemData<\/code>: The actual code for the item, explained further in the next code snippet.<\/li>\n<\/ul>\nvar projectCode = {\r\n\r\n \/\/ Microthemer full code editor\r\n files: {\r\n full_code: {\r\n index: 0,\r\n itemData: itemData\r\n }\r\n },\r\n\r\n \/\/ Microthemer UI folders and selectors\r\n folders: {\r\n content_header: {\r\n index:100,\r\n selectors: {\r\n '.entry-title': {\r\n index:0,\r\n itemData: itemData\r\n },\r\n }\r\n },\r\n buttons: {\r\n index:200,\r\n selectors: {\r\n '.btn': {\r\n index:0,\r\n itemData: itemData\r\n },\r\n '.btn-success': {\r\n index:1,\r\n itemData: itemData\r\n },\r\n '.btn-error': {\r\n index:2,\r\n itemData: itemData\r\n }\r\n }\r\n }\r\n }\r\n};<\/code><\/pre>\nitemData for .btn-success selector<\/h4>\n
The following code example shows the itemData<\/code> for the .btn-success<\/code> selector.<\/p>\n\nsassCode<\/code>: Used to build the compilation string.<\/li>\ncompiledCSS<\/code>: Stores compiled CSS for writing to a stylesheet or style node in the document head.<\/li>\nsassEntities<\/code>: Sass entities for single selector or file. Allows for before and after change analysis, and is used to build the projectEntities<\/code> object.<\/li>\nmediaQueries<\/code>: Same data as above, but for a selector used inside a media query.<\/li>\n<\/ul>\nvar itemData = {\r\n sassCode: \".btn-success { @extend .btn; background-color: $primary-color; @include rounded; }\",\r\n compiledCSS: \".btn-success { background-color: green; border-radius: 999px; }\",\r\n sassEntities: {\r\n extend: {\r\n '.btn': {\r\n values: ['.btn']\r\n }\r\n },\r\n variable: {\r\n primary_color: {\r\n values: [1]\r\n }\r\n },\r\n mixin: {\r\n rounded: {\r\n values: [1]\r\n }\r\n }\r\n },\r\n mediaQueries: {\r\n 'min-width(960px)': {\r\n sassCode: \".btn-success { border:4px solid darken($primary-color, 10%); &::before { content: '\\\\2713'; margin-right: .5em; } }\",\r\n compiledCSS: \".btn-success::before { content: '\\\\2713'; margin-right: .5em; }\",\r\n sassEntities: {\r\n variable: {\r\n primary_color: {\r\n values: [1]\r\n }\r\n },\r\n function: {\r\n darken: {\r\n values: [1]\r\n }\r\n }\r\n }\r\n }\r\n }\r\n};<\/code><\/pre>\nprojectEntities<\/h3>\n
The projectEntities<\/code> object allows us to check which selectors use particular Sass entities. <\/p>\n\nvariable<\/code>, function<\/code>, mixin<\/code>, extend<\/code>: The type of Sass entity.<\/li>\n- E.g.
primary_color<\/code>: The Sass entity name. Microthemer normalizes hyphenated names because Sass uses hyphens and underscores interchangeably.<\/li>\nvalues<\/code>: An array of declaration values or instances. Instances are represented by the number 1. The Gonzales PE Sass parser converts numeric declaration values to strings. So I\u2019ve elected to use the integer 1 to flag instances.<\/li>\nitemDeps<\/code>: An array of selectors that makes use of the Sass entity. This is explained further in the next code snippet.<\/li>\nrelatedEntities<\/code>: Our rounded<\/code> mixin has a side effect of updating the global $secondary-color<\/code> variable to blue<\/code>, hence the blue error button. This side effect makes the rounded<\/code> and $secondary-color<\/code> entities co-dependent. So, when the $secondary-color<\/code> variable is included, the rounded<\/code>mixin should be included too, and vice versa.<\/li>\n<\/ul>\nvar projectEntities = {\r\n variable: {\r\n primary_color: {\r\n values: ['green', 1],\r\n itemDeps: itemDeps\r\n },\r\n secondary_color: {\r\n values: [\"red\", \"blue !global\", 1],\r\n itemDeps: itemDeps,\r\n relatedEntities: {\r\n mixin: {\r\n rounded: {}\r\n }\r\n }\r\n },\r\n dark_color: {\r\n values: [\"black\", 1],\r\n itemDeps: itemDeps\r\n }\r\n },\r\n function: {\r\n darken: {\r\n values: [1]\r\n },\r\n toRem: {\r\n values: [\"@function toRem($px, $rootSize: 16){\u21b5 @return #{$px \/ $rootSize}rem;\u21b5}\", 1],\r\n itemDeps: itemDeps\r\n }\r\n },\r\n mixin: {\r\n rounded: {\r\n values: [\"@mixin rounded(){\u21b5 border-radius:999px;\u21b5 $secondary-color: blue !global;\u21b5}\", 1],\r\n itemDeps: itemDeps,\r\n relatedEntities: {\r\n variable: {\r\n secondary_color: {\r\n values: [\"blue !global\"],\r\n }\r\n }\r\n }\r\n }\r\n },\r\n extend: {\r\n '.btn': {\r\n values: ['.btn', '.btn'],\r\n itemDeps: itemDeps\r\n }\r\n }\r\n};<\/code><\/pre>\nitemDeps for the $primary-color Sass entity<\/h4>\n
The following code example shows the itemDeps<\/code> for the $primary-color<\/code> (primary_color<\/code>) variable. The $primary-color<\/code> variable is used by two forms of the .btn-success<\/code> selector, including a selector inside the min-width(960px)<\/code> media query. <\/p>\n\npath<\/code>: Used to retrieve selector data from the projectCode<\/code> object.<\/li>\nmediaQuery<\/code>: Used when updating style nodes or writing to a CSS stylesheet.<\/li>\n<\/ul>\nvar itemDeps = [\r\n {\r\n path: [\"folders\", 'header', 'selectors', '.btn-success'],\r\n },\r\n {\r\n path: [\"folders\", 'header', 'selectors', '.btn-success', 'mediaQueries', 'min-width(960px)'],\r\n mediaQuery: 'min-width(960px)'\r\n }\r\n];<\/code><\/pre>\nconnectedEntities<\/h3>\n
The connectedEntities<\/code> object allows us to find related pieces of code. We populate it following a change to the code base. So, if we were to remove the font-size<\/code> declaration from the .btn<\/code> selector, the code would change from this: <\/p>\n.btn {\r\n display: inline-block;\r\n padding: 1em;\r\n color: white;\r\n text-decoration: none;\r\n font-size: toRem(21);\r\n}<\/code><\/pre>\n…to this:<\/p>\n
.btn {\r\n display: inline-block;\r\n padding: 1em;\r\n color: white;\r\n text-decoration: none;\r\n}<\/code><\/pre>\nAnd we would store Microthemer\u2019s analysis in the following connectedEntities<\/code> object. <\/p>\n\n- \n
changed<\/code>: The change analysis, which captures the removal of the toRem<\/code> function.<\/p>\n\nactions<\/code>: an array of user actions.<\/li>\nform<\/code>: Declaration (e.g. $var: 18px<\/code>) or instance (e.g. font-size: $var<\/code>).<\/li>\nvalue<\/code>: A text value for a declaration, or the integer 1 for an instance.<\/li>\n<\/ul>\n<\/li>\ncoDependent<\/code>: Extended selectors must always compile with the extending selector, and vice versa. The relationship is co-dependent. Variables, functions, and mixins are only semi-dependent. Instances must compile with declarations, but declarations do not need to compile with instances. However, Microthemer treats them as co-dependent for the sake of simplicity. In the future, logic will be added to filter out unnecessary instances, but this has been left out for the first release.<\/li>\nrelated<\/code>: the rounded<\/code> mixin is related to the $secondary-color<\/code> variable. It updates that variable using the global<\/code> flag. The two entities are co-dependent; they should always compile together. But in our example, the .btn<\/code> selector makes no use of the rounded<\/code> mixin. So, the related<\/code> property below is not populated with anything.<\/li>\n<\/ul>\nvar connectedEntities = {\r\n changed: {\r\n function: {\r\n toRem: {\r\n actions: [{\r\n action: \"removed\",\r\n form: \"instance\",\r\n value: 1\r\n }]\r\n }\r\n }\r\n },\r\n coDependent: {\r\n extend: {\r\n '.btn': {}\r\n }\r\n },\r\n related: {}\r\n};<\/code><\/pre>\ncompileResources<\/h3>\n
The compileResources<\/code> object allows us to compile a subset of code in the correct order. In the previous section we removed the font-size declaration. The code below shows how the compileResources<\/code> object would look after that change. <\/p>\n\n- \n
compileParts<\/code>: An array of resources to be compiled.<\/p>\n\npath<\/code>: Used to update the compiledCSS<\/code> property of the relevant projectCode<\/code>item.<\/li>\nsassCode<\/code>: Used to build the sassString<\/code> for compilation. We append a CSS comment to each piece (\/*MTPART*\/<\/code>) . This comment is used to split the combined CSS output into the cssParts<\/code> array.<\/li>\n<\/ul>\n<\/li>\nsassString<\/code>: A string of Sass code that compiles to CSS.<\/li>\ncssParts<\/code>: CSS output in the form of an array. The array keys for cssParts<\/code> line up with the compileParts<\/code> array.<\/li>\n<\/ul>\nvar compileResources = {\r\n\r\n compileParts: [\r\n {\r\n path: [\"files\", \"full_code\"],\r\n sassCode: \"\/*MTFILE*\/$primary-color: green; $secondary-color: red; $dark-color: black; @function toRem($px, $rootSize: 16){ @return #{$px \/ $rootSize}rem; } @mixin rounded(){ border-radius:999px; $secondary-color: blue !global;}\/*MTPART*\/\"\r\n },\r\n {\r\n path: [\"folders\", \"buttons\", \".btn\"],\r\n sassCode: \".btn { display: inline-block; padding: 1em; color: white; text-decoration: none; }\/*MTPART*\/\"\r\n },\r\n {\r\n path: [\"folders\", \"buttons\", \".btn-success\"],\r\n sassCode: \".btn-success { @extend .btn; background-color: $primary-color; @include rounded; }\/*MTPART*\/\"\r\n },\r\n {\r\n path: [\"folders\", \"buttons\", \".btn-error\"],\r\n sassCode: \".btn-error { @extend .btn; background-color: $secondary-color; }\/*MTPART*\/\"\r\n }\r\n ],\r\n\r\n sassString: \r\n \"\/*MTFILE*\/$primary-color: green; $secondary-color: red; $dark-color: black; @function toRem($px, $rootSize: 16){ @return #{$px \/ $rootSize}rem; } @mixin rounded(){ border-radius:999px; $secondary-color: blue !global;}\/*MTPART*\/\"+\r\n \".btn { display: inline-block; padding: 1em; color: white; text-decoration: none;}\/*MTPART*\/\"+\r\n \".btn-success {@extend .btn; background-color: $primary-color; @include rounded;}\/*MTPART*\/\"+\r\n \".btn-error {@extend .btn; background-color: $secondary-color;}\/*MTPART*\/\",\r\n\r\n cssParts: [\r\n \"\/*MTFILE*\/\/*MTPART*\/\",\r\n \".btn, .btn-success, .btn-error { display: inline-block; padding: 1em; color: white; text-decoration: none;}\/*MTPART*\/\",\r\n \".btn-success { background-color: green; border-radius: 999px;}\/*MTPART*\/\",\r\n \".btn-error { background-color: blue;}\/*MTPART*\/\"\r\n ]\r\n};<\/code><\/pre>\nWhy were four resources included?<\/h4>\n\nfull_code<\/code>: The toRem<\/code> Sass entity changed and the full_code<\/code> resource contains the toRem<\/code> function declaration.<\/li>\n.btn<\/code>: the selector was edited.<\/li>\n.btn-success<\/code>: Uses @extend .btn<\/code> and so it must always compile with .btn<\/code>. The combined selector becomes .btn, .btn-success<\/code>.<\/li>\n.btn-error<\/code>: This also uses @extend .btn<\/code> and so must be included for the same reasons as .btn-success<\/code>.<\/li>\n<\/ol>\nTwo selectors are not included because they are not related to the .btn<\/code> selector.<\/p>\n\n.entry-title<\/code><\/li>\n.btn-success<\/code> (inside the media query)<\/li>\n<\/ol>\nRecursive resource gathering<\/h3>\n
Aside from the data structure, the most time consuming challenge was figuring out how to pull in the right subset of Sass code. When one piece of code connects to another piece, we need to check for connections on the second piece too. There is a chain reaction. To support this, the following gatherCompileResources<\/code> function is recursive<\/em>.<\/p>\n\n- We loop the
connectedEntities<\/code> object down to the level of Sass entity names.<\/li>\n- We use recursion if a function or mixin has side effects (like updating global variables).<\/li>\n
- The
checkObject<\/code> function returns the value of an object at a particular depth, or false if no value exists.<\/li>\n- The
updateObject<\/code> function sets the value of an object at a particular depth.<\/li>\n- We add dependent resources to the
compileParts<\/code> array, using absoluteIndex<\/code> as the key.<\/li>\n- Microthemer calculates
absoluteIndex<\/code> by adding the folder index to the selector index. This works because folder indexes increment in hundreds, and the maximum number of selectors per folder is 40, which is fewer than one hundred.<\/li>\n- We use recursion if resources added to the
compileParts<\/code> array also have co-dependent relationships.<\/li>\n<\/ul>\nfunction gatherCompileResources(compileResources, connectedEntities, projectEntities, projectCode, config){\r\n\r\n let compileParts = compileResources.compileParts;\r\n\r\n \/\/ reasons: changed \/ coDependent \/ related\r\n const reasons = Object.keys(connectedEntities);\r\n for (const reason of reasons) {\r\n\r\n \/\/ types: variable \/ function \/ mixin \/ extend\r\n const types = Object.keys(connectedEntities[reason]);\r\n for (const type of types) {\r\n\r\n \/\/ names: e.g. toRem \/ .btn \/ primary_color\r\n const names = Object.keys(connectedEntities[reason][type]);\r\n for (const name of names) {\r\n\r\n \/\/ check side-effects for Sass entity (if not checked already)\r\n if (!checkObject(config.relatedChecked, [type, name])){\r\n\r\n updateObject(config.relatedChecked, [type, name], 1);\r\n\r\n const relatedEntities = checkObject(projectEntities, [type, name, 'relatedEntities']);\r\n if (relatedEntities){\r\n compileParts = gatherCompileResources(\r\n compileResources, { related: relatedEntities }, projectEntities, projectCode, config\r\n );\r\n }\r\n }\r\n\r\n \/\/ check if there are dependent pieces of code\r\n const itemDeps = checkObject(projectEntities, [type, name, 'itemDeps']);\r\n if (itemDeps && itemDeps.length > 0){\r\n\r\n for (const dep of itemDeps) {\r\n\r\n let path = dep.path,\r\n resourceID = path.join('.');\r\n\r\n if (!config.resourceAdded[resourceID]){\r\n\r\n \/\/ if we have a valid resource\r\n let resource = checkObject(projectCode, path);\r\n if (resource){\r\n\r\n config.resourceAdded[resourceID] = 1;\r\n\r\n \/\/ get folder index + resource index\r\n let absoluteIndex = getAbsoluteIndex(path);\r\n\r\n \/\/ add compile part\r\n compileParts[absoluteIndex] = {\r\n sassCode: resource.sassCode,\r\n mediaQuery: resource.mediaQuery,\r\n path: path\r\n };\r\n \r\n \/\/ if resource is co-dependent, pull in others\r\n let coDependent = getCoDependent(resource);\r\n if (coDependent){\r\n compileParts = gatherCompileResources(\r\n compileResources, { coDependent: coDependent }, projectEntities, projectCode, config\r\n );\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n return compileParts;\r\n}<\/code><\/pre>\nThe application process<\/h3>\n
We\u2019ve covered the technical aspects now. To see how it all ties together, let\u2019s walk through the data processing steps. <\/p>\n
From keystroke to style render<\/h4>\n\n- A user keystroke fires the textarea change event.<\/li>\n
- We convert the single selector being edited into a
sassEntities<\/code> object. This allows for comparison with the pre-edit Sass entities: projectCode > dataItem > sassEntities<\/code>.<\/li>\n- \n If any Sass entities changed:<\/p>\n
\n- We update
projectCode > dataItem > sassEntities<\/code>.<\/li>\n- If an
@extend<\/code> rule changed:\n\n- We search the
projectCode<\/code> object to find matching selectors.<\/li>\n- We store the
path<\/code> for matching selectors on the current data item: projectCode > dataItem > sassEntities > extend > target > [ path ]<\/code>.<\/li>\n<\/ul>\n<\/li>\n- We rebuild the
projectEntities<\/code> object by looping over the projectCode<\/code> object.<\/li>\n- We populate
connectedEntities > changed<\/code> with the change analysis.<\/li>\n- \n If
extend<\/code>, variable<\/code>, function<\/code>, or mixin<\/code> entities are present:<\/p>\n\n- We populate
connectedEntities > coDependent<\/code> with the relevant entities.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<\/li>\n- The recursive
gatherCompileResources<\/code> function uses the connectedEntities<\/code> object to populate the compileResources<\/code> object.<\/li>\n- We concatenate the