grid-utils.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113
  1. let parser = require('postcss-value-parser')
  2. let list = require('postcss').list
  3. let uniq = require('../utils').uniq
  4. let escapeRegexp = require('../utils').escapeRegexp
  5. let splitSelector = require('../utils').splitSelector
  6. function convert(value) {
  7. if (
  8. value &&
  9. value.length === 2 &&
  10. value[0] === 'span' &&
  11. parseInt(value[1], 10) > 0
  12. ) {
  13. return [false, parseInt(value[1], 10)]
  14. }
  15. if (value && value.length === 1 && parseInt(value[0], 10) > 0) {
  16. return [parseInt(value[0], 10), false]
  17. }
  18. return [false, false]
  19. }
  20. exports.translate = translate
  21. function translate(values, startIndex, endIndex) {
  22. let startValue = values[startIndex]
  23. let endValue = values[endIndex]
  24. if (!startValue) {
  25. return [false, false]
  26. }
  27. let [start, spanStart] = convert(startValue)
  28. let [end, spanEnd] = convert(endValue)
  29. if (start && !endValue) {
  30. return [start, false]
  31. }
  32. if (spanStart && end) {
  33. return [end - spanStart, spanStart]
  34. }
  35. if (start && spanEnd) {
  36. return [start, spanEnd]
  37. }
  38. if (start && end) {
  39. return [start, end - start]
  40. }
  41. return [false, false]
  42. }
  43. exports.parse = parse
  44. function parse(decl) {
  45. let node = parser(decl.value)
  46. let values = []
  47. let current = 0
  48. values[current] = []
  49. for (let i of node.nodes) {
  50. if (i.type === 'div') {
  51. current += 1
  52. values[current] = []
  53. } else if (i.type === 'word') {
  54. values[current].push(i.value)
  55. }
  56. }
  57. return values
  58. }
  59. exports.insertDecl = insertDecl
  60. function insertDecl(decl, prop, value) {
  61. if (value && !decl.parent.some(i => i.prop === `-ms-${prop}`)) {
  62. decl.cloneBefore({
  63. prop: `-ms-${prop}`,
  64. value: value.toString()
  65. })
  66. }
  67. }
  68. // Track transforms
  69. exports.prefixTrackProp = prefixTrackProp
  70. function prefixTrackProp({ prop, prefix }) {
  71. return prefix + prop.replace('template-', '')
  72. }
  73. function transformRepeat({ nodes }, { gap }) {
  74. let { count, size } = nodes.reduce(
  75. (result, node) => {
  76. if (node.type === 'div' && node.value === ',') {
  77. result.key = 'size'
  78. } else {
  79. result[result.key].push(parser.stringify(node))
  80. }
  81. return result
  82. },
  83. {
  84. key: 'count',
  85. size: [],
  86. count: []
  87. }
  88. )
  89. // insert gap values
  90. if (gap) {
  91. size = size.filter(i => i.trim())
  92. let val = []
  93. for (let i = 1; i <= count; i++) {
  94. size.forEach((item, index) => {
  95. if (index > 0 || i > 1) {
  96. val.push(gap)
  97. }
  98. val.push(item)
  99. })
  100. }
  101. return val.join(' ')
  102. }
  103. return `(${size.join('')})[${count.join('')}]`
  104. }
  105. exports.prefixTrackValue = prefixTrackValue
  106. function prefixTrackValue({ value, gap }) {
  107. let result = parser(value).nodes.reduce((nodes, node) => {
  108. if (node.type === 'function' && node.value === 'repeat') {
  109. return nodes.concat({
  110. type: 'word',
  111. value: transformRepeat(node, { gap })
  112. })
  113. }
  114. if (gap && node.type === 'space') {
  115. return nodes.concat(
  116. {
  117. type: 'space',
  118. value: ' '
  119. },
  120. {
  121. type: 'word',
  122. value: gap
  123. },
  124. node
  125. )
  126. }
  127. return nodes.concat(node)
  128. }, [])
  129. return parser.stringify(result)
  130. }
  131. // Parse grid-template-areas
  132. let DOTS = /^\.+$/
  133. function track(start, end) {
  134. return { start, end, span: end - start }
  135. }
  136. function getColumns(line) {
  137. return line.trim().split(/\s+/g)
  138. }
  139. exports.parseGridAreas = parseGridAreas
  140. function parseGridAreas({ rows, gap }) {
  141. return rows.reduce((areas, line, rowIndex) => {
  142. if (gap.row) rowIndex *= 2
  143. if (line.trim() === '') return areas
  144. getColumns(line).forEach((area, columnIndex) => {
  145. if (DOTS.test(area)) return
  146. if (gap.column) columnIndex *= 2
  147. if (typeof areas[area] === 'undefined') {
  148. areas[area] = {
  149. column: track(columnIndex + 1, columnIndex + 2),
  150. row: track(rowIndex + 1, rowIndex + 2)
  151. }
  152. } else {
  153. let { column, row } = areas[area]
  154. column.start = Math.min(column.start, columnIndex + 1)
  155. column.end = Math.max(column.end, columnIndex + 2)
  156. column.span = column.end - column.start
  157. row.start = Math.min(row.start, rowIndex + 1)
  158. row.end = Math.max(row.end, rowIndex + 2)
  159. row.span = row.end - row.start
  160. }
  161. })
  162. return areas
  163. }, {})
  164. }
  165. // Parse grid-template
  166. function testTrack(node) {
  167. return node.type === 'word' && /^\[.+]$/.test(node.value)
  168. }
  169. function verifyRowSize(result) {
  170. if (result.areas.length > result.rows.length) {
  171. result.rows.push('auto')
  172. }
  173. return result
  174. }
  175. exports.parseTemplate = parseTemplate
  176. function parseTemplate({ decl, gap }) {
  177. let gridTemplate = parser(decl.value).nodes.reduce(
  178. (result, node) => {
  179. let { type, value } = node
  180. if (testTrack(node) || type === 'space') return result
  181. // area
  182. if (type === 'string') {
  183. result = verifyRowSize(result)
  184. result.areas.push(value)
  185. }
  186. // values and function
  187. if (type === 'word' || type === 'function') {
  188. result[result.key].push(parser.stringify(node))
  189. }
  190. // divider(/)
  191. if (type === 'div' && value === '/') {
  192. result.key = 'columns'
  193. result = verifyRowSize(result)
  194. }
  195. return result
  196. },
  197. {
  198. key: 'rows',
  199. columns: [],
  200. rows: [],
  201. areas: []
  202. }
  203. )
  204. return {
  205. areas: parseGridAreas({
  206. rows: gridTemplate.areas,
  207. gap
  208. }),
  209. columns: prefixTrackValue({
  210. value: gridTemplate.columns.join(' '),
  211. gap: gap.column
  212. }),
  213. rows: prefixTrackValue({
  214. value: gridTemplate.rows.join(' '),
  215. gap: gap.row
  216. })
  217. }
  218. }
  219. // Insert parsed grid areas
  220. /**
  221. * Get an array of -ms- prefixed props and values
  222. * @param {Object} [area] area object with column and row data
  223. * @param {Boolean} [addRowSpan] should we add grid-column-row value?
  224. * @param {Boolean} [addColumnSpan] should we add grid-column-span value?
  225. * @return {Array<Object>}
  226. */
  227. function getMSDecls(area, addRowSpan = false, addColumnSpan = false) {
  228. let result = [
  229. {
  230. prop: '-ms-grid-row',
  231. value: String(area.row.start)
  232. }
  233. ]
  234. if (area.row.span > 1 || addRowSpan) {
  235. result.push({
  236. prop: '-ms-grid-row-span',
  237. value: String(area.row.span)
  238. })
  239. }
  240. result.push({
  241. prop: '-ms-grid-column',
  242. value: String(area.column.start)
  243. })
  244. if (area.column.span > 1 || addColumnSpan) {
  245. result.push({
  246. prop: '-ms-grid-column-span',
  247. value: String(area.column.span)
  248. })
  249. }
  250. return result
  251. }
  252. function getParentMedia(parent) {
  253. if (parent.type === 'atrule' && parent.name === 'media') {
  254. return parent
  255. }
  256. if (!parent.parent) {
  257. return false
  258. }
  259. return getParentMedia(parent.parent)
  260. }
  261. /**
  262. * change selectors for rules with duplicate grid-areas.
  263. * @param {Array<Rule>} rules
  264. * @param {Array<String>} templateSelectors
  265. * @return {Array<Rule>} rules with changed selectors
  266. */
  267. function changeDuplicateAreaSelectors(ruleSelectors, templateSelectors) {
  268. ruleSelectors = ruleSelectors.map(selector => {
  269. let selectorBySpace = list.space(selector)
  270. let selectorByComma = list.comma(selector)
  271. if (selectorBySpace.length > selectorByComma.length) {
  272. selector = selectorBySpace.slice(-1).join('')
  273. }
  274. return selector
  275. })
  276. return ruleSelectors.map(ruleSelector => {
  277. let newSelector = templateSelectors.map((tplSelector, index) => {
  278. let space = index === 0 ? '' : ' '
  279. return `${space}${tplSelector} > ${ruleSelector}`
  280. })
  281. return newSelector
  282. })
  283. }
  284. /**
  285. * check if selector of rules are equal
  286. * @param {Rule} ruleA
  287. * @param {Rule} ruleB
  288. * @return {Boolean}
  289. */
  290. function selectorsEqual(ruleA, ruleB) {
  291. return ruleA.selectors.some(sel => {
  292. return ruleB.selectors.includes(sel)
  293. })
  294. }
  295. /**
  296. * Parse data from all grid-template(-areas) declarations
  297. * @param {Root} css css root
  298. * @return {Object} parsed data
  299. */
  300. function parseGridTemplatesData(css) {
  301. let parsed = []
  302. // we walk through every grid-template(-areas) declaration and store
  303. // data with the same area names inside the item
  304. css.walkDecls(/grid-template(-areas)?$/, d => {
  305. let rule = d.parent
  306. let media = getParentMedia(rule)
  307. let gap = getGridGap(d)
  308. let inheritedGap = inheritGridGap(d, gap)
  309. let { areas } = parseTemplate({ decl: d, gap: inheritedGap || gap })
  310. let areaNames = Object.keys(areas)
  311. // skip node if it doesn't have areas
  312. if (areaNames.length === 0) {
  313. return true
  314. }
  315. // check parsed array for item that include the same area names
  316. // return index of that item
  317. let index = parsed.reduce((acc, { allAreas }, idx) => {
  318. let hasAreas = allAreas && areaNames.some(area => allAreas.includes(area))
  319. return hasAreas ? idx : acc
  320. }, null)
  321. if (index !== null) {
  322. // index is found, add the grid-template data to that item
  323. let { allAreas, rules } = parsed[index]
  324. // check if rule has no duplicate area names
  325. let hasNoDuplicates = rules.some(r => {
  326. return r.hasDuplicates === false && selectorsEqual(r, rule)
  327. })
  328. let duplicatesFound = false
  329. // check need to gather all duplicate area names
  330. let duplicateAreaNames = rules.reduce((acc, r) => {
  331. if (!r.params && selectorsEqual(r, rule)) {
  332. duplicatesFound = true
  333. return r.duplicateAreaNames
  334. }
  335. if (!duplicatesFound) {
  336. areaNames.forEach(name => {
  337. if (r.areas[name]) {
  338. acc.push(name)
  339. }
  340. })
  341. }
  342. return uniq(acc)
  343. }, [])
  344. // update grid-row/column-span values for areas with duplicate
  345. // area names. @see #1084 and #1146
  346. rules.forEach(r => {
  347. areaNames.forEach(name => {
  348. let area = r.areas[name]
  349. if (area && area.row.span !== areas[name].row.span) {
  350. areas[name].row.updateSpan = true
  351. }
  352. if (area && area.column.span !== areas[name].column.span) {
  353. areas[name].column.updateSpan = true
  354. }
  355. })
  356. })
  357. parsed[index].allAreas = uniq([...allAreas, ...areaNames])
  358. parsed[index].rules.push({
  359. hasDuplicates: !hasNoDuplicates,
  360. params: media.params,
  361. selectors: rule.selectors,
  362. node: rule,
  363. duplicateAreaNames,
  364. areas
  365. })
  366. } else {
  367. // index is NOT found, push the new item to the parsed array
  368. parsed.push({
  369. allAreas: areaNames,
  370. areasCount: 0,
  371. rules: [
  372. {
  373. hasDuplicates: false,
  374. duplicateRules: [],
  375. params: media.params,
  376. selectors: rule.selectors,
  377. node: rule,
  378. duplicateAreaNames: [],
  379. areas
  380. }
  381. ]
  382. })
  383. }
  384. return undefined
  385. })
  386. return parsed
  387. }
  388. /**
  389. * insert prefixed grid-area declarations
  390. * @param {Root} css css root
  391. * @param {Function} isDisabled check if the rule is disabled
  392. * @return {void}
  393. */
  394. exports.insertAreas = insertAreas
  395. function insertAreas(css, isDisabled) {
  396. // parse grid-template declarations
  397. let gridTemplatesData = parseGridTemplatesData(css)
  398. // return undefined if no declarations found
  399. if (gridTemplatesData.length === 0) {
  400. return undefined
  401. }
  402. // we need to store the rules that we will insert later
  403. let rulesToInsert = {}
  404. css.walkDecls('grid-area', gridArea => {
  405. let gridAreaRule = gridArea.parent
  406. let hasPrefixedRow = gridAreaRule.first.prop === '-ms-grid-row'
  407. let gridAreaMedia = getParentMedia(gridAreaRule)
  408. if (isDisabled(gridArea)) {
  409. return undefined
  410. }
  411. let gridAreaRuleIndex = css.index(gridAreaMedia || gridAreaRule)
  412. let value = gridArea.value
  413. // found the data that matches grid-area identifier
  414. let data = gridTemplatesData.filter(d => d.allAreas.includes(value))[0]
  415. if (!data) {
  416. return true
  417. }
  418. let lastArea = data.allAreas[data.allAreas.length - 1]
  419. let selectorBySpace = list.space(gridAreaRule.selector)
  420. let selectorByComma = list.comma(gridAreaRule.selector)
  421. let selectorIsComplex =
  422. selectorBySpace.length > 1 &&
  423. selectorBySpace.length > selectorByComma.length
  424. // prevent doubling of prefixes
  425. if (hasPrefixedRow) {
  426. return false
  427. }
  428. // create the empty object with the key as the last area name
  429. // e.g if we have templates with "a b c" values, "c" will be the last area
  430. if (!rulesToInsert[lastArea]) {
  431. rulesToInsert[lastArea] = {}
  432. }
  433. let lastRuleIsSet = false
  434. // walk through every grid-template rule data
  435. for (let rule of data.rules) {
  436. let area = rule.areas[value]
  437. let hasDuplicateName = rule.duplicateAreaNames.includes(value)
  438. // if we can't find the area name, update lastRule and continue
  439. if (!area) {
  440. let lastRule = rulesToInsert[lastArea].lastRule
  441. let lastRuleIndex
  442. if (lastRule) {
  443. lastRuleIndex = css.index(lastRule)
  444. } else {
  445. /* c8 ignore next 2 */
  446. lastRuleIndex = -1
  447. }
  448. if (gridAreaRuleIndex > lastRuleIndex) {
  449. rulesToInsert[lastArea].lastRule = gridAreaMedia || gridAreaRule
  450. }
  451. continue
  452. }
  453. // for grid-templates inside media rule we need to create empty
  454. // array to push prefixed grid-area rules later
  455. if (rule.params && !rulesToInsert[lastArea][rule.params]) {
  456. rulesToInsert[lastArea][rule.params] = []
  457. }
  458. if ((!rule.hasDuplicates || !hasDuplicateName) && !rule.params) {
  459. // grid-template has no duplicates and not inside media rule
  460. getMSDecls(area, false, false)
  461. .reverse()
  462. .forEach(i =>
  463. gridAreaRule.prepend(
  464. Object.assign(i, {
  465. raws: {
  466. between: gridArea.raws.between
  467. }
  468. })
  469. )
  470. )
  471. rulesToInsert[lastArea].lastRule = gridAreaRule
  472. lastRuleIsSet = true
  473. } else if (rule.hasDuplicates && !rule.params && !selectorIsComplex) {
  474. // grid-template has duplicates and not inside media rule
  475. let cloned = gridAreaRule.clone()
  476. cloned.removeAll()
  477. getMSDecls(area, area.row.updateSpan, area.column.updateSpan)
  478. .reverse()
  479. .forEach(i =>
  480. cloned.prepend(
  481. Object.assign(i, {
  482. raws: {
  483. between: gridArea.raws.between
  484. }
  485. })
  486. )
  487. )
  488. cloned.selectors = changeDuplicateAreaSelectors(
  489. cloned.selectors,
  490. rule.selectors
  491. )
  492. if (rulesToInsert[lastArea].lastRule) {
  493. rulesToInsert[lastArea].lastRule.after(cloned)
  494. }
  495. rulesToInsert[lastArea].lastRule = cloned
  496. lastRuleIsSet = true
  497. } else if (
  498. rule.hasDuplicates &&
  499. !rule.params &&
  500. selectorIsComplex &&
  501. gridAreaRule.selector.includes(rule.selectors[0])
  502. ) {
  503. // grid-template has duplicates and not inside media rule
  504. // and the selector is complex
  505. gridAreaRule.walkDecls(/-ms-grid-(row|column)/, d => d.remove())
  506. getMSDecls(area, area.row.updateSpan, area.column.updateSpan)
  507. .reverse()
  508. .forEach(i =>
  509. gridAreaRule.prepend(
  510. Object.assign(i, {
  511. raws: {
  512. between: gridArea.raws.between
  513. }
  514. })
  515. )
  516. )
  517. } else if (rule.params) {
  518. // grid-template is inside media rule
  519. // if we're inside media rule, we need to store prefixed rules
  520. // inside rulesToInsert object to be able to preserve the order of media
  521. // rules and merge them easily
  522. let cloned = gridAreaRule.clone()
  523. cloned.removeAll()
  524. getMSDecls(area, area.row.updateSpan, area.column.updateSpan)
  525. .reverse()
  526. .forEach(i =>
  527. cloned.prepend(
  528. Object.assign(i, {
  529. raws: {
  530. between: gridArea.raws.between
  531. }
  532. })
  533. )
  534. )
  535. if (rule.hasDuplicates && hasDuplicateName) {
  536. cloned.selectors = changeDuplicateAreaSelectors(
  537. cloned.selectors,
  538. rule.selectors
  539. )
  540. }
  541. cloned.raws = rule.node.raws
  542. if (css.index(rule.node.parent) > gridAreaRuleIndex) {
  543. // append the prefixed rules right inside media rule
  544. // with grid-template
  545. rule.node.parent.append(cloned)
  546. } else {
  547. // store the rule to insert later
  548. rulesToInsert[lastArea][rule.params].push(cloned)
  549. }
  550. // set new rule as last rule ONLY if we didn't set lastRule for
  551. // this grid-area before
  552. if (!lastRuleIsSet) {
  553. rulesToInsert[lastArea].lastRule = gridAreaMedia || gridAreaRule
  554. }
  555. }
  556. }
  557. return undefined
  558. })
  559. // append stored rules inside the media rules
  560. Object.keys(rulesToInsert).forEach(area => {
  561. let data = rulesToInsert[area]
  562. let lastRule = data.lastRule
  563. Object.keys(data)
  564. .reverse()
  565. .filter(p => p !== 'lastRule')
  566. .forEach(params => {
  567. if (data[params].length > 0 && lastRule) {
  568. lastRule.after({ name: 'media', params })
  569. lastRule.next().append(data[params])
  570. }
  571. })
  572. })
  573. return undefined
  574. }
  575. /**
  576. * Warn user if grid area identifiers are not found
  577. * @param {Object} areas
  578. * @param {Declaration} decl
  579. * @param {Result} result
  580. * @return {void}
  581. */
  582. exports.warnMissedAreas = warnMissedAreas
  583. function warnMissedAreas(areas, decl, result) {
  584. let missed = Object.keys(areas)
  585. decl.root().walkDecls('grid-area', gridArea => {
  586. missed = missed.filter(e => e !== gridArea.value)
  587. })
  588. if (missed.length > 0) {
  589. decl.warn(result, 'Can not find grid areas: ' + missed.join(', '))
  590. }
  591. return undefined
  592. }
  593. /**
  594. * compare selectors with grid-area rule and grid-template rule
  595. * show warning if grid-template selector is not found
  596. * (this function used for grid-area rule)
  597. * @param {Declaration} decl
  598. * @param {Result} result
  599. * @return {void}
  600. */
  601. exports.warnTemplateSelectorNotFound = warnTemplateSelectorNotFound
  602. function warnTemplateSelectorNotFound(decl, result) {
  603. let rule = decl.parent
  604. let root = decl.root()
  605. let duplicatesFound = false
  606. // slice selector array. Remove the last part (for comparison)
  607. let slicedSelectorArr = list
  608. .space(rule.selector)
  609. .filter(str => str !== '>')
  610. .slice(0, -1)
  611. // we need to compare only if selector is complex.
  612. // e.g '.grid-cell' is simple, but '.parent > .grid-cell' is complex
  613. if (slicedSelectorArr.length > 0) {
  614. let gridTemplateFound = false
  615. let foundAreaSelector = null
  616. root.walkDecls(/grid-template(-areas)?$/, d => {
  617. let parent = d.parent
  618. let templateSelectors = parent.selectors
  619. let { areas } = parseTemplate({ decl: d, gap: getGridGap(d) })
  620. let hasArea = areas[decl.value]
  621. // find the the matching selectors
  622. for (let tplSelector of templateSelectors) {
  623. if (gridTemplateFound) {
  624. break
  625. }
  626. let tplSelectorArr = list.space(tplSelector).filter(str => str !== '>')
  627. gridTemplateFound = tplSelectorArr.every(
  628. (item, idx) => item === slicedSelectorArr[idx]
  629. )
  630. }
  631. if (gridTemplateFound || !hasArea) {
  632. return true
  633. }
  634. if (!foundAreaSelector) {
  635. foundAreaSelector = parent.selector
  636. }
  637. // if we found the duplicate area with different selector
  638. if (foundAreaSelector && foundAreaSelector !== parent.selector) {
  639. duplicatesFound = true
  640. }
  641. return undefined
  642. })
  643. // warn user if we didn't find template
  644. if (!gridTemplateFound && duplicatesFound) {
  645. decl.warn(
  646. result,
  647. 'Autoprefixer cannot find a grid-template ' +
  648. `containing the duplicate grid-area "${decl.value}" ` +
  649. `with full selector matching: ${slicedSelectorArr.join(' ')}`
  650. )
  651. }
  652. }
  653. }
  654. /**
  655. * warn user if both grid-area and grid-(row|column)
  656. * declarations are present in the same rule
  657. * @param {Declaration} decl
  658. * @param {Result} result
  659. * @return {void}
  660. */
  661. exports.warnIfGridRowColumnExists = warnIfGridRowColumnExists
  662. function warnIfGridRowColumnExists(decl, result) {
  663. let rule = decl.parent
  664. let decls = []
  665. rule.walkDecls(/^grid-(row|column)/, d => {
  666. if (
  667. !d.prop.endsWith('-end') &&
  668. !d.value.startsWith('span') &&
  669. !d.prop.endsWith('-gap')
  670. ) {
  671. decls.push(d)
  672. }
  673. })
  674. if (decls.length > 0) {
  675. decls.forEach(d => {
  676. d.warn(
  677. result,
  678. 'You already have a grid-area declaration present in the rule. ' +
  679. `You should use either grid-area or ${d.prop}, not both`
  680. )
  681. })
  682. }
  683. return undefined
  684. }
  685. // Gap utils
  686. exports.getGridGap = getGridGap
  687. function getGridGap(decl) {
  688. let gap = {}
  689. // try to find gap
  690. let testGap = /^(grid-)?((row|column)-)?gap$/
  691. decl.parent.walkDecls(testGap, ({ prop, value }) => {
  692. if (/^(grid-)?gap$/.test(prop)) {
  693. let [row, , column] = parser(value).nodes
  694. gap.row = row && parser.stringify(row)
  695. gap.column = column ? parser.stringify(column) : gap.row
  696. }
  697. if (/^(grid-)?row-gap$/.test(prop)) gap.row = value
  698. if (/^(grid-)?column-gap$/.test(prop)) gap.column = value
  699. })
  700. return gap
  701. }
  702. /**
  703. * parse media parameters (for example 'min-width: 500px')
  704. * @param {String} params parameter to parse
  705. * @return {}
  706. */
  707. function parseMediaParams(params) {
  708. if (!params) {
  709. return []
  710. }
  711. let parsed = parser(params)
  712. let prop
  713. let value
  714. parsed.walk(node => {
  715. if (node.type === 'word' && /min|max/g.test(node.value)) {
  716. prop = node.value
  717. } else if (node.value.includes('px')) {
  718. value = parseInt(node.value.replace(/\D/g, ''))
  719. }
  720. })
  721. return [prop, value]
  722. }
  723. /**
  724. * Compare the selectors and decide if we
  725. * need to inherit gap from compared selector or not.
  726. * @type {String} selA
  727. * @type {String} selB
  728. * @return {Boolean}
  729. */
  730. function shouldInheritGap(selA, selB) {
  731. let result
  732. // get arrays of selector split in 3-deep array
  733. let splitSelectorArrA = splitSelector(selA)
  734. let splitSelectorArrB = splitSelector(selB)
  735. if (splitSelectorArrA[0].length < splitSelectorArrB[0].length) {
  736. // abort if selectorA has lower descendant specificity then selectorB
  737. // (e.g '.grid' and '.hello .world .grid')
  738. return false
  739. } else if (splitSelectorArrA[0].length > splitSelectorArrB[0].length) {
  740. // if selectorA has higher descendant specificity then selectorB
  741. // (e.g '.foo .bar .grid' and '.grid')
  742. let idx = splitSelectorArrA[0].reduce((res, [item], index) => {
  743. let firstSelectorPart = splitSelectorArrB[0][0][0]
  744. if (item === firstSelectorPart) {
  745. return index
  746. }
  747. return false
  748. }, false)
  749. if (idx) {
  750. result = splitSelectorArrB[0].every((arr, index) => {
  751. return arr.every(
  752. (part, innerIndex) =>
  753. // because selectorA has more space elements, we need to slice
  754. // selectorA array by 'idx' number to compare them
  755. splitSelectorArrA[0].slice(idx)[index][innerIndex] === part
  756. )
  757. })
  758. }
  759. } else {
  760. // if selectorA has the same descendant specificity as selectorB
  761. // this condition covers cases such as: '.grid.foo.bar' and '.grid'
  762. result = splitSelectorArrB.some(byCommaArr => {
  763. return byCommaArr.every((bySpaceArr, index) => {
  764. return bySpaceArr.every(
  765. (part, innerIndex) => splitSelectorArrA[0][index][innerIndex] === part
  766. )
  767. })
  768. })
  769. }
  770. return result
  771. }
  772. /**
  773. * inherit grid gap values from the closest rule above
  774. * with the same selector
  775. * @param {Declaration} decl
  776. * @param {Object} gap gap values
  777. * @return {Object | Boolean} return gap values or false (if not found)
  778. */
  779. exports.inheritGridGap = inheritGridGap
  780. function inheritGridGap(decl, gap) {
  781. let rule = decl.parent
  782. let mediaRule = getParentMedia(rule)
  783. let root = rule.root()
  784. // get an array of selector split in 3-deep array
  785. let splitSelectorArr = splitSelector(rule.selector)
  786. // abort if the rule already has gaps
  787. if (Object.keys(gap).length > 0) {
  788. return false
  789. }
  790. // e.g ['min-width']
  791. let [prop] = parseMediaParams(mediaRule.params)
  792. let lastBySpace = splitSelectorArr[0]
  793. // get escaped value from the selector
  794. // if we have '.grid-2.foo.bar' selector, will be '\.grid\-2'
  795. let escaped = escapeRegexp(lastBySpace[lastBySpace.length - 1][0])
  796. let regexp = new RegExp(`(${escaped}$)|(${escaped}[,.])`)
  797. // find the closest rule with the same selector
  798. let closestRuleGap
  799. root.walkRules(regexp, r => {
  800. let gridGap
  801. // abort if are checking the same rule
  802. if (rule.toString() === r.toString()) {
  803. return false
  804. }
  805. // find grid-gap values
  806. r.walkDecls('grid-gap', d => (gridGap = getGridGap(d)))
  807. // skip rule without gaps
  808. if (!gridGap || Object.keys(gridGap).length === 0) {
  809. return true
  810. }
  811. // skip rules that should not be inherited from
  812. if (!shouldInheritGap(rule.selector, r.selector)) {
  813. return true
  814. }
  815. let media = getParentMedia(r)
  816. if (media) {
  817. // if we are inside media, we need to check that media props match
  818. // e.g ('min-width' === 'min-width')
  819. let propToCompare = parseMediaParams(media.params)[0]
  820. if (propToCompare === prop) {
  821. closestRuleGap = gridGap
  822. return true
  823. }
  824. } else {
  825. closestRuleGap = gridGap
  826. return true
  827. }
  828. return undefined
  829. })
  830. // if we find the closest gap object
  831. if (closestRuleGap && Object.keys(closestRuleGap).length > 0) {
  832. return closestRuleGap
  833. }
  834. return false
  835. }
  836. exports.warnGridGap = warnGridGap
  837. function warnGridGap({ gap, hasColumns, decl, result }) {
  838. let hasBothGaps = gap.row && gap.column
  839. if (!hasColumns && (hasBothGaps || (gap.column && !gap.row))) {
  840. delete gap.column
  841. decl.warn(
  842. result,
  843. 'Can not implement grid-gap without grid-template-columns'
  844. )
  845. }
  846. }
  847. /**
  848. * normalize the grid-template-rows/columns values
  849. * @param {String} str grid-template-rows/columns value
  850. * @return {Array} normalized array with values
  851. * @example
  852. * let normalized = normalizeRowColumn('1fr repeat(2, 20px 50px) 1fr')
  853. * normalized // <= ['1fr', '20px', '50px', '20px', '50px', '1fr']
  854. */
  855. function normalizeRowColumn(str) {
  856. let normalized = parser(str).nodes.reduce((result, node) => {
  857. if (node.type === 'function' && node.value === 'repeat') {
  858. let key = 'count'
  859. let [count, value] = node.nodes.reduce(
  860. (acc, n) => {
  861. if (n.type === 'word' && key === 'count') {
  862. acc[0] = Math.abs(parseInt(n.value))
  863. return acc
  864. }
  865. if (n.type === 'div' && n.value === ',') {
  866. key = 'value'
  867. return acc
  868. }
  869. if (key === 'value') {
  870. acc[1] += parser.stringify(n)
  871. }
  872. return acc
  873. },
  874. [0, '']
  875. )
  876. if (count) {
  877. for (let i = 0; i < count; i++) {
  878. result.push(value)
  879. }
  880. }
  881. return result
  882. }
  883. if (node.type === 'space') {
  884. return result
  885. }
  886. return result.concat(parser.stringify(node))
  887. }, [])
  888. return normalized
  889. }
  890. exports.autoplaceGridItems = autoplaceGridItems
  891. /**
  892. * Autoplace grid items
  893. * @param {Declaration} decl
  894. * @param {Result} result
  895. * @param {Object} gap gap values
  896. * @param {String} autoflowValue grid-auto-flow value
  897. * @return {void}
  898. * @see https://github.com/postcss/autoprefixer/issues/1148
  899. */
  900. function autoplaceGridItems(decl, result, gap, autoflowValue = 'row') {
  901. let { parent } = decl
  902. let rowDecl = parent.nodes.find(i => i.prop === 'grid-template-rows')
  903. let rows = normalizeRowColumn(rowDecl.value)
  904. let columns = normalizeRowColumn(decl.value)
  905. // Build array of area names with dummy values. If we have 3 columns and
  906. // 2 rows, filledRows will be equal to ['1 2 3', '4 5 6']
  907. let filledRows = rows.map((_, rowIndex) => {
  908. return Array.from(
  909. { length: columns.length },
  910. (v, k) => k + rowIndex * columns.length + 1
  911. ).join(' ')
  912. })
  913. let areas = parseGridAreas({ rows: filledRows, gap })
  914. let keys = Object.keys(areas)
  915. let items = keys.map(i => areas[i])
  916. // Change the order of cells if grid-auto-flow value is 'column'
  917. if (autoflowValue.includes('column')) {
  918. items = items.sort((a, b) => a.column.start - b.column.start)
  919. }
  920. // Insert new rules
  921. items.reverse().forEach((item, index) => {
  922. let { column, row } = item
  923. let nodeSelector = parent.selectors
  924. .map(sel => sel + ` > *:nth-child(${keys.length - index})`)
  925. .join(', ')
  926. // create new rule
  927. let node = parent.clone().removeAll()
  928. // change rule selector
  929. node.selector = nodeSelector
  930. // insert prefixed row/column values
  931. node.append({ prop: '-ms-grid-row', value: row.start })
  932. node.append({ prop: '-ms-grid-column', value: column.start })
  933. // insert rule
  934. parent.after(node)
  935. })
  936. return undefined
  937. }