mirror of https://github.com/artf/grapesjs.git
9 changed files with 554 additions and 227 deletions
@ -1,3 +1,9 @@ |
|||
module.exports = { |
|||
textTags: ['br', 'b', 'i', 'u', 'a', 'ul', 'ol'] |
|||
textTags: ['br', 'b', 'i', 'u', 'a', 'ul', 'ol'], |
|||
|
|||
// Custom CSS parser
|
|||
parserCss: null, |
|||
|
|||
// Custom HTML parser
|
|||
parserHtml: null |
|||
}; |
|||
|
|||
@ -0,0 +1,221 @@ |
|||
import { keys } from 'underscore'; |
|||
|
|||
// At-rules
|
|||
// https://developer.mozilla.org/it/docs/Web/API/CSSRule#Type_constants
|
|||
const atRules = { |
|||
4: 'media', |
|||
5: 'font-face', |
|||
6: 'page', |
|||
7: 'keyframes', |
|||
11: 'counter-style', |
|||
12: 'supports', |
|||
13: 'document', |
|||
14: 'font-feature-values', |
|||
15: 'viewport' |
|||
}; |
|||
const atRuleKeys = keys(atRules); |
|||
const singleAtRules = ['5', '6', '11', '15']; |
|||
const singleAtRulesNames = ['font-face', 'page', 'counter-style', 'viewport']; |
|||
|
|||
/** |
|||
* Parse selector string to array. |
|||
* Only classe based are valid as CSS rules inside editor, not valid |
|||
* selectors will be dropped as additional |
|||
* It's ok with the last part of the string as state (:hover, :active) |
|||
* @param {string} str Selectors string |
|||
* @return {Object} |
|||
* @example |
|||
* var res = parseSelector('.test1, .test1.test2, .test2 .test3'); |
|||
* console.log(res); |
|||
* // {
|
|||
* //result: [['test1'], ['test1', 'test2']],
|
|||
* //add: ['.test2 .test3']
|
|||
* //}
|
|||
*/ |
|||
export const parseSelector = (str = '') => { |
|||
const add = []; |
|||
const result = []; |
|||
const sels = str.split(','); |
|||
|
|||
for (var i = 0, len = sels.length; i < len; i++) { |
|||
var sel = sels[i].trim(); |
|||
|
|||
// Will accept only concatenated classes and last
|
|||
// class might be with state (eg. :hover), nothing else.
|
|||
// Can also accept SINGLE ID selectors, eg. `#myid`, `#myid:hover`
|
|||
// Composed are not valid: `#myid.some-class`, `#myid.some-class:hover`
|
|||
if ( |
|||
/^(\.{1}[\w\-]+)+(:{1,2}[\w\-()]+)?$/gi.test(sel) || |
|||
/^(#{1}[\w\-]+){1}(:{1,2}[\w\-()]+)?$/gi.test(sel) |
|||
) { |
|||
var cls = sel.split('.').filter(Boolean); |
|||
result.push(cls); |
|||
} else { |
|||
add.push(sel); |
|||
} |
|||
} |
|||
|
|||
return { |
|||
result, |
|||
add |
|||
}; |
|||
}; |
|||
|
|||
/** |
|||
* Parse style declarations of the node |
|||
* @param {CSSRule} node |
|||
* @return {Object} |
|||
*/ |
|||
export const parseStyle = node => { |
|||
const stl = node.style; |
|||
const style = {}; |
|||
|
|||
for (var i = 0, len = stl.length; i < len; i++) { |
|||
const propName = stl[i]; |
|||
const propValue = stl.getPropertyValue(propName); |
|||
const important = stl.getPropertyPriority(propName); |
|||
style[propName] = `${propValue}${important ? ` !${important}` : ''}`; |
|||
} |
|||
|
|||
return style; |
|||
}; |
|||
|
|||
/** |
|||
* Get the condition when possible |
|||
* @param {CSSRule} node |
|||
* @return {string} |
|||
*/ |
|||
export const parseCondition = node => { |
|||
const condition = |
|||
node.conditionText || |
|||
(node.media && node.media.mediaText) || |
|||
node.name || |
|||
node.selectorText || |
|||
''; |
|||
return condition.trim(); |
|||
}; |
|||
|
|||
/** |
|||
* Create node for the editor |
|||
* @param {Array<String>} selectors Array containing strings of classes |
|||
* @param {Object} style Key-value object of style declarations |
|||
* @return {Object} |
|||
*/ |
|||
export const createNode = (selectors, style, opts = {}) => { |
|||
const node = {}; |
|||
const selLen = selectors.length; |
|||
const lastClass = selectors[selLen - 1]; |
|||
const stateArr = lastClass ? lastClass.split(/:(.+)/) : []; |
|||
const state = stateArr[1]; |
|||
const { atRule, selectorsAdd, mediaText } = opts; |
|||
const singleAtRule = singleAtRulesNames.indexOf(atRule) >= 0; |
|||
singleAtRule && (node.singleAtRule = 1); |
|||
atRule && (node.atRuleType = atRule); |
|||
selectorsAdd && (node.selectorsAdd = selectorsAdd); |
|||
mediaText && (node.mediaText = mediaText); |
|||
|
|||
// Isolate the state from selectors
|
|||
if (state) { |
|||
selectors[selLen - 1] = stateArr[0]; |
|||
node.state = state; |
|||
stateArr.splice(stateArr.length - 1, 1); |
|||
} |
|||
|
|||
node.selectors = selectors; |
|||
node.style = style; |
|||
|
|||
return node; |
|||
}; |
|||
|
|||
/** |
|||
* Fetch data from node |
|||
* @param {StyleSheet|CSSRule} el |
|||
* @return {Array<Object>} |
|||
*/ |
|||
export const parseNode = el => { |
|||
var result = []; |
|||
var nodes = el.cssRules || []; |
|||
|
|||
for (var i = 0, len = nodes.length; i < len; i++) { |
|||
const node = nodes[i]; |
|||
const type = node.type.toString(); |
|||
let singleAtRule = 0; |
|||
let atRuleType = ''; |
|||
let condition = ''; |
|||
// keyText is for CSSKeyframeRule
|
|||
let sels = node.selectorText || node.keyText; |
|||
const isSingleAtRule = singleAtRules.indexOf(type) >= 0; |
|||
|
|||
// Check if the node is an at-rule
|
|||
if (isSingleAtRule) { |
|||
singleAtRule = 1; |
|||
atRuleType = atRules[type]; |
|||
condition = parseCondition(node); |
|||
} else if (atRuleKeys.indexOf(type) >= 0) { |
|||
var subRules = parseNode(node); |
|||
condition = parseCondition(node); |
|||
|
|||
for (var s = 0, lens = subRules.length; s < lens; s++) { |
|||
var subRule = subRules[s]; |
|||
condition && (subRule.mediaText = condition); |
|||
subRule.atRuleType = atRules[type]; |
|||
} |
|||
result = result.concat(subRules); |
|||
} |
|||
|
|||
if (!sels && !isSingleAtRule) continue; |
|||
const style = parseStyle(node); |
|||
const selsParsed = parseSelector(sels); |
|||
const selsAdd = selsParsed.add; |
|||
sels = selsParsed.result; |
|||
|
|||
let lastRule; |
|||
// For each group of selectors
|
|||
for (var k = 0, len3 = sels.length; k < len3; k++) { |
|||
const model = createNode(sels[k], style, { |
|||
atRule: atRules[type] |
|||
}); |
|||
result.push(model); |
|||
lastRule = model; |
|||
} |
|||
|
|||
// Need to push somewhere not class-based selectors, if some rule was
|
|||
// created will push them there, otherwise will create a new rule
|
|||
if (selsAdd.length) { |
|||
var selsAddStr = selsAdd.join(', '); |
|||
if (lastRule) { |
|||
lastRule.selectorsAdd = selsAddStr; |
|||
} else { |
|||
const model = { |
|||
selectors: [], |
|||
selectorsAdd: selsAddStr, |
|||
style |
|||
}; |
|||
singleAtRule && (model.singleAtRule = singleAtRule); |
|||
atRuleType && (model.atRuleType = atRuleType); |
|||
condition && (model.mediaText = condition); |
|||
result.push(model); |
|||
} |
|||
} |
|||
// console.log('LAST PUSH', result[result.length - 1]);
|
|||
} |
|||
|
|||
return result; |
|||
}; |
|||
|
|||
/** |
|||
* Parse CSS string and return the array of objects |
|||
* @param {String} str CSS string |
|||
* @return {Array<Object>} Array of objects for the definition of CSSRules |
|||
*/ |
|||
export default str => { |
|||
const el = document.createElement('style'); |
|||
el.innerHTML = str; |
|||
|
|||
// There is no .sheet before adding it to the <head>
|
|||
document.head.appendChild(el); |
|||
const sheet = el.sheet; |
|||
document.head.removeChild(el); |
|||
|
|||
return parseNode(sheet); |
|||
}; |
|||
@ -1,203 +1,59 @@ |
|||
import { keys } from 'underscore'; |
|||
|
|||
// At-rules
|
|||
// https://developer.mozilla.org/it/docs/Web/API/CSSRule#Type_constants
|
|||
const atRules = { |
|||
4: 'media', |
|||
5: 'font-face', |
|||
6: 'page', |
|||
7: 'keyframes', |
|||
11: 'counter-style', |
|||
12: 'supports', |
|||
13: 'document', |
|||
14: 'font-feature-values', |
|||
15: 'viewport' |
|||
}; |
|||
const atRuleKeys = keys(atRules); |
|||
const singleAtRules = ['5', '6', '11', '15']; |
|||
|
|||
module.exports = config => ({ |
|||
/** |
|||
* Parse selector string to array. |
|||
* Only classe based are valid as CSS rules inside editor, not valid |
|||
* selectors will be dropped as additional |
|||
* It's ok with the last part of the string as state (:hover, :active) |
|||
* @param {string} str Selectors string |
|||
* @return {Object} |
|||
* @example |
|||
* var res = ParserCss.parseSelector('.test1, .test1.test2, .test2 .test3'); |
|||
* console.log(res); |
|||
* // {
|
|||
* //result: [['test1'], ['test1', 'test2']],
|
|||
* //add: ['.test2 .test3']
|
|||
* //}
|
|||
*/ |
|||
parseSelector(str = '') { |
|||
const add = []; |
|||
const result = []; |
|||
const sels = str.split(','); |
|||
|
|||
for (var i = 0, len = sels.length; i < len; i++) { |
|||
var sel = sels[i].trim(); |
|||
|
|||
// Will accept only concatenated classes and last
|
|||
// class might be with state (eg. :hover), nothing else.
|
|||
// Can also accept SINGLE ID selectors, eg. `#myid`, `#myid:hover`
|
|||
// Composed are not valid: `#myid.some-class`, `#myid.some-class:hover`
|
|||
if ( |
|||
/^(\.{1}[\w\-]+)+(:{1,2}[\w\-()]+)?$/gi.test(sel) || |
|||
/^(#{1}[\w\-]+){1}(:{1,2}[\w\-()]+)?$/gi.test(sel) |
|||
) { |
|||
var cls = sel.split('.').filter(Boolean); |
|||
result.push(cls); |
|||
} else { |
|||
add.push(sel); |
|||
} |
|||
} |
|||
|
|||
return { |
|||
result, |
|||
add |
|||
}; |
|||
}, |
|||
import { isString } from 'underscore'; |
|||
import BrowserCssParser, { |
|||
parseSelector, |
|||
createNode |
|||
} from './BrowserParserCss'; |
|||
|
|||
module.exports = (config = {}) => ({ |
|||
/** |
|||
* Parse style declarations of the node |
|||
* @param {CSSRule} node |
|||
* @return {Object} |
|||
* Parse CSS string to a desired model object |
|||
* @param {String} str CSS string |
|||
* @return {Array<Object>} |
|||
*/ |
|||
parseStyle(node) { |
|||
const stl = node.style; |
|||
const style = {}; |
|||
|
|||
for (var i = 0, len = stl.length; i < len; i++) { |
|||
const propName = stl[i]; |
|||
const propValue = stl.getPropertyValue(propName); |
|||
const important = stl.getPropertyPriority(propName); |
|||
style[propName] = `${propValue}${important ? ` !${important}` : ''}`; |
|||
} |
|||
|
|||
return style; |
|||
}, |
|||
parse(str) { |
|||
let result = []; |
|||
const { parserCss, em = {} } = config; |
|||
const editor = em && em.get && em.get('Editor'); |
|||
const nodes = parserCss ? parserCss(str, editor) : BrowserCssParser(str); |
|||
nodes.forEach(node => (result = result.concat(this.checkNode(node)))); |
|||
|
|||
/** |
|||
* Get the condition when possible |
|||
* @param {CSSRule} node |
|||
* @return {string} |
|||
*/ |
|||
parseCondition(node) { |
|||
const condition = |
|||
node.conditionText || |
|||
(node.media && node.media.mediaText) || |
|||
node.name || |
|||
node.selectorText || |
|||
''; |
|||
return condition.trim(); |
|||
return result; |
|||
}, |
|||
|
|||
/** |
|||
* Fetch data from node |
|||
* @param {StyleSheet|CSSRule} el |
|||
* @return {Array<Object>} |
|||
* Check the returned node from a custom parser and transforms it to |
|||
* a valid object for the CSS composer |
|||
* @return {[type]} |
|||
*/ |
|||
parseNode(el) { |
|||
var result = []; |
|||
var nodes = el.cssRules || []; |
|||
|
|||
for (var i = 0, len = nodes.length; i < len; i++) { |
|||
const node = nodes[i]; |
|||
const type = node.type.toString(); |
|||
let singleAtRule = 0; |
|||
let atRuleType = ''; |
|||
let condition = ''; |
|||
// keyText is for CSSKeyframeRule
|
|||
let sels = node.selectorText || node.keyText; |
|||
const isSingleAtRule = singleAtRules.indexOf(type) >= 0; |
|||
|
|||
// Check if the node is an at-rule
|
|||
if (isSingleAtRule) { |
|||
singleAtRule = 1; |
|||
atRuleType = atRules[type]; |
|||
condition = this.parseCondition(node); |
|||
} else if (atRuleKeys.indexOf(type) >= 0) { |
|||
var subRules = this.parseNode(node); |
|||
condition = this.parseCondition(node); |
|||
|
|||
for (var s = 0, lens = subRules.length; s < lens; s++) { |
|||
var subRule = subRules[s]; |
|||
condition && (subRule.mediaText = condition); |
|||
subRule.atRuleType = atRules[type]; |
|||
} |
|||
result = result.concat(subRules); |
|||
checkNode(node) { |
|||
const { selectors, style } = node; |
|||
|
|||
if (isString(selectors)) { |
|||
const nodes = []; |
|||
const selsParsed = parseSelector(selectors); |
|||
const classSets = selsParsed.result; |
|||
const selectorsAdd = selsParsed.add.join(', '); |
|||
const opts = { |
|||
atRule: node.atRule, |
|||
mediaText: node.params |
|||
}; |
|||
|
|||
if (classSets.length) { |
|||
classSets.forEach(classSet => { |
|||
nodes.push(createNode(classSet, style, opts)); |
|||
}); |
|||
} else { |
|||
nodes.push(createNode([], style, opts)); |
|||
} |
|||
|
|||
if (!sels && !isSingleAtRule) continue; |
|||
const style = this.parseStyle(node); |
|||
const selsParsed = this.parseSelector(sels); |
|||
const selsAdd = selsParsed.add; |
|||
sels = selsParsed.result; |
|||
|
|||
let lastRule; |
|||
// For each group of selectors
|
|||
for (var k = 0, len3 = sels.length; k < len3; k++) { |
|||
var selArr = sels[k]; |
|||
var model = {}; |
|||
singleAtRule && (model.singleAtRule = singleAtRule); |
|||
atRuleType && (model.atRuleType = atRuleType); |
|||
|
|||
//Isolate state from selector
|
|||
var stateArr = selArr[selArr.length - 1].split(/:(.+)/); |
|||
if (stateArr[1]) { |
|||
selArr[selArr.length - 1] = stateArr[0]; |
|||
model.state = stateArr[1]; |
|||
stateArr.splice(stateArr.length - 1, 1); |
|||
} |
|||
|
|||
model.selectors = selArr; |
|||
model.style = style; |
|||
lastRule = model; |
|||
result.push(model); |
|||
if (selectorsAdd) { |
|||
const lastNode = nodes[nodes.length - 1]; |
|||
lastNode.selectorsAdd = selectorsAdd; |
|||
} |
|||
|
|||
// Need to push somewhere not class-based selectors, if some rule was
|
|||
// created will push them there, otherwise will create a new rule
|
|||
if (selsAdd.length) { |
|||
var selsAddStr = selsAdd.join(', '); |
|||
if (lastRule) { |
|||
lastRule.selectorsAdd = selsAddStr; |
|||
} else { |
|||
const model = { |
|||
selectors: [], |
|||
selectorsAdd: selsAddStr, |
|||
style |
|||
}; |
|||
singleAtRule && (model.singleAtRule = singleAtRule); |
|||
atRuleType && (model.atRuleType = atRuleType); |
|||
condition && (model.mediaText = condition); |
|||
result.push(model); |
|||
} |
|||
} |
|||
// console.log('LAST PUSH', result[result.length - 1]);
|
|||
node = nodes; |
|||
} |
|||
|
|||
return result; |
|||
}, |
|||
|
|||
/** |
|||
* Parse CSS string to a desired model object |
|||
* @param {string} str HTML string |
|||
* @return {Object|Array<Object>} |
|||
*/ |
|||
parse(str) { |
|||
const el = document.createElement('style'); |
|||
el.innerHTML = str; |
|||
|
|||
// There is no .sheet before adding it to the <head>
|
|||
document.head.appendChild(el); |
|||
const sheet = el.sheet; |
|||
document.head.removeChild(el); |
|||
const result = this.parseNode(sheet); |
|||
|
|||
return result.length == 1 ? result[0] : result; |
|||
return node; |
|||
} |
|||
}); |
|||
|
|||
Loading…
Reference in new issue