@hsusulist/luc 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.ps1 CHANGED
@@ -43,6 +43,10 @@ $script:UseAnsi = $false
43
43
  $script:InAltScreen = $false
44
44
  $script:Interactive = $false
45
45
  $script:Version = ''
46
+ $script:MaintenanceAction = 'install'
47
+ $script:ExistingComponents = @()
48
+ $script:ExistingInstall = $null
49
+ $script:ScriptDirectory = if ($PSCommandPath) { Split-Path -Parent $PSCommandPath } else { $null }
46
50
 
47
51
  # ---------------------------------------------------------------------------
48
52
  # Console helpers
@@ -253,6 +257,153 @@ function Show-SourcePage {
253
257
  }
254
258
  }
255
259
 
260
+ function Get-InstalledDetails {
261
+ param([string]$RequestedPath)
262
+
263
+ $state = $null
264
+ $stateFile = Join-Path (Join-Path $env:LOCALAPPDATA 'LUC') 'state.json'
265
+ if (Test-Path -LiteralPath $stateFile -PathType Leaf) {
266
+ try {
267
+ $state = Get-Content -LiteralPath $stateFile -Raw | ConvertFrom-Json -ErrorAction Stop
268
+ } catch {
269
+ Write-Note "Could not read the install state: $($_.Exception.Message)" 'warn'
270
+ }
271
+ }
272
+
273
+ $path = $RequestedPath
274
+ if (-not $path -and $script:ScriptDirectory -and
275
+ (Test-Path -LiteralPath (Join-Path $script:ScriptDirectory 'luc.exe') -PathType Leaf)) {
276
+ $path = $script:ScriptDirectory
277
+ }
278
+ if (-not $path -and $state -and $state.installPath) { $path = [string]$state.installPath }
279
+ if (-not $path) { $path = $defaultInstallPath }
280
+ $path = Resolve-InstallPathValue $path
281
+ if (-not (Test-Path -LiteralPath (Join-Path $path 'luc.exe') -PathType Leaf)) { return $null }
282
+
283
+ if ($state -and $state.installPath) {
284
+ try {
285
+ $statePath = Resolve-InstallPathValue ([string]$state.installPath)
286
+ if (-not [string]::Equals($statePath, $path, [StringComparison]::OrdinalIgnoreCase)) {
287
+ $state = $null
288
+ }
289
+ } catch {
290
+ Write-Note "Could not resolve the install state path: $($_.Exception.Message)" 'warn'
291
+ $state = $null
292
+ }
293
+ } elseif ($RequestedPath) {
294
+ $state = $null
295
+ }
296
+
297
+ $uninstallKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Luc_is1'
298
+ $registeredPath = $null
299
+ $registeredVersion = $null
300
+ try {
301
+ $registration = Get-ItemProperty -LiteralPath $uninstallKey -ErrorAction Stop
302
+ $registeredPath = [string]$registration.InstallLocation
303
+ $registeredVersion = [string]$registration.DisplayVersion
304
+ } catch { }
305
+ $registeredMatches = $false
306
+ if ($registeredPath) {
307
+ try {
308
+ $registeredMatches = [string]::Equals(
309
+ (Resolve-InstallPathValue $registeredPath), $path, [StringComparison]::OrdinalIgnoreCase)
310
+ if (-not $registeredMatches) { $registeredVersion = $null }
311
+ } catch {
312
+ Write-Note "Could not resolve the Windows installer path: $($_.Exception.Message)" 'warn'
313
+ $registeredPath = $null
314
+ $registeredVersion = $null
315
+ }
316
+ }
317
+ $innoStateMatches = $state -and $state.source -eq 'inno' -and $state.installPath
318
+
319
+ $rawComponents = @()
320
+ if ($state -and $state.components) {
321
+ $rawComponents = @($state.components)
322
+ } else {
323
+ $manifestFile = Join-Path $path '.luc-manifest.json'
324
+ if (Test-Path -LiteralPath $manifestFile -PathType Leaf) {
325
+ try {
326
+ $manifest = Get-Content -LiteralPath $manifestFile -Raw | ConvertFrom-Json -ErrorAction Stop
327
+ if ($manifest.components) { $rawComponents = @($manifest.components) }
328
+ } catch {
329
+ Write-Note "Could not read the install manifest: $($_.Exception.Message)" 'warn'
330
+ }
331
+ }
332
+ }
333
+ if ($rawComponents.Count -eq 0) {
334
+ if ($registeredMatches -or ($innoStateMatches -and -not $registeredPath)) {
335
+ try {
336
+ $saved = (Get-ItemProperty -LiteralPath 'HKCU:\Software\LUC' -Name Components -ErrorAction Stop).Components
337
+ if ($saved) { $rawComponents = @([string]$saved -split '[,\s]+') }
338
+ } catch { }
339
+ }
340
+ }
341
+
342
+ $components = @()
343
+ foreach ($name in $rawComponents) {
344
+ $name = ([string]$name).ToLowerInvariant()
345
+ if ($name -eq 'ailib') { $name = 'ai' }
346
+ if ($optionalComponents -contains $name -and $components -notcontains $name) {
347
+ $components += $name
348
+ }
349
+ }
350
+
351
+ $uninstaller = $null
352
+ if ($innoStateMatches -or $registeredMatches) {
353
+ $uninstaller = Get-ChildItem -LiteralPath $path -Filter 'unins*.exe' -File -ErrorAction SilentlyContinue |
354
+ Select-Object -First 1
355
+ }
356
+ return [pscustomobject]@{
357
+ Path = $path
358
+ Version = if ($state -and $state.version) { [string]$state.version } elseif ($registeredVersion) { $registeredVersion } else { 'unknown' }
359
+ Components = $components
360
+ Uninstaller = $uninstaller
361
+ }
362
+ }
363
+
364
+ function Show-MaintenancePage {
365
+ param($Install)
366
+
367
+ $items = @(
368
+ @{ Action = 'libraries'; Label = 'Install libraries'; Desc = 'Add or remove optional components' },
369
+ @{ Action = 'repair'; Label = 'Fix / reinstall'; Desc = 'Reinstall the current components' }
370
+ )
371
+ if ($Install.Uninstaller) {
372
+ $items += @{ Action = 'uninstall'; Label = 'Uninstall'; Desc = 'Remove LUC from this computer' }
373
+ }
374
+ $items += @{ Action = 'cancel'; Label = 'Cancel'; Desc = 'Exit without making changes' }
375
+ $index = 0
376
+ while ($true) {
377
+ Clear-Tui
378
+ Write-TuiHeader 'LUC is installed' 'Choose an action'
379
+ Write-Tui (" Version {0}" -f $Install.Version)
380
+ Write-Tui (" Folder {0}" -f $Install.Path) '90'
381
+ Write-Tui ''
382
+ for ($i = 0; $i -lt $items.Count; $i++) {
383
+ $marker = if ($i -eq $index) { '>' } else { ' ' }
384
+ $code = if ($i -eq $index) { '1;36' } else { '' }
385
+ Write-Tui (" $marker " + $items[$i].Label) $code
386
+ Write-Tui (" " + $items[$i].Desc) '90'
387
+ Write-Tui ''
388
+ }
389
+ Write-TuiFooter 'Up/Down move Enter select q quit'
390
+ $key = [Console]::ReadKey($true)
391
+ switch ($key.Key) {
392
+ 'UpArrow' { $index = ($index - 1 + $items.Count) % $items.Count }
393
+ 'DownArrow' { $index = ($index + 1) % $items.Count }
394
+ 'Enter' { return $items[$index].Action }
395
+ 'Escape' { return 'cancel' }
396
+ 'Q' { return 'cancel' }
397
+ }
398
+ }
399
+ }
400
+
401
+ function Start-InnoUninstaller {
402
+ param($Install)
403
+ if (-not $Install.Uninstaller) { throw 'The Windows installer uninstaller was not found.' }
404
+ Start-Process -FilePath $Install.Uninstaller.FullName -ArgumentList '/NORESTART' | Out-Null
405
+ }
406
+
256
407
  function Show-LicenseEntry {
257
408
  param([string]$LicenseText)
258
409
  Show-LicensePage $LicenseText
@@ -846,7 +997,7 @@ public static extern bool MoveFileEx(string existingFileName, string newFileName
846
997
  }
847
998
 
848
999
  function Install-ArchivePlan {
849
- param([object[]]$Items)
1000
+ param([object[]]$Items, [string[]]$RemovePaths = @())
850
1001
  $journal = New-Object System.Collections.ArrayList
851
1002
  try {
852
1003
  foreach ($item in $Items) {
@@ -865,6 +1016,13 @@ function Install-ArchivePlan {
865
1016
  try { Move-Item -LiteralPath $item.Staging -Destination $destination -Force }
866
1017
  catch { throw "Cannot write '$destination': $($_.Exception.Message)" }
867
1018
  }
1019
+ foreach ($destination in $RemovePaths) {
1020
+ if (-not (Test-Path -LiteralPath $destination -PathType Leaf)) { continue }
1021
+ $old = "$destination.luc-old-$([guid]::NewGuid().ToString('N').Substring(0, 8))"
1022
+ try { Move-Item -LiteralPath $destination -Destination $old -Force }
1023
+ catch { throw "Cannot remove '$destination'. Close the program using it and try again." }
1024
+ [void]$journal.Add([pscustomobject]@{ Destination = $destination; Old = $old })
1025
+ }
868
1026
  } catch {
869
1027
  $failure = $_
870
1028
  for ($i = $journal.Count - 1; $i -ge 0; $i--) {
@@ -966,6 +1124,26 @@ $downloadTemp = $null
966
1124
  $tempChecksumFile = $null
967
1125
  $zip = $null
968
1126
  try {
1127
+ if ($script:Interactive -and -not $LCodeOnly -and -not $Components) {
1128
+ $script:ExistingInstall = Get-InstalledDetails -RequestedPath $InstallPath
1129
+ if ($script:ExistingInstall) {
1130
+ Enter-AltScreen
1131
+ $script:MaintenanceAction = Show-MaintenancePage $script:ExistingInstall
1132
+ Exit-AltScreen
1133
+ if ($script:MaintenanceAction -eq 'cancel') {
1134
+ Write-Note 'Installation cancelled.' 'warn'
1135
+ exit 2
1136
+ }
1137
+ if ($script:MaintenanceAction -eq 'uninstall') {
1138
+ Start-InnoUninstaller $script:ExistingInstall
1139
+ Write-Note 'LUC uninstaller started.' 'ok'
1140
+ exit 0
1141
+ }
1142
+ $InstallPath = $script:ExistingInstall.Path
1143
+ $script:ExistingComponents = @($script:ExistingInstall.Components)
1144
+ }
1145
+ }
1146
+
969
1147
  $sourceSpecified = $PSBoundParameters.ContainsKey('Source')
970
1148
  if (-not $Source) {
971
1149
  if ($ArchivePath) { $Source = 'local' } else { $Source = 'github' }
@@ -1081,7 +1259,13 @@ try {
1081
1259
  $licenseText = Read-ZipEntryText $zip 'LICENCE'
1082
1260
  if (-not $licenseText) { $licenseText = 'LUC is free software released under the Apache License 2.0.' }
1083
1261
 
1084
- $selected = if ($LCodeOnly) { @('lcode') } else { @() }
1262
+ if ($LCodeOnly) {
1263
+ $selected = @('lcode')
1264
+ } elseif ($script:MaintenanceAction -ne 'install') {
1265
+ $selected = @($script:ExistingComponents)
1266
+ } else {
1267
+ $selected = @()
1268
+ }
1085
1269
  if (-not $LCodeOnly -and $Components) {
1086
1270
  foreach ($item in $Components) {
1087
1271
  foreach ($name in ($item -split '[,\s]+')) {
@@ -1094,7 +1278,8 @@ try {
1094
1278
  }
1095
1279
  }
1096
1280
 
1097
- if (-not $LCodeOnly -and $script:Interactive -and -not $Components) {
1281
+ if (-not $LCodeOnly -and $script:Interactive -and -not $Components -and
1282
+ $script:MaintenanceAction -ne 'repair') {
1098
1283
  Enter-AltScreen
1099
1284
  $picked = Show-ComponentPage -Preselected $selected -LicenseText $licenseText
1100
1285
  Exit-AltScreen
@@ -1115,6 +1300,14 @@ try {
1115
1300
  $InstallPath = Resolve-InstallPathValue $InstallPath
1116
1301
  }
1117
1302
 
1303
+ if (-not $script:ExistingInstall -or
1304
+ -not [string]::Equals($script:ExistingInstall.Path, $InstallPath, [StringComparison]::OrdinalIgnoreCase)) {
1305
+ $script:ExistingInstall = Get-InstalledDetails -RequestedPath $InstallPath
1306
+ }
1307
+ if ($script:ExistingInstall) {
1308
+ $script:ExistingComponents = @($script:ExistingInstall.Components)
1309
+ }
1310
+
1118
1311
  $vsCodeTarget = Join-Path $env:USERPROFILE '.vscode\extensions\hsusulist.luc'
1119
1312
  $binaryName = if ($LCodeOnly) { 'lcode.exe' } else { 'luc.exe' }
1120
1313
  $upgrade = Test-Path -LiteralPath (Join-Path $InstallPath $binaryName) -PathType Leaf
@@ -1140,6 +1333,10 @@ try {
1140
1333
  Exit-AltScreen
1141
1334
  if ($null -eq $picked -or -not $InstallPath) { Write-Note 'Installation cancelled.' 'warn'; exit 2 }
1142
1335
  $selected = @($picked)
1336
+ $InstallPath = Resolve-InstallPathValue $InstallPath
1337
+ $script:ExistingInstall = Get-InstalledDetails -RequestedPath $InstallPath
1338
+ $script:ExistingComponents = if ($script:ExistingInstall) { @($script:ExistingInstall.Components) } else { @() }
1339
+ $upgrade = Test-Path -LiteralPath (Join-Path $InstallPath $binaryName) -PathType Leaf
1143
1340
  }
1144
1341
  }
1145
1342
 
@@ -1149,10 +1346,24 @@ try {
1149
1346
  throw "Installation archive did not produce $binaryName."
1150
1347
  }
1151
1348
 
1349
+ $removePaths = @()
1350
+ if ($upgrade -and -not $LCodeOnly -and $script:ExistingComponents.Count -gt 0) {
1351
+ $previousPlan = Get-ArchivePlan -Zip $zip -SelectedComponents $script:ExistingComponents -Target $InstallPath -VsCodeTarget $vsCodeTarget
1352
+ $newDestinations = @{}
1353
+ foreach ($item in $archivePlan.Items) { $newDestinations[$item.Destination.ToLowerInvariant()] = $true }
1354
+ foreach ($item in $previousPlan.Items) {
1355
+ if (-not $newDestinations.ContainsKey($item.Destination.ToLowerInvariant()) -and
1356
+ (Test-Path -LiteralPath $item.Destination -PathType Leaf)) {
1357
+ $removePaths += $item.Destination
1358
+ }
1359
+ }
1360
+ }
1361
+
1152
1362
  $roots = @($InstallPath)
1153
- if ($selected -contains 'vscode') { $roots += $vsCodeTarget }
1363
+ if (($selected -contains 'vscode') -or ($script:ExistingComponents -contains 'vscode')) { $roots += $vsCodeTarget }
1364
+ $lockItems = @($archivePlan.Items) + @($removePaths | ForEach-Object { @{ Destination = $_ } })
1154
1365
  Write-Step 'Checking for running programs and locked files'
1155
- Test-InstallLocks -Items $archivePlan.Items -Roots $roots
1366
+ Test-InstallLocks -Items $lockItems -Roots $roots
1156
1367
  Write-StepDone 'No running LUC process blocks the install'
1157
1368
 
1158
1369
  Test-DiskSpace $InstallPath $archivePlan.TotalBytes
@@ -1165,17 +1376,19 @@ try {
1165
1376
  Write-StepDone "Extracted $($archivePlan.Items.Count) files"
1166
1377
 
1167
1378
  Write-Step 'Installing files'
1168
- Install-ArchivePlan $archivePlan.Items
1169
- Write-StepDone 'Files installed'
1379
+ Install-ArchivePlan -Items $archivePlan.Items -RemovePaths $removePaths
1380
+ if ($removePaths.Count) { Write-StepDone "Files installed; removed $($removePaths.Count) deselected files" }
1381
+ else { Write-StepDone 'Files installed' }
1170
1382
 
1171
1383
  if (-not (Test-Path -LiteralPath (Join-Path $InstallPath $binaryName) -PathType Leaf)) {
1172
1384
  throw "Installation finished without $binaryName. The previous version was restored."
1173
1385
  }
1174
1386
 
1175
1387
  Write-InstallManifest $script:Version $InstallPath $selected -LCodeOnly:$LCodeOnly
1176
- Write-InstallState $script:Version $InstallPath $selected $Source -LCodeOnly:$LCodeOnly
1388
+ $stateSource = if ($script:ExistingInstall -and $script:ExistingInstall.Uninstaller) { 'inno' } else { $Source }
1389
+ Write-InstallState $script:Version $InstallPath $selected $stateSource -LCodeOnly:$LCodeOnly
1177
1390
 
1178
- # Keep a copy of this script next to luc.exe so that "luc --install" works
1391
+ # Keep a copy of this script next to luc.exe so that "luc install" works
1179
1392
  # even when the archive was built before install.ps1 was bundled in it.
1180
1393
  if (-not $LCodeOnly) {
1181
1394
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hsusulist/luc",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "LUC language installer and launcher",
5
5
  "bin": {
6
6
  "luc": "bin/luc.js"
@@ -41,7 +41,7 @@ if (process.platform !== 'win32' || findBinary()) {
41
41
 
42
42
  const script = path.join(__dirname, '..', 'install.ps1');
43
43
  if (!fs.existsSync(script)) {
44
- console.warn('luc: install.ps1 is missing; run "luc --install" after installing.');
44
+ console.warn('luc: install.ps1 is missing; run "luc install" after installing.');
45
45
  process.exit(0);
46
46
  }
47
47
 
@@ -53,6 +53,6 @@ const result = spawnSync(
53
53
  );
54
54
 
55
55
  if (result.error || result.status !== 0) {
56
- console.warn('luc: automatic install did not finish. Run "luc --install" to open the installer.');
56
+ console.warn('luc: automatic install did not finish. Run "luc install" to open the installer.');
57
57
  }
58
58
  process.exit(0);
@@ -24,7 +24,7 @@ if (fs.existsSync(target)) {
24
24
  throw new Error('refusing to overwrite existing npm/src directory');
25
25
  }
26
26
  fs.mkdirSync(target);
27
- for (const name of ['luc_core.c', 'luc_libs.c', 'luc_trans.c', 'luc.h']) {
27
+ for (const name of ['luc_core.c', 'luc_libs.c', 'luc_nums.c', 'luc_trans.c', 'luc.h']) {
28
28
  const from = path.join(source, name);
29
29
  if (!fs.existsSync(from)) throw new Error('missing release source: ' + from);
30
30
  fs.copyFileSync(from, path.join(target, name));
@@ -63,6 +63,298 @@ static void lc_linux_insert(LcLinuxEditor *e, char c) {
63
63
  e->dirty = 1;
64
64
  }
65
65
 
66
+ static int lc_linux_word_char(unsigned char c) {
67
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
68
+ (c >= '0' && c <= '9') || c == '_';
69
+ }
70
+
71
+ static unsigned char lc_linux_lower(unsigned char c) {
72
+ return c >= 'A' && c <= 'Z' ? (unsigned char)(c + ('a' - 'A')) : c;
73
+ }
74
+
75
+ static int lc_linux_starts_word(const char *s, const char *word) {
76
+ size_t i = 0;
77
+ while (*s == ' ' || *s == '\t') s++;
78
+ while (word[i] && lc_linux_lower((unsigned char)s[i]) == lc_linux_lower((unsigned char)word[i])) i++;
79
+ return word[i] == 0 && !lc_linux_word_char((unsigned char)s[i]);
80
+ }
81
+
82
+ static int lc_linux_ends_word(const char *s, const char *word) {
83
+ size_t n = strlen(s), m = strlen(word), i;
84
+ while (n && (s[n - 1] == ' ' || s[n - 1] == '\t')) n--;
85
+ if (n < m) return 0;
86
+ for (i = 0; i < m; i++) {
87
+ if (lc_linux_lower((unsigned char)s[n - m + i]) != lc_linux_lower((unsigned char)word[i])) return 0;
88
+ }
89
+ return n == m || !lc_linux_word_char((unsigned char)s[n - m - 1]);
90
+ }
91
+
92
+ static int lc_linux_has_word(const char *s, const char *word) {
93
+ size_t i, n = strlen(s), m = strlen(word), j;
94
+ if (!m || m > n) return 0;
95
+ for (i = 0; i + m <= n; i++) {
96
+ if (i && lc_linux_word_char((unsigned char)s[i - 1])) continue;
97
+ if (i + m < n && lc_linux_word_char((unsigned char)s[i + m])) continue;
98
+ for (j = 0; j < m; j++) {
99
+ if (lc_linux_lower((unsigned char)s[i + j]) != lc_linux_lower((unsigned char)word[j])) break;
100
+ }
101
+ if (j == m) return 1;
102
+ }
103
+ return 0;
104
+ }
105
+
106
+ static int lc_linux_ext(const char *path, const char *ext) {
107
+ const char *dot = strrchr(path, '.');
108
+ size_t i, n = strlen(ext);
109
+ if (!dot || strlen(dot + 1) != n) return 0;
110
+ for (i = 0; i < n; i++) {
111
+ if (lc_linux_lower((unsigned char)dot[i + 1]) != lc_linux_lower((unsigned char)ext[i])) return 0;
112
+ }
113
+ return 1;
114
+ }
115
+
116
+ static void lc_linux_strip_comment(char *s, const char *path) {
117
+ int quote = 0, escape = 0;
118
+ int hash = lc_linux_ext(path, "py") || lc_linux_ext(path, "sh") ||
119
+ lc_linux_ext(path, "bash") || lc_linux_ext(path, "zsh") ||
120
+ lc_linux_ext(path, "rb") || lc_linux_ext(path, "pl") ||
121
+ lc_linux_ext(path, "r") || lc_linux_ext(path, "yaml") ||
122
+ lc_linux_ext(path, "yml");
123
+ int dash = !path[0] || lc_linux_ext(path, "luc") || lc_linux_ext(path, "lua") ||
124
+ lc_linux_ext(path, "sql");
125
+ int slash = lc_linux_ext(path, "c") || lc_linux_ext(path, "h") ||
126
+ lc_linux_ext(path, "cpp") || lc_linux_ext(path, "hpp") ||
127
+ lc_linux_ext(path, "cc") || lc_linux_ext(path, "cxx") ||
128
+ lc_linux_ext(path, "js") || lc_linux_ext(path, "ts") ||
129
+ lc_linux_ext(path, "java") || lc_linux_ext(path, "go") ||
130
+ lc_linux_ext(path, "rs") || lc_linux_ext(path, "cs") ||
131
+ lc_linux_ext(path, "kt") || lc_linux_ext(path, "swift") ||
132
+ lc_linux_ext(path, "dart") || lc_linux_ext(path, "scala") ||
133
+ lc_linux_ext(path, "php") || lc_linux_ext(path, "css") ||
134
+ lc_linux_ext(path, "scss") || lc_linux_ext(path, "less") ||
135
+ lc_linux_ext(path, "tf") || lc_linux_ext(path, "hcl");
136
+ size_t i, n = strlen(s);
137
+ for (i = 0; i < n; i++) {
138
+ unsigned char c = (unsigned char)s[i];
139
+ if (quote) {
140
+ if (escape) escape = 0;
141
+ else if (c == '\\') escape = 1;
142
+ else if (c == (unsigned char)quote) quote = 0;
143
+ continue;
144
+ }
145
+ if (c == '\'' || c == '"' || c == '`') { quote = c; continue; }
146
+ if (hash && c == '#') { s[i] = 0; return; }
147
+ if (dash && c == '-' && s[i + 1] == '-') { s[i] = 0; return; }
148
+ if (slash && c == '/' && s[i + 1] == '/') { s[i] = 0; return; }
149
+ if (slash && c == '/' && s[i + 1] == '*') { s[i] = 0; return; }
150
+ }
151
+ }
152
+
153
+ static int lc_linux_html_block(const char *s, char *closer, size_t cap) {
154
+ static const char *voids[] = {
155
+ "area", "base", "br", "col", "embed", "hr", "img", "input",
156
+ "link", "meta", "param", "source", "track", "wbr", NULL
157
+ };
158
+ char tag[128];
159
+ size_t n = strlen(s), i = 1, j = 0, k;
160
+ if (n < 3 || s[0] != '<' || s[1] == '/' || s[1] == '!' ||
161
+ s[1] == '?' || s[n - 1] != '>') return 0;
162
+ while (i < n - 1 && ((s[i] >= 'a' && s[i] <= 'z') ||
163
+ (s[i] >= 'A' && s[i] <= 'Z') || (s[i] >= '0' && s[i] <= '9') ||
164
+ s[i] == ':' || s[i] == '-')) {
165
+ if (j + 1 >= sizeof tag) return 0;
166
+ tag[j++] = s[i++];
167
+ }
168
+ if (!j || s[n - 2] == '/') return 0;
169
+ tag[j] = 0;
170
+ for (k = 0; voids[k]; k++) {
171
+ size_t a = strlen(tag), b = strlen(voids[k]), x;
172
+ if (a != b) continue;
173
+ for (x = 0; x < a; x++) {
174
+ if (lc_linux_lower((unsigned char)tag[x]) != lc_linux_lower((unsigned char)voids[k][x])) break;
175
+ }
176
+ if (x == a) return 0;
177
+ }
178
+ return snprintf(closer, cap, "</%s>", tag) < (int)cap;
179
+ }
180
+
181
+ static int lc_linux_block(const char *path, char *code, char *closer, size_t cap) {
182
+ static const char *py_blocks[] = {
183
+ "if", "elif", "else", "for", "while", "def", "class", "try",
184
+ "except", "finally", "with", "match", "case", NULL
185
+ };
186
+ static const char *end_blocks[] = {
187
+ "def", "class", "module", "if", "unless", "while", "until",
188
+ "for", "begin", "case", NULL
189
+ };
190
+ size_t n = strlen(code), i;
191
+ const char *p = code;
192
+ closer[0] = 0;
193
+ while (n && (code[n - 1] == ' ' || code[n - 1] == '\t' || code[n - 1] == '\r')) code[--n] = 0;
194
+ if (!n) return 0;
195
+ if (code[n - 1] == '{') return snprintf(closer, cap, "}") < (int)cap;
196
+ if ((lc_linux_ext(path, "json") || lc_linux_ext(path, "js") ||
197
+ lc_linux_ext(path, "py") || lc_linux_ext(path, "c") ||
198
+ lc_linux_ext(path, "h") || lc_linux_ext(path, "go") ||
199
+ lc_linux_ext(path, "rs") || lc_linux_ext(path, "java") ||
200
+ lc_linux_ext(path, "cs") || lc_linux_ext(path, "ts") ||
201
+ lc_linux_ext(path, "php")) && code[n - 1] == '[') {
202
+ return snprintf(closer, cap, "]") < (int)cap;
203
+ }
204
+ if (lc_linux_ext(path, "html") || lc_linux_ext(path, "htm") ||
205
+ lc_linux_ext(path, "xml") || lc_linux_ext(path, "svg")) {
206
+ return lc_linux_html_block(code, closer, cap);
207
+ }
208
+ if (lc_linux_ext(path, "py")) {
209
+ if (code[n - 1] != ':') return 0;
210
+ if (lc_linux_starts_word(p, "async")) {
211
+ p += 5;
212
+ while (*p == ' ' || *p == '\t') p++;
213
+ }
214
+ for (i = 0; py_blocks[i]; i++) if (lc_linux_starts_word(p, py_blocks[i])) return 1;
215
+ return 0;
216
+ }
217
+ if (lc_linux_ext(path, "yaml") || lc_linux_ext(path, "yml")) return code[n - 1] == ':';
218
+ if (!path[0] || lc_linux_ext(path, "luc") || lc_linux_ext(path, "lua")) {
219
+ if (lc_linux_starts_word(code, "else") || lc_linux_starts_word(code, "elseif")) return 1;
220
+ if (lc_linux_ext(path, "lua") && lc_linux_ends_word(code, "repeat")) {
221
+ return snprintf(closer, cap, "until") < (int)cap;
222
+ }
223
+ if (lc_linux_ends_word(code, "then") || lc_linux_ends_word(code, "do") ||
224
+ (lc_linux_has_word(code, "function") && code[n - 1] == ')') ||
225
+ (lc_linux_starts_word(code, "command") && code[n - 1] == ')')) {
226
+ return snprintf(closer, cap, "end") < (int)cap;
227
+ }
228
+ return 0;
229
+ }
230
+ if (lc_linux_ext(path, "rb") || lc_linux_ext(path, "pl")) {
231
+ if (lc_linux_starts_word(code, "else") || lc_linux_starts_word(code, "elsif") ||
232
+ lc_linux_starts_word(code, "when") || lc_linux_starts_word(code, "rescue") ||
233
+ lc_linux_starts_word(code, "ensure") || lc_linux_starts_word(code, "continue")) return 1;
234
+ for (i = 0; end_blocks[i]; i++) {
235
+ if (lc_linux_starts_word(code, end_blocks[i])) return snprintf(closer, cap, "end") < (int)cap;
236
+ }
237
+ if (lc_linux_ends_word(code, "do") || lc_linux_has_word(code, "do")) {
238
+ return snprintf(closer, cap, "end") < (int)cap;
239
+ }
240
+ if ((lc_linux_ext(path, "pl") && lc_linux_starts_word(code, "sub")) ||
241
+ (lc_linux_ext(path, "rb") && lc_linux_starts_word(code, "def"))) {
242
+ return snprintf(closer, cap, "end") < (int)cap;
243
+ }
244
+ }
245
+ if (lc_linux_ext(path, "sh") || lc_linux_ext(path, "bash") || lc_linux_ext(path, "zsh")) {
246
+ if (lc_linux_starts_word(code, "else") || lc_linux_starts_word(code, "elif")) return 1;
247
+ if (lc_linux_starts_word(code, "case") && lc_linux_ends_word(code, "in")) {
248
+ return snprintf(closer, cap, "esac") < (int)cap;
249
+ }
250
+ if (lc_linux_ends_word(code, "then")) return snprintf(closer, cap, "fi") < (int)cap;
251
+ if (lc_linux_ends_word(code, "do")) return snprintf(closer, cap, "done") < (int)cap;
252
+ }
253
+ if (lc_linux_ext(path, "sql")) {
254
+ if (lc_linux_starts_word(code, "else") || lc_linux_starts_word(code, "when")) return 1;
255
+ if (lc_linux_ends_word(code, "case") || lc_linux_ends_word(code, "begin")) {
256
+ return snprintf(closer, cap, "END;") < (int)cap;
257
+ }
258
+ if (lc_linux_ends_word(code, "then")) {
259
+ return snprintf(closer, cap, lc_linux_has_word(code, "if") ? "END IF;" : "END;") < (int)cap;
260
+ }
261
+ if (lc_linux_ends_word(code, "loop")) return snprintf(closer, cap, "END LOOP;") < (int)cap;
262
+ }
263
+ return 0;
264
+ }
265
+
266
+ static int lc_linux_tail_closer(const char *tail, const char *closer) {
267
+ size_t i = 0, j, n = strlen(tail), m = strlen(closer);
268
+ while (i < n && (tail[i] == ' ' || tail[i] == '\t' || tail[i] == '\r')) i++;
269
+ if (n - i < m) return 0;
270
+ for (j = 0; j < m; j++) {
271
+ if (lc_linux_lower((unsigned char)tail[i + j]) != lc_linux_lower((unsigned char)closer[j])) return 0;
272
+ }
273
+ i += m;
274
+ while (i < n && (tail[i] == ' ' || tail[i] == '\t' || tail[i] == '\r')) i++;
275
+ if (i < n && tail[i] == ';') i++;
276
+ while (i < n && (tail[i] == ' ' || tail[i] == '\t' || tail[i] == '\r')) i++;
277
+ return i == n;
278
+ }
279
+
280
+ static void lc_linux_newline(LcLinuxEditor *e) {
281
+ size_t start = lc_linux_line_start(e, e->cursor);
282
+ size_t end = lc_linux_line_end(e, e->cursor);
283
+ size_t prefix_len = e->cursor - start, tail_len = end - e->cursor;
284
+ size_t base_len = 0, inner_len, close_len = 0, i, insert_len;
285
+ char closer[256], *prefix, *code_buf, *code, *tail, *base, *insert;
286
+ int block, has_closer, tail_blank = 1, make_closer;
287
+ const char *path = e->path;
288
+ prefix = (char *)malloc(prefix_len + 1);
289
+ code_buf = (char *)malloc(prefix_len + 1);
290
+ tail = (char *)malloc(tail_len + 1);
291
+ if (!prefix || !code_buf || !tail) {
292
+ free(prefix); free(code_buf); free(tail);
293
+ snprintf(e->status, sizeof e->status, "out of memory");
294
+ return;
295
+ }
296
+ memcpy(prefix, e->data + start, prefix_len); prefix[prefix_len] = 0;
297
+ memcpy(code_buf, prefix, prefix_len + 1); code = code_buf;
298
+ memcpy(tail, e->data + e->cursor, tail_len); tail[tail_len] = 0;
299
+ lc_linux_strip_comment(code, path);
300
+ while (*code == ' ' || *code == '\t') code++;
301
+ block = lc_linux_block(path, code, closer, sizeof closer);
302
+ while (e->data[start + base_len] == ' ' || e->data[start + base_len] == '\t') base_len++;
303
+ base = (char *)malloc(base_len + 1);
304
+ if (!base) {
305
+ free(prefix); free(code_buf); free(tail);
306
+ snprintf(e->status, sizeof e->status, "out of memory");
307
+ return;
308
+ }
309
+ memcpy(base, e->data + start, base_len); base[base_len] = 0;
310
+ for (i = 0; i < tail_len; i++) {
311
+ if (tail[i] != ' ' && tail[i] != '\t' && tail[i] != '\r') { tail_blank = 0; break; }
312
+ }
313
+ has_closer = block && closer[0] && lc_linux_tail_closer(tail, closer);
314
+ if (block && !tail_blank && !has_closer) block = 0;
315
+ make_closer = block && closer[0];
316
+ inner_len = base_len + (block ? 4 : 0);
317
+ insert_len = 1 + inner_len;
318
+ if (make_closer) {
319
+ if (has_closer) {
320
+ size_t first = 0, last = tail_len;
321
+ while (first < last && (tail[first] == ' ' || tail[first] == '\t' || tail[first] == '\r')) first++;
322
+ while (last > first && (tail[last - 1] == ' ' || tail[last - 1] == '\t' || tail[last - 1] == '\r')) last--;
323
+ close_len = last - first;
324
+ memmove(tail, tail + first, close_len);
325
+ } else {
326
+ close_len = strlen(closer);
327
+ }
328
+ insert_len += 1 + base_len + close_len;
329
+ }
330
+ insert = (char *)malloc(insert_len);
331
+ if (!insert || !lc_linux_reserve(e, insert_len)) {
332
+ free(prefix); free(code_buf); free(tail); free(base); free(insert);
333
+ snprintf(e->status, sizeof e->status, "out of memory");
334
+ return;
335
+ }
336
+ i = 0;
337
+ insert[i++] = '\n';
338
+ memcpy(insert + i, base, base_len); i += base_len;
339
+ if (block) { memcpy(insert + i, " ", 4); i += 4; }
340
+ if (make_closer) {
341
+ insert[i++] = '\n';
342
+ memcpy(insert + i, base, base_len); i += base_len;
343
+ memcpy(insert + i, has_closer ? tail : closer, close_len); i += close_len;
344
+ }
345
+ if (block && tail_len) {
346
+ memmove(e->data + e->cursor, e->data + end, e->len - end + 1);
347
+ e->len -= tail_len;
348
+ }
349
+ memmove(e->data + e->cursor + insert_len, e->data + e->cursor,
350
+ e->len - e->cursor + 1);
351
+ memcpy(e->data + e->cursor, insert, insert_len);
352
+ e->cursor += 1 + inner_len;
353
+ e->len += insert_len;
354
+ e->dirty = 1;
355
+ free(prefix); free(code_buf); free(tail); free(base); free(insert);
356
+ }
357
+
66
358
  static int lc_linux_save(LcLinuxEditor *e) {
67
359
  FILE *f;
68
360
  if (!e->path[0]) {
@@ -116,7 +408,7 @@ static void lc_linux_draw(const LcLinuxEditor *e, size_t top, int rows, int cols
116
408
  size_t line = 0, i = 0, cursor_line, cursor_col = 0, scan = 0;
117
409
  int row = 0;
118
410
  cursor_line = lc_linux_line_number(e, e->cursor);
119
- while (scan < e.cursor && e->data[scan] != '\n') { cursor_col++; scan++; }
411
+ while (scan < e->cursor && e->data[scan] != '\n') { cursor_col++; scan++; }
120
412
 
121
413
  printf("\x1b[H\x1b[2J");
122
414
  while (row < rows - 1 && i < e->len + 1) {
@@ -271,7 +563,7 @@ static void lc_code(const char *start) {
271
563
  e.dirty = 1;
272
564
  }
273
565
  } else if (key == 13) {
274
- lc_linux_insert(&e, '\n');
566
+ lc_linux_newline(&e);
275
567
  } else if (key >= 32 && key <= 255) {
276
568
  lc_linux_insert(&e, (char)key);
277
569
  }