Skip to content

Instantly share code, notes, and snippets.

@onmyway133
Last active October 27, 2021 16:25
Show Gist options
  • Save onmyway133/c486939f82fc4d3a8ed4be21538fdd32 to your computer and use it in GitHub Desktop.
Save onmyway133/c486939f82fc4d3a8ed4be21538fdd32 to your computer and use it in GitHub Desktop.
AutoLayoutConverter - 🐜 Convert Cartography to NSLayoutAnchor
.DS_Store
.tags*
/.idea/
/build/
/dist/
/external_binaries/
/out/
/vendor/download/
/vendor/debian_jessie_amd64-sysroot/
/vendor/debian_jessie_arm-sysroot/
/vendor/debian_jessie_arm64-sysroot/
/vendor/debian_jessie_i386-sysroot/
/vendor/debian_wheezy_amd64-sysroot/
/vendor/debian_wheezy_arm-sysroot/
/vendor/debian_wheezy_i386-sysroot/
/vendor/python_26/
/vendor/npm/
/vendor/llvm/
/vendor/llvm-build/
/vendor/.gclient
node_modules/
*.xcodeproj
*.swp
*.pyc
*.VC.db
*.VC.VC.opendb
.vs/
.vscode/
*.vcxproj
*.vcxproj.user
*.vcxproj.filters
*.sln
*.log
/brightray/brightray.opensdf
/brightray/brightray.sdf
/brightray/brightray.sln
/brightray/brightray.vcxproj*
/brightray/brightray.suo
/brightray/brightray.v12.suo
/brightray/brightray.xcodeproj/
const Glob = require('glob')
const Fs = require('fs')
function main() {
const projectPath = getProjectPath()
const files = getAllFiles(projectPath)
files.forEach((file) => {
handleFile(file)
})
}
/// The 1st argument is node, 2nd is index.js, 3rd is path
function getProjectPath() {
return process.argv[2]
}
/// Only select `swift` file
function getAllFiles(projectPath) {
if (projectPath.endsWith('.swift')) {
return [projectPath]
} else {
const files = Glob.sync(`${projectPath}/**/*.swift`)
return files
}
}
/// Read file, replace by matches, and write again
function handleFile(file) {
console.log(file)
Fs.readFile(file, 'utf8', (error, data) => {
// non greedy `*?`
const pattern = '(.)*constrain\\(.+\\) {\\s(\\s|.)*?\\n(\\s)*}'
const regex = new RegExp(pattern)
let matches = data.match(regex)
// RegEx only return the 1st match per execution, let's do a recursion
while (matches != null) {
const match = matches[0]
const indentationLength = findIndentationLength(match)
const rawTransforms = handleMatch(match)
const transforms = handleSuperview(rawTransforms, match)
const statements = handleTransforms(transforms, indentationLength)
const string = handleStatements(statements, indentationLength)
data = data.replace(match, string)
// Examine again
matches = data.match(regex)
}
// Handle import
data = handleImport(data)
Fs.writeFile(file, data, 'utf8')
})
}
/// Replace or remove import
function handleImport(data) {
if (data.includes('import Sugar')) {
return data.replace('import Cartography', '')
} else {
return data.replace('import Cartography', 'import Sugar')
}
}
/// Format transform to have `,` and linebreaks
function handleTransforms(transforms, indentationLength) {
let string = ''
// Expect the first is always line break
if (transforms[0] != '\n') {
transforms.unshift('\n')
}
const indentation = makeIndentation(indentationLength + 2)
transforms.forEach((line, index) => {
if (line.length > 1) {
string = string.concat(indentation + line)
if (index < transforms.length - 1) {
string = string.concat(',\n')
}
} else {
string = string.concat('\n')
}
})
return string
}
/// Replace superView
function handleSuperview(transforms, match) {
const superView = findSuperview(match)
if (superView == null) {
return transforms
}
const superViewPattern = ' superView.'
transforms = transforms.map((transform) => {
if (transform.includes(superViewPattern)) {
return transform.replace(superViewPattern, ` ${superView}.`)
} else {
return transform
}
})
return transforms
}
/// Embed the statements inside `Constraint.on`
function handleStatements(statements, indentationLength) {
const indentation = makeIndentation(indentationLength)
return `${indentation}Constraint.on(${statements}\n${indentation})`
}
/// Turn every line into flatten transformed line
function handleMatch(match) {
let lines = match.split('\n')
lines = lines.map((line) => {
return handleLine(line.trim())
}).filter((transforms) => {
return transforms != null
})
let flatten = [].concat.apply([], lines)
return flatten
}
/// Check to handle lines, turn them into `LayoutAnchor` statements
function handleLine(line) {
const itemPattern = '\\w*\\.\\w* (=|<|>)= \\w*\\.*\\..*'
const sizePattern = '\w*\.\w* (=|<|>)= (\s|.)*'
if (line.includes('edges == ')) {
return handleEdges(line)
} else if (line.includes('center == ')) {
return handleCenter(line)
} else if (hasPattern(line, itemPattern) && !hasSizeKeywords(line)) {
return handleItem(line)
} else if (hasPattern(line, sizePattern)) {
return handleSize(line)
} else if (line.includes('>=') || line.includes('>=')) {
return line
} else if (line.includes('.')) {
// return the line itself to let the human fix
return line
} else if (line.length == 0) {
return ['\n']
} else {
return null
}
}
/// For ex: listView.bottom == listView.superview!.bottom - 43
/// listView.bottom == listView.superview!.bottom - Metrics.BackButtonWidth
function handleItem(line) {
const equalSign = getEqualSign(line)
const parts = line.split(` ${equalSign} `)
const left = parts[0]
let rightParts = parts[1].trim()
let right = rightParts.split(' ')[0]
let number = rightParts.replace(right, '').replace('- ', '-').trim()
if (number.startsWith('+ ')) {
number = number.slice(2)
}
let equal = getEqual(line)
if (number == null || number.length == 0 ) {
return [
`${left}Anchor.constraint(${equal}: ${right}Anchor)`
]
} else {
return [
`${left}Anchor.constraint(${equal}: ${right}Anchor, constant: ${number})`
]
}
}
/// For ex: segmentedControl.height == 24
/// backButton.width == Metrics.BackButtonWidth
function handleSize(line) {
const equalSign = getEqualSign(line)
const parts = line.split(` ${equalSign} `)
const left = parts[0]
const right = parts[1]
let equal = getEqual(line)
return [
`${left}Anchor.constraint(${equal}Constant: ${right})`
]
}
/// For ex: mapView.edges == listView.edges
function handleEdges(line) {
const parts = line.split(' == ')
const left = parts[0].split('.')[0]
const right = removeLastPart(parts[1])
return [
`${left}.topAnchor.constraint(equalTo: ${right}.topAnchor)`,
`${left}.bottomAnchor.constraint(equalTo: ${right}.bottomAnchor)`,
`${left}.leftAnchor.constraint(equalTo: ${right}.leftAnchor)`,
`${left}.rightAnchor.constraint(equalTo: ${right}.rightAnchor)`
]
}
/// For ex: mapView.center == listView.center
function handleCenter(line) {
const parts = line.split(' == ')
const left = parts[0].split('.')[0]
const right = removeLastPart(parts[1])
return [
`${left}.centerXAnchor.constraint(equalTo: ${right}.centerXAnchor)`,
`${left}.centerYAnchor.constraint(equalTo: ${right}.centerYAnchor)`
]
}
function hasPattern(string, pattern) {
const regex = new RegExp(pattern)
let matches = string.match(regex)
if (matches == null) {
return false
}
matches = matches.filter((match) => {
return match !== undefined && match.length > 1
})
return matches.length > 0
}
function removeLastPart(string) {
const parts = string.split('.')
parts.pop()
return parts.join('.')
}
function getEqual(line) {
if (line.includes('==')) {
return 'equalTo'
} else if (line.includes('<=')) {
return 'lessThanOrEqualTo'
} else {
return 'greaterThanOrEqualTo'
}
}
function getEqualSign(line) {
if (line.includes('==')) {
return '=='
} else if (line.includes('>=')) {
return '>='
} else {
return '<='
}
}
function findIndentationLength(match) {
return match.split('constrain')[0].length + 1
}
function makeIndentation(length) {
return Array(length).join(' ')
}
// For ex: constrain(tableView, headerView, view) { tableView, headerView, superView in
function findSuperview(match) {
const line = match.split('\n')[0]
if (!line.includes(' superView in')) {
return null
}
const pattern = ', \\w*\\)'
const regex = RegExp(pattern)
const string = line.match(regex)[0] // `, view)
return string.replace(', ', '').replace(')', '')
}
// Check special size keywords
function hasSizeKeywords(line) {
const keywords = ['Metrics', 'Dimensions', 'UIScreen']
return keywords.filter((keyword) => {
return line.includes(keyword)
}).length != 0
}
main()
{
"name": "AutoLayoutConverter",
"version": "1.0.0",
"description": "Convert Cartography to LayoutAnchor",
"main": "index.js",
"author": "Khoa Pham",
"license": "MIT",
"dependencies": {
"glob": "^7.1.2"
},
"scripts": {
"start": "node index.js"
}
}
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
brace-expansion@^1.1.7:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
brace-expansion "^1.1.7"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@billypchan
Copy link

billypchan commented Oct 12, 2021

Feedback:

  • if self is in the constrain input, it can not replaced and cause compiler error.
  • script hangs in some files. (I guess the issue occurs when there is a class constant in the Cartography code. when a . in constraint input the script can not handle)
  • can not convert priority ~ operator correctly
  • can not handle inset
  • can not handle align(top/bottom: var1, var2, var3...) (the lines are removed)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment