grid-utils.js 29 KB

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