Repository: postcss/autoprefixer
Pull request #992 (2018-02-22 12:13:00)

[grid-template] Added support for grid-template shortcut 
This PR is cleaner and don't commits from my previous PR.

evgeny-petukhov: @ai Please, review.

Patch:
diff --git a/lib/hacks/grid-area.js b/lib/hacks/grid-area.js
index d863e9e..7885361 100644
--- a/lib/hacks/grid-area.js
+++ b/lib/hacks/grid-area.js
@@ -1,5 +1,5 @@
 const Declaration = require('../declaration');
-const shorthand = require('./grid-shorthand');
+const utils = require('./grid-utils');
 
 class GridArea extends Declaration {
 
@@ -15,10 +15,10 @@ class GridArea extends Declaration {
             return undefined;
         }
 
-        const values = shorthand.parse(decl);
+        const values = utils.parse(decl);
 
-        const [rowStart, rowSpan] = shorthand.translate(values, 0, 2);
-        const [columnStart, columnSpan] = shorthand.translate(values, 1, 3);
+        const [rowStart, rowSpan] = utils.translate(values, 0, 2);
+        const [columnStart, columnSpan] = utils.translate(values, 1, 3);
 
         if (rowStart) {
             decl.cloneBefore({
diff --git a/lib/hacks/grid-row-column.js b/lib/hacks/grid-row-column.js
index 29c327d..79ae746 100644
--- a/lib/hacks/grid-row-column.js
+++ b/lib/hacks/grid-row-column.js
@@ -1,5 +1,5 @@
 const Declaration = require('../declaration');
-const shorthand = require('./grid-shorthand');
+const utils = require('./grid-utils');
 
 class GridRowColumn extends Declaration {
 
@@ -11,8 +11,8 @@ class GridRowColumn extends Declaration {
     insert(decl, prefix, prefixes) {
         if (prefix !== '-ms-') return super.insert(decl, prefix, prefixes);
 
-        const values = shorthand.parse(decl);
-        const [start, span] = shorthand.translate(values, 0, 1);
+        const values = utils.parse(decl);
+        const [start, span] = utils.translate(values, 0, 1);
 
         if (start) {
             decl.cloneBefore({
diff --git a/lib/hacks/grid-rows-columns.js b/lib/hacks/grid-rows-columns.js
index 66411e8..5c552e5 100644
--- a/lib/hacks/grid-rows-columns.js
+++ b/lib/hacks/grid-rows-columns.js
@@ -1,5 +1,5 @@
 const Declaration = require('../declaration');
-const shorthand = require('./grid-shorthand');
+const { changeRepeat } = require('./grid-utils');
 
 class GridRowsColumns extends Declaration {
 
@@ -31,7 +31,7 @@ class GridRowsColumns extends Declaration {
      */
     set(decl, prefix) {
         if (prefix === '-ms-' && decl.value.indexOf('repeat(') !== -1) {
-            decl.value = shorthand.changeRepeat(decl.value);
+            decl.value = changeRepeat(decl.value);
         }
         return super.set(decl, prefix);
     }
diff --git a/lib/hacks/grid-shorthand.js b/lib/hacks/grid-shorthand.js
deleted file mode 100644
index 853c7e0..0000000
--- a/lib/hacks/grid-shorthand.js
+++ /dev/null
@@ -1,121 +0,0 @@
-const parser = require('postcss-value-parser');
-
-function convert(value) {
-    if (value &&
-        value.length === 2 &&
-        value[0] === 'span' &&
-        parseInt(value[1], 10) > 0
-    ) {
-        return [false, parseInt(value[1], 10)];
-    }
-
-    if (value &&
-        value.length === 1 &&
-        parseInt(value[0], 10) > 0
-    ) {
-        return [parseInt(value[0], 10), false];
-    }
-
-    return [false, false];
-}
-
-function translate(values, startIndex, endIndex) {
-    const startValue = values[startIndex];
-    const endValue = values[endIndex];
-
-    if (!startValue) {
-        return [false, false];
-    }
-
-    const [start, spanStart] = convert(startValue);
-    const [end, spanEnd] = convert(endValue);
-
-    if (start && !endValue) {
-        return [start, false];
-    }
-
-    if (spanStart && end) {
-        return [end - spanStart, spanStart];
-    }
-
-    if (start && spanEnd) {
-        return [start, spanEnd];
-    }
-
-    if (start && end) {
-        return [start, end - start];
-    }
-
-    return [false, false];
-}
-
-function parse(decl) {
-    const node = parser(decl.value);
-
-    let values = [];
-    let current = 0;
-    values[current] = [];
-
-    for (const i of node.nodes) {
-        if (i.type === 'div') {
-            current += 1;
-            values[current] = [];
-        } else if (i.type === 'word') {
-            values[current].push(i.value);
-        }
-    }
-
-    return values;
-}
-
-function parseTemplateShortcut(decl) {
-    const node = parser(decl.value, { loose: true });
-
-    let values = [];
-    let current = 0;
-    values[current] = [];
-
-    for (const i of node.nodes) {
-        if (i.type === 'div') {
-            current += 1;
-            values[current] = [];
-        } else {
-            values[current].push(parser.stringify(i));
-        }
-    }
-
-    return values;
-}
-
-function walkRepeat(node) {
-    const fixed = [];
-    for (const i of node.nodes) {
-        if (i.nodes) {
-            walkRepeat(i);
-        }
-        fixed.push(i);
-        if (i.type === 'function' && i.value === 'repeat') {
-            const first = i.nodes.shift();
-            if (first) {
-                const count = first.value;
-                i.nodes.shift();
-                i.value = '';
-                fixed.push({ type: 'word', value: `[${count}]` });
-            }
-        }
-    }
-    node.nodes = fixed;
-}
-
-function changeRepeat(value) {
-    const ast = parser(value);
-    walkRepeat(ast);
-    return ast.toString();
-}
-
-module.exports = {
-    parse,
-    translate,
-    changeRepeat,
-    parseTemplateShortcut
-};
diff --git a/lib/hacks/grid-template-areas.js b/lib/hacks/grid-template-areas.js
index 0c84f46..3a2afd7 100644
--- a/lib/hacks/grid-template-areas.js
+++ b/lib/hacks/grid-template-areas.js
@@ -1,104 +1,27 @@
 const Declaration = require('../declaration');
+const {
+    parseGridAreas,
+    insertAreas
+} = require('./grid-utils');
 
-const DOTS = /^\.+$/;
 
-function track(start, end) {
-    return { start, end, span: end - start };
-}
-
-function getRows(tpl) {
+function getGridRows(tpl) {
     return tpl.trim().slice(1, -1).split(/['"]\s*['"]?/g);
 }
 
-function getColumns(line) {
-    return line.trim().split(/\s+/g);
-}
-
-function parseGridAreas(tpl) {
-    return getRows(tpl).reduce((areas, line, rowIndex) => {
-        if (line.trim() === '') return areas;
-        getColumns(line).forEach((area, columnIndex) => {
-            if (DOTS.test(area)) return;
-            if (typeof areas[area] === 'undefined') {
-                areas[area] = {
-                    column: track(columnIndex + 1, columnIndex + 2),
-                    row: track(rowIndex + 1, rowIndex + 2)
-                };
-            } else {
-                const { column, row } = areas[area];
-
-                column.start = Math.min(column.start, columnIndex + 1);
-                column.end = Math.max(column.end, columnIndex + 2);
-                column.span = column.end - column.start;
-
-                row.start = Math.min(row.start, rowIndex + 1);
-                row.end = Math.max(row.end, rowIndex + 2);
-                row.span = row.end - row.start;
-            }
-        });
-        return areas;
-    }, {});
-}
-
 class GridTemplateAreas extends Declaration {
 
     static names = ['grid-template-areas'];
 
-    getRoot(parent) {
-        if (parent.type === 'atrule' || !parent.parent)  {
-            return parent;
-        }
-        return this.getRoot(parent.parent);
-    }
-
     /**
      * Translate grid-template-areas to separate -ms- prefixed properties
      */
     insert(decl, prefix, prefixes, result) {
         if (prefix !== '-ms-') return super.insert(decl, prefix, prefixes);
 
-        const areas = parseGridAreas(decl.value);
-        let missed = Object.keys(areas);
-
-        this.getRoot(decl.parent).walkDecls('grid-area', gridArea => {
-            const value = gridArea.value;
-            const area = areas[value];
-
-            missed = missed.filter(e => e !== value);
-
-            if (area) {
-                gridArea.parent
-                    .walkDecls(/-ms-grid-(row|column)/, (d) => {
-                        d.remove();
-                    });
-
-                gridArea.cloneBefore({
-                    prop: '-ms-grid-row',
-                    value: String(area.row.start)
-                });
-                if (area.row.span > 1) {
-                    gridArea.cloneBefore({
-                        prop: '-ms-grid-row-span',
-                        value: String(area.row.span)
-                    });
-                }
-                gridArea.cloneBefore({
-                    prop: '-ms-grid-column',
-                    value: String(area.column.start)
-                });
-                if (area.column.span > 1) {
-                    gridArea.cloneBefore({
-                        prop: '-ms-grid-column-span',
-                        value: String(area.column.span)
-                    });
-                }
-            }
-            return undefined;
-        });
+        const areas = parseGridAreas(getGridRows(decl.value));
 
-        if (missed.length > 0) {
-            decl.warn(result, 'Can not find grid areas: ' + missed.join(', '));
-        }
+        insertAreas(areas, decl, result);
 
         return decl;
     }
diff --git a/lib/hacks/grid-template.js b/lib/hacks/grid-template.js
index 0e3c474..07400e0 100644
--- a/lib/hacks/grid-template.js
+++ b/lib/hacks/grid-template.js
@@ -1,5 +1,8 @@
 const Declaration = require('../declaration');
-const shorthand = require('./grid-shorthand');
+const {
+    parseTemplate,
+    insertAreas
+} = require('./grid-utils');
 
 class GridTemplate extends Declaration {
 
@@ -7,45 +10,42 @@ class GridTemplate extends Declaration {
         'grid-template'
     ];
 
-    /**
-     * Do not add prefix for unsupported value in IE
-     */
-    check(decl) {
-        return decl.value.includes('/') &&
-            !decl.value.includes('[') &&
-            !decl.value.includes('"') &&
-            !decl.value.includes('\'');
-    }
 
     /**
      * Translate grid-template to separate -ms- prefixed properties
      */
-    insert(decl, prefix, prefixes) {
+    insert(decl, prefix, prefixes, result) {
         if (prefix !== '-ms-') return super.insert(decl, prefix, prefixes);
 
         if (decl.parent.some(i => i.prop === '-ms-grid-rows')) {
             return undefined;
         }
 
-        const [
-            templateRows,
-            templateColumns
-        ] = shorthand.parseTemplateShortcut(decl);
+        const {
+            rows,
+            columns,
+            areas
+        } = parseTemplate(decl);
+        const hasAreas = Object.keys(areas).length > 0;
 
-        if (templateRows) {
+        if (rows && columns || hasAreas) {
             decl.cloneBefore({
                 prop: '-ms-grid-rows',
-                value: shorthand.changeRepeat(templateRows.join(''))
+                value: rows
             });
         }
 
-        if (templateColumns) {
+        if (columns) {
             decl.cloneBefore({
                 prop: '-ms-grid-columns',
-                value: shorthand.changeRepeat(templateColumns.join(''))
+                value: columns
             });
         }
 
+        if (hasAreas) {
+            insertAreas(areas, decl, result);
+        }
+
         return decl;
     }
 }
diff --git a/lib/hacks/grid-utils.js b/lib/hacks/grid-utils.js
new file mode 100644
index 0000000..a130f7b
--- /dev/null
+++ b/lib/hacks/grid-utils.js
@@ -0,0 +1,252 @@
+const parser = require('postcss-value-parser');
+
+function convert(value) {
+    if (value &&
+        value.length === 2 &&
+        value[0] === 'span' &&
+        parseInt(value[1], 10) > 0
+    ) {
+        return [false, parseInt(value[1], 10)];
+    }
+
+    if (value &&
+        value.length === 1 &&
+        parseInt(value[0], 10) > 0
+    ) {
+        return [parseInt(value[0], 10), false];
+    }
+
+    return [false, false];
+}
+
+function translate(values, startIndex, endIndex) {
+    const startValue = values[startIndex];
+    const endValue = values[endIndex];
+
+    if (!startValue) {
+        return [false, false];
+    }
+
+    const [start, spanStart] = convert(startValue);
+    const [end, spanEnd] = convert(endValue);
+
+    if (start && !endValue) {
+        return [start, false];
+    }
+
+    if (spanStart && end) {
+        return [end - spanStart, spanStart];
+    }
+
+    if (start && spanEnd) {
+        return [start, spanEnd];
+    }
+
+    if (start && end) {
+        return [start, end - start];
+    }
+
+    return [false, false];
+}
+
+function parse(decl) {
+    const node = parser(decl.value);
+
+    let values = [];
+    let current = 0;
+    values[current] = [];
+
+    for (const i of node.nodes) {
+        if (i.type === 'div') {
+            current += 1;
+            values[current] = [];
+        } else if (i.type === 'word') {
+            values[current].push(i.value);
+        }
+    }
+
+    return values;
+}
+
+// Transform repeat
+
+function transformRepeat({ nodes }) {
+    const repeat = nodes.reduce((result, node) => {
+        if (node.type === 'div' && node.value === ',') {
+            result.key = 'function';
+        } else {
+            result[result.key].push(parser.stringify(node));
+        }
+        return result;
+    }, {
+        key: 'count',
+        function: [],
+        count: []
+    });
+    return `(${repeat.function.join('')})[${repeat.count.join('')}]`;
+}
+
+function changeRepeat(value) {
+    const result = parser(value)
+        .nodes
+        .map(i => {
+            if (i.type === 'function' && i.value === 'repeat') {
+                return {
+                    type: 'word',
+                    value: transformRepeat(i)
+                };
+            }
+            return i;
+        });
+    return parser.stringify(result);
+}
+
+
+// Parse grid-template-areas
+
+const DOTS = /^\.+$/;
+
+function track(start, end) {
+    return { start, end, span: end - start };
+}
+
+function getColumns(line) {
+    return line.trim().split(/\s+/g);
+}
+
+function parseGridAreas(rows) {
+    return rows.reduce((areas, line, rowIndex) => {
+        if (line.trim() === '') return areas;
+        getColumns(line).forEach((area, columnIndex) => {
+            if (DOTS.test(area)) return;
+            if (typeof areas[area] === 'undefined') {
+                areas[area] = {
+                    column: track(columnIndex + 1, columnIndex + 2),
+                    row: track(rowIndex + 1, rowIndex + 2)
+                };
+            } else {
+                const { column, row } = areas[area];
+
+                column.start = Math.min(column.start, columnIndex + 1);
+                column.end = Math.max(column.end, columnIndex + 2);
+                column.span = column.end - column.start;
+
+                row.start = Math.min(row.start, rowIndex + 1);
+                row.end = Math.max(row.end, rowIndex + 2);
+                row.span = row.end - row.start;
+            }
+        });
+        return areas;
+    }, {});
+}
+
+
+// Parse grid-template
+
+function testTrack(node) {
+    return node.type === 'word' && /^\[.+\]$/.test(node.value);
+}
+
+function parseTemplate(decl) {
+    const gridTemplate = parser(decl.value)
+        .nodes
+        .reduce((result, node) => {
+            const { type, value } = node;
+
+            if (testTrack(node) || type === 'space') return result;
+
+            // area
+            if (type === 'string') {
+                result.areas.push(value);
+            }
+
+            // values and function
+            if (type === 'word' || type === 'function') {
+                if (type === 'function' && value === 'repeat') {
+                    result[result.key].push(transformRepeat(node));
+                } else {
+                    result[result.key].push(parser.stringify(node));
+                }
+            }
+
+            // devider(/)
+            if (type === 'div' && value === '/') {
+                result.key = 'columns';
+            }
+
+            return result;
+        }, {
+            key: 'rows',
+            columns: [],
+            rows: [],
+            areas: []
+        });
+    return {
+        areas: parseGridAreas(gridTemplate.areas),
+        columns: gridTemplate.columns.join(' '),
+        rows: gridTemplate.rows.join(' ')
+    };
+}
+
+
+// Insert parsed grid areas
+
+function getRoot(parent) {
+    if (parent.type === 'atrule' || !parent.parent)  {
+        return parent;
+    }
+    return getRoot(parent.parent);
+}
+
+function insertAreas(areas, decl, result) {
+    let missed = Object.keys(areas);
+
+    getRoot(decl.parent).walkDecls('grid-area', gridArea => {
+        const value = gridArea.value;
+        const area = areas[value];
+
+        missed = missed.filter(e => e !== value);
+
+        if (area) {
+            gridArea.parent
+                .walkDecls(/-ms-grid-(row|column)/, (d) => {
+                    d.remove();
+                });
+
+            gridArea.cloneBefore({
+                prop: '-ms-grid-row',
+                value: String(area.row.start)
+            });
+            if (area.row.span > 1) {
+                gridArea.cloneBefore({
+                    prop: '-ms-grid-row-span',
+                    value: String(area.row.span)
+                });
+            }
+            gridArea.cloneBefore({
+                prop: '-ms-grid-column',
+                value: String(area.column.start)
+            });
+            if (area.column.span > 1) {
+                gridArea.cloneBefore({
+                    prop: '-ms-grid-column-span',
+                    value: String(area.column.span)
+                });
+            }
+        }
+        return undefined;
+    });
+
+    if (missed.length > 0) {
+        decl.warn(result, 'Can not find grid areas: ' + missed.join(', '));
+    }
+}
+
+module.exports = {
+    parse,
+    translate,
+    changeRepeat,
+    parseTemplate,
+    parseGridAreas,
+    insertAreas
+};


Test patch:
diff --git a/test/cases/grid-template.css b/test/cases/grid-template.css
index 86f26ed..145fc2d 100644
--- a/test/cases/grid-template.css
+++ b/test/cases/grid-template.css
@@ -7,15 +7,15 @@
         [header-left] "head head" 30px [header-right]
         [main-left]   "nav  main" 1fr  [main-right]
         [footer-left] "nav  foot" 30px [footer-right]
-        / 120px 1fr;
+        / 120px repeat(4, 250px 10px);
 }
 
 .c {
     grid-template:
-        "hd hd hd hd   hd   hd   hd   hd   hd" minmax(100px, auto)
-        "sd sd sd main main main main main main" minmax(100px, auto)
-        "ft ft ft ft   ft   ft   ft   ft   ft" minmax(100px, auto)
-                / 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr ;
+        "hd hd hd hd hd hd hd hd hd" minmax(100px, auto)
+        "sd sd sd mn mn mn mn mn mn" minmax(100px, auto)
+        "ft ft ft ft ft ft ft ft ft" minmax(100px, auto)
+                / 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
 }
 
 .d {
@@ -25,3 +25,35 @@
 .e {
     grid-template: 10px;
 }
+
+.head {
+    grid-area: head;
+}
+
+.nav {
+    grid-area: nav;
+}
+
+.main {
+    grid-area: main;
+}
+
+.foot {
+    grid-area: foot;
+}
+
+.hd {
+    grid-area: hd;
+}
+
+.sd {
+    grid-area: sd;
+}
+
+.mn {
+    grid-area: mn;
+}
+
+.ft {
+    grid-area: ft;
+}
diff --git a/test/cases/grid-template.out.css b/test/cases/grid-template.out.css
index c3f342f..1d35569 100644
--- a/test/cases/grid-template.out.css
+++ b/test/cases/grid-template.out.css
@@ -5,19 +5,27 @@
 }
 
 .b {
+    -ms-grid-rows:
+        30px 1fr 30px;
+    -ms-grid-columns:
+        120px (250px 10px)[4];
     grid-template:
         [header-left] "head head" 30px [header-right]
         [main-left]   "nav  main" 1fr  [main-right]
         [footer-left] "nav  foot" 30px [footer-right]
-        / 120px 1fr;
+        / 120px repeat(4, 250px 10px);
 }
 
 .c {
+    -ms-grid-rows:
+        minmax(100px, auto) minmax(100px, auto) minmax(100px, auto);
+    -ms-grid-columns:
+        1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
     grid-template:
-        "hd hd hd hd   hd   hd   hd   hd   hd" minmax(100px, auto)
-        "sd sd sd main main main main main main" minmax(100px, auto)
-        "ft ft ft ft   ft   ft   ft   ft   ft" minmax(100px, auto)
-                / 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr ;
+        "hd hd hd hd hd hd hd hd hd" minmax(100px, auto)
+        "sd sd sd mn mn mn mn mn mn" minmax(100px, auto)
+        "ft ft ft ft ft ft ft ft ft" minmax(100px, auto)
+                / 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
 }
 
 .d {
@@ -29,3 +37,57 @@
 .e {
     grid-template: 10px;
 }
+
+.head {
+    -ms-grid-row: 1;
+    -ms-grid-column: 1;
+    -ms-grid-column-span: 2;
+    grid-area: head;
+}
+
+.nav {
+    -ms-grid-row: 2;
+    -ms-grid-row-span: 2;
+    -ms-grid-column: 1;
+    grid-area: nav;
+}
+
+.main {
+    -ms-grid-row: 2;
+    -ms-grid-column: 2;
+    grid-area: main;
+}
+
+.foot {
+    -ms-grid-row: 3;
+    -ms-grid-column: 2;
+    grid-area: foot;
+}
+
+.hd {
+    -ms-grid-row: 1;
+    -ms-grid-column: 1;
+    -ms-grid-column-span: 9;
+    grid-area: hd;
+}
+
+.sd {
+    -ms-grid-row: 2;
+    -ms-grid-column: 1;
+    -ms-grid-column-span: 3;
+    grid-area: sd;
+}
+
+.mn {
+    -ms-grid-row: 2;
+    -ms-grid-column: 4;
+    -ms-grid-column-span: 6;
+    grid-area: mn;
+}
+
+.ft {
+    -ms-grid-row: 3;
+    -ms-grid-column: 1;
+    -ms-grid-column-span: 9;
+    grid-area: ft;
+}