const _ = require('lodash'); const oneLineTrim = require('common-tags/lib/oneLineTrim'); const previousSpec = require('./v3'); const ghostVersions = require('../utils').versions; const docsBaseUrl = `https://ghost.org/docs/themes/`; // TODO: we don't use versioned docs anymore and the previous rules should only contain // correct links. The usage of replacing the previousBaseUrl can probably be removed const prevDocsBaseUrl = `https://themes.ghost.org/v${ghostVersions.v3.docs}/docs/`; const prevDocsBaseUrlRegEx = new RegExp(prevDocsBaseUrl, 'g'); const previousKnownHelpers = previousSpec.knownHelpers; const previousTemplates = previousSpec.templates; const previousRules = _.cloneDeep(previousSpec.rules); function cssCardRule(cardName, className) { return { level: 'warning', rule: `The .${className} CSS class is required to appear styled in your theme`, details: oneLineTrim`The .${className} CSS class is required otherwise the ${cardName} card will appear unstyled. Find out more about required theme changes for the Koenig editor here.`, regex: new RegExp(`\\.${className}`, 'g'), className: `.${className}`, css: true, cardAsset: cardName }; } // assign new or overwrite existing knownHelpers, templates, or rules here: let knownHelpers = ['match', 'tiers']; let templates = []; let rules = { // New rules 'GS010-PJ-GHOST-API-PRESENT': { level: 'warning', rule: 'package.json property "engines.ghost-api" is deprecated.', details: oneLineTrim`Remove "ghost-api" from your package.json.
The ghost-api support will be removed in next major version of Ghost and should not be used. Check the package.json documentation for further information.` }, 'GS010-PJ-GHOST-API-V01': { level: 'error', rule: 'package.json property "engines.ghost-api" is incompatible with current version of Ghost API and will fall back to "v4"', details: oneLineTrim`Change "ghost-api" in your package.json to higher version. E.g. {"engines": {"ghost-api": "v4"}}.
If "ghost-api" property is left at "v0.1", Ghost will use its default setting of "v4".
Check the package.json documentation for further information.` }, 'GS010-PJ-GHOST-API-V2': { level: 'warning', rule: 'package.json property "engines.ghost-api" is using a deprecated version of Ghost API', details: oneLineTrim`Change "ghost-api" in your package.json to higher version. E.g. {"engines": {"ghost-api": "v4"}}.
If "ghost-api" property is left at "v2", it will stop working with next major version upgrade and default to v5.
Check the package.json documentation for further information.` }, 'GS010-PJ-CUST-THEME-TOTAL-SETTINGS': { level: 'error', rule: 'package.json property "config.custom" contains too many settings', details: oneLineTrim`Remove key from "config.custom" in your package.json to have less than or exactly 20 settings.
Check the package.json documentation for further information.` }, 'GS010-PJ-CUST-THEME-SETTINGS-CASE': { level: 'error', rule: 'package.json property "config.custom" contains a property that isn\'t snake-cased', details: oneLineTrim`Rewrite all property in "config.custom" in your package.json in snake case.
Check the package.json documentation for further information.` }, 'GS010-PJ-CUST-THEME-SETTINGS-TYPE': { level: 'error', rule: 'package.json objects defined in "config.custom" should have a known "type".', details: oneLineTrim`Only use the following types: "select", "boolean", "color", "image", "text".
Check the package.json documentation for further information.` }, 'GS010-PJ-CUST-THEME-SETTINGS-GROUP': { level: 'recommendation', rule: 'package.json objects defined in "config.custom" should have a known "group".', details: oneLineTrim`Only use the following groups: "post", "homepage".
Check the package.json documentation for further information.` }, 'GS010-PJ-CUST-THEME-SETTINGS-SELECT-OPTIONS': { level: 'error', rule: 'package.json objects defined in "config.custom" of type "select" need to have at least 2 "options".', details: oneLineTrim`Make sure there is at least 2 "options" in each "select" custom theme property.
Check the package.json documentation for further information.` }, 'GS010-PJ-CUST-THEME-SETTINGS-SELECT-DEFAULT': { level: 'error', rule: 'package.json objects defined in "config.custom" of type "select" need to have a valid "default".', details: oneLineTrim`Make sure the "default" property matches a value in "options" of the same "select".
Check the package.json documentation for further information.` }, 'GS010-PJ-CUST-THEME-SETTINGS-BOOLEAN-DEFAULT': { level: 'error', rule: 'package.json objects defined in "config.custom" of type "boolean" need to have a valid "default".', details: oneLineTrim`Make sure the "default" property is either true or false.
Check the package.json documentation for further information.` }, 'GS010-PJ-CUST-THEME-SETTINGS-COLOR-DEFAULT': { level: 'error', rule: 'package.json objects defined in "config.custom" of type "color" need to have a valid "default".', details: oneLineTrim`Make sure the "default" property is a valid 6-hexadecimal-digit color code like #15171a.
Check the package.json documentation for further information.` }, 'GS010-PJ-CUST-THEME-SETTINGS-IMAGE-DEFAULT': { level: 'error', rule: 'package.json objects defined in "config.custom" of type "image" can\'t have a "default" value.', details: oneLineTrim`Make sure the "default" property is either null, an empty string '' or isn't present.
Check the package.json documentation for further information.` }, 'GS001-DEPR-LABS-MEMBERS': { level: 'warning', rule: 'The {{@labs.members}} helper should not be used.', details: oneLineTrim`Remove {{@labs.members}} from the theme.
The {{@labs.members}} helper will always return true in Ghost v4 and will be removed from Ghost v5, at which point it will return null and evaluate to false. Find more information about the @labs property here.`, regex: /@labs\.members/g, helper: '{{@labs.members}}' }, 'GS080-FEACH-POSTS': { level: 'warning', rule: 'The default visibility for posts in {{#foreach}} block helper changed in Ghost v4.', details: oneLineTrim`The default visibility for posts in {{#foreach}} block helper changed from public to all.
Find more information about the {{foreach}} helper here.`, regex: /{{\s*?#foreach\s*?\w*?\s*?}}/g, helper: '{{#foreach}}', validInAPI: ['v3'] }, 'GS080-CARD-LAST4': { level: 'warning', rule: 'The default_payment_card_last4 field now coalesces to **** in Ghost 4.x instead of null.', details: oneLineTrim`The default_payment_card_last4 field no longer outputs a falsy(null) value in case of missing card details starting from Ghost 4.x and instead coalesces to **** Find more information about the default_payment_card_last4 attribute here.`, regex: /default_payment_card_last4/g, helper: '{{default_payment_card_last4}}', validInAPI: ['v3'] }, 'GS080-FEACH-PV': { level: 'recommendation', rule: 'The use of visibility="all" is no longer required for posts in {{#foreach}} helper.', details: oneLineTrim`The default visibility in {{#foreach}} helper for posts changed in v4 from public to all and is no longer required when looping over posts.
Check out the documentation for {{#foreach}} here.`, regex: /{{\s*?#foreach\b[\w\s='"]*?visibility=("|')all("|')[\w\s='"]*?}}/g, helper: '{{#foreach}}', validInAPI: ['v3'] }, 'GS001-DEPR-CURR-SYM': { level: 'warning', rule: 'Replace {{[#].currency_symbol}} with {{price currency=currency}}.', details: oneLineTrim`The hardcoded currency_symbol attribute was removed in favour of passing the currency to updated {{price}} helper. Find more information about the updated {{price}} helper here.`, helper: '{{[#].currency_symbol}}', regex: /currency_symbol/g }, 'GS001-DEPR-SITE-LANG': { level: 'warning', rule: 'Replace {{@site.lang}} with {{@site.locale}}', details: oneLineTrim`Replace {{@site.lang}} helper with {{@site.locale}}.
The {{@site.lang}} helper will be removed in next version of Ghost and should not be used. Find more information about the @site property here.`, regex: /@site\.lang/g, helper: '{{@site.lang}}' }, 'GS070-VALID-TRANSLATIONS': { level: 'error', rule: 'Theme translations must be parsable', fatal: true, // overwritten from v3 to be fatal details: oneLineTrim`Theme translations (located in locales/*.json) need to be readable by the node JSON parser, or they will not be applied.` }, 'GS090-NO-IMG-URL-IN-CONDITIONALS': { level: 'warning', rule: 'The {{img_url}} helper should not be used as a parameter to {{#if}} or {{#unless}}', fatal: false, details: oneLineTrim`The {{img_url}} helper should not be used as a parameter to {{#if}} or {{#unless}}` }, 'GS090-NO-UNKNOWN-CUSTOM-THEME-SETTINGS': { level: 'error', rule: 'An unknown custom theme setting has been used.', fatal: false, details: oneLineTrim`The custom theme setting should all be defined in the package.json config.custom object.` }, 'GS090-NO-UNKNOWN-CUSTOM-THEME-SELECT-VALUE-IN-MATCH': { level: 'error', rule: 'A custom theme setting of type select has been compared to a value that isn\'t defined.', fatal: false, details: oneLineTrim`Custom theme settings of type select can only be compared to their defined options when used in a match block.` }, 'GS090-NO-PRODUCTS-HELPER': { level: 'warning', rule: 'Replace {{products}} with {{tiers}}', details: oneLineTrim`The {{products}} helper has been deprecated in favor of {{tiers}}
The {{products}} helper will be removed in Ghost v5 and should not be used. Find more information about the {{tiers}} property here.`, helper: '{{products}}' }, 'GS090-NO-PRODUCT-DATA-HELPER': { level: 'warning', rule: 'Replace {{@product}} with {{#get "tiers"}}', details: oneLineTrim`The {{@product}} data helper has been deprecated in favor of {{#get "tiers"}}
The {{@product}} data helper will be removed in Ghost v5 and should not be used. Find more information about the {{#get "tiers"}} property here.`, helper: '{{@product}}' }, 'GS090-NO-PRODUCTS-DATA-HELPER': { level: 'warning', rule: 'Replace {{@products}} with {{#get "tiers"}}', details: oneLineTrim`The {{@products}} data helper has been deprecated in favor of {{#get "tiers"}}
The {{@products}} data helper will be removed in Ghost v5 and should not be used. Find more information about the {{#get "tiers"}} property here.`, helper: '{{@products}}' }, 'GS100-NO-UNUSED-CUSTOM-THEME-SETTING': { level: 'error', rule: 'A custom theme setting defined in package.json hasn\'t been used in any theme file.', details: oneLineTrim`Custom theme settings defined in package.json must be used at least once in the theme templates.` }, 'GS050-CSS-KGCO': cssCardRule('callout', 'kg-callout-card'), 'GS050-CSS-KGCOE': cssCardRule('callout', 'kg-callout-card-emoji'), 'GS050-CSS-KGCOT': cssCardRule('callout', 'kg-callout-card-text'), 'GS050-CSS-KGCOBGGY': cssCardRule('callout', 'kg-callout-card-background-grey'), 'GS050-CSS-KGCOBGW': cssCardRule('callout', 'kg-callout-card-background-white'), 'GS050-CSS-KGCOBGB': cssCardRule('callout', 'kg-callout-card-background-blue'), 'GS050-CSS-KGCOBGGN': cssCardRule('callout', 'kg-callout-card-background-green'), 'GS050-CSS-KGCOBGY': cssCardRule('callout', 'kg-callout-card-background-yellow'), 'GS050-CSS-KGCOBGR': cssCardRule('callout', 'kg-callout-card-background-red'), 'GS050-CSS-KGCOBGPK': cssCardRule('callout', 'kg-callout-card-background-pink'), 'GS050-CSS-KGCOBGPE': cssCardRule('callout', 'kg-callout-card-background-purple'), 'GS050-CSS-KGCOBGA': cssCardRule('callout', 'kg-callout-card-background-accent'), 'GS050-CSS-KG-NFT': cssCardRule('nft', 'kg-nft-card'), 'GS050-CSS-KG-NFTCO': cssCardRule('nft', 'kg-nft-card-container'), 'GS050-CSS-KG-NFTMD': cssCardRule('nft', 'kg-nft-metadata'), 'GS050-CSS-KG-NFTIMG': cssCardRule('nft', 'kg-nft-image'), 'GS050-CSS-KG-NFTHD': cssCardRule('nft', 'kg-nft-header'), 'GS050-CSS-KG-NFTTIT': cssCardRule('nft', 'kg-nft-title'), 'GS050-CSS-KG-NFTLG': cssCardRule('nft', 'kg-nft-logo'), 'GS050-CSS-KG-NFTCTR': cssCardRule('nft', 'kg-nft-creator'), 'GS050-CSS-KG-NFTDSC': cssCardRule('nft', 'kg-nft-description'), 'GS050-CSS-KGTGL': cssCardRule('toggle', 'kg-toggle-card'), 'GS050-CSS-KGTGLH': cssCardRule('toggle', 'kg-toggle-heading'), 'GS050-CSS-KGTGLHT': cssCardRule('toggle', 'kg-toggle-heading-text'), 'GS050-CSS-KGTGLIC': cssCardRule('toggle', 'kg-toggle-card-icon'), 'GS050-CSS-KGTGLC': cssCardRule('toggle', 'kg-toggle-content'), 'GS050-CSS-KGAUD': cssCardRule('audio', 'kg-audio-card'), 'GS050-CSS-KGAUDTHUMB': cssCardRule('audio', 'kg-audio-thumbnail'), 'GS050-CSS-KGAUDTHUMBPL': cssCardRule('audio', 'kg-audio-thumbnail.placeholder'), 'GS050-CSS-KGAUDPLCNT': cssCardRule('audio', 'kg-audio-player-container'), 'GS050-CSS-KGAUDTI': cssCardRule('audio', 'kg-audio-title'), 'GS050-CSS-KGAUDPL': cssCardRule('audio', 'kg-audio-player'), 'GS050-CSS-KGAUDCURRTM': cssCardRule('audio', 'kg-audio-current-time'), 'GS050-CSS-KGAUDTM': cssCardRule('audio', 'kg-audio-time'), 'GS050-CSS-KGAUDDUR': cssCardRule('audio', 'kg-audio-duration'), 'GS050-CSS-KGAUDPLICO': cssCardRule('audio', 'kg-audio-play-icon'), 'GS050-CSS-KGAUDPAUICO': cssCardRule('audio', 'kg-audio-pause-icon'), 'GS050-CSS-KGAUDSKSL': cssCardRule('audio', 'kg-audio-seek-slider'), 'GS050-CSS-KGAUDPLRT': cssCardRule('audio', 'kg-audio-playback-rate'), 'GS050-CSS-KGAUDMTICO': cssCardRule('audio', 'kg-audio-mute-icon'), 'GS050-CSS-KGAUDUNMTICO': cssCardRule('audio', 'kg-audio-unmute-icon'), 'GS050-CSS-KGAUDVOLSL': cssCardRule('audio', 'kg-audio-volume-slider'), 'GS050-CSS-KGVID': cssCardRule('video', 'kg-video-card'), 'GS050-CSS-KGVIDHD': cssCardRule('video', 'kg-video-hide'), 'GS050-CSS-KGVIDCNT': cssCardRule('video', 'kg-video-container'), 'GS050-CSS-KGVIDOVL': cssCardRule('video', 'kg-video-overlay'), 'GS050-CSS-KGVIDLGPLICO': cssCardRule('video', 'kg-video-large-play-icon'), 'GS050-CSS-KGVIDTHUMB': cssCardRule('video', 'kg-video-thumbnail'), 'GS050-CSS-KGVIDTHUMBPL': cssCardRule('video', 'kg-video-thumbnail.placeholder'), 'GS050-CSS-KGVIDPLCNT': cssCardRule('video', 'kg-video-player-container'), 'GS050-CSS-KGVIDTI': cssCardRule('video', 'kg-video-title'), 'GS050-CSS-KGVIDPL': cssCardRule('video', 'kg-video-player'), 'GS050-CSS-KGVIDCURRTM': cssCardRule('video', 'kg-video-current-time'), 'GS050-CSS-KGVIDTM': cssCardRule('video', 'kg-video-time'), 'GS050-CSS-KGVIDDUR': cssCardRule('video', 'kg-video-duration'), 'GS050-CSS-KGVIDPLICO': cssCardRule('video', 'kg-video-play-icon'), 'GS050-CSS-KGVIDPAUICO': cssCardRule('video', 'kg-video-pause-icon'), 'GS050-CSS-KGVIDSKSL': cssCardRule('video', 'kg-video-seek-slider'), 'GS050-CSS-KGVIDPLRT': cssCardRule('video', 'kg-video-playback-rate'), 'GS050-CSS-KGVIDMTICO': cssCardRule('video', 'kg-video-mute-icon'), 'GS050-CSS-KGVIDUNMTICO': cssCardRule('video', 'kg-video-unmute-icon'), 'GS050-CSS-KGVIDVOLSL': cssCardRule('video', 'kg-video-volume-slider'), 'GS050-CSS-KGBTN': cssCardRule('button', 'kg-button-card'), 'GS050-CSS-KGBTNL': cssCardRule('button', 'kg-button-card.kg-align-left'), 'GS050-CSS-KGBTNC': cssCardRule('button', 'kg-button-card.kg-align-center'), 'GS050-CSS-KGBTNBTN': cssCardRule('button', 'kg-btn'), 'GS050-CSS-KGBTNBTNA': cssCardRule('button', 'kg-btn-accent'), 'GS050-CSS-KGPR': cssCardRule('product', 'kg-product-card'), 'GS050-CSS-KGPRBTNA': cssCardRule('product', 'kg-product-card-btn-accent'), 'GS050-CSS-KGPRBTN': cssCardRule('product', 'kg-product-card-button'), 'GS050-CSS-KGPRCO': cssCardRule('product', 'kg-product-card-container'), 'GS050-CSS-KGPRDE': cssCardRule('product', 'kg-product-card-description'), 'GS050-CSS-KGPRIM': cssCardRule('product', 'kg-product-card-image'), 'GS050-CSS-KGPRRA': cssCardRule('product', 'kg-product-card-rating'), 'GS050-CSS-KGPRRAA': cssCardRule('product', 'kg-product-card-rating-active'), 'GS050-CSS-KGPRRAS': cssCardRule('product', 'kg-product-card-rating-star'), 'GS050-CSS-KGPRTI': cssCardRule('product', 'kg-product-card-title'), 'GS050-CSS-KGPRTICO': cssCardRule('product', 'kg-product-card-title-container'), 'GS050-CSS-KGBA': cssCardRule('before-after', 'kg-before-after-card'), 'GS050-CSS-KGBAIA': cssCardRule('before-after', 'kg-before-after-card-image-before'), 'GS050-CSS-KGBAIB': cssCardRule('before-after', 'kg-before-after-card-image-after'), 'GS050-CSS-KGFL': cssCardRule('file', 'kg-file-card'), 'GS050-CSS-KGFLCON': cssCardRule('file', 'kg-file-card-container'), 'GS050-CSS-KGFLCNT': cssCardRule('file', 'kg-file-card-contents'), 'GS050-CSS-KGFLTTL': cssCardRule('file', 'kg-file-card-title'), 'GS050-CSS-KGFLCAP': cssCardRule('file', 'kg-file-card-caption'), 'GS050-CSS-KGFLNM': cssCardRule('file', 'kg-file-card-filename'), 'GS050-CSS-KGFLSZ': cssCardRule('file', 'kg-file-card-filesize'), 'GS050-CSS-KGFLMD': cssCardRule('file', 'kg-file-card-medium'), 'GS050-CSS-KGFLSM': cssCardRule('file', 'kg-file-card-small'), 'GS050-CSS-KGBQALT': cssCardRule('blockquote', 'kg-blockquote-alt') }; knownHelpers = _.union(previousKnownHelpers, knownHelpers); templates = _.union(previousTemplates, templates); // Merge the previous rules into the new rules, but overwrite any specified property, // as well as adding any new rule to the spec. // Furthermore, replace the usage of the old doc URLs that we're linking to, with the // new version. delete previousRules['GS002-DISQUS-ID']; delete previousRules['GS002-ID-HELPER']; rules = _.merge({}, previousRules, rules); rules = _.each(rules, function replaceDocsUrl(value) { value.details = value.details.replace(prevDocsBaseUrlRegEx, docsBaseUrl); }); module.exports = { knownHelpers: knownHelpers, templates: templates, rules: rules, defaultPackageJSON: previousSpec.defaultPackageJSON };