reslop 0.1.4 → 0.1.6

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/lib/git.js CHANGED
@@ -7,7 +7,7 @@ const path = require('node:path');
7
7
  const diff = require('./diff.js');
8
8
  const { parseDiff, synthesizeNewFile, itemsFromFiles } = diff;
9
9
  const { itemPath, isPathInScope, REVIEW_DIR } = require('./files.js');
10
- const { npmBin, spawnBase } = require('./sys.js');
10
+ const { npmBin, spawnBase, npmOpts } = require('./sys.js');
11
11
  const deps = require('./deps.js');
12
12
  const { foldDepItems, mergeDepFile, depFileMeta } = deps;
13
13
  const { collectUsedNames, parseAuditReport, parseOutdatedReport } = deps;
@@ -25,6 +25,23 @@ const GIT_MAX_BUFFER = 32 * 1024 * 1024;
25
25
  const BINARY_SIZE = 1000000;
26
26
  const AUDIT_MS = 15000;
27
27
  const INSTALL_MS = 120000;
28
+ const BRANCH_FORMAT = [
29
+ '%(HEAD)',
30
+ '%(refname:short)',
31
+ '%(objectname:short)',
32
+ '%(committerdate:relative)',
33
+ '%(upstream:track)',
34
+ '%(subject)',
35
+ ].join('%09');
36
+ const TRACK_AHEAD = /ahead (\d+)/;
37
+ const TRACK_BEHIND = /behind (\d+)/;
38
+ const ORIGIN_HEAD = 'refs/remotes/origin/HEAD';
39
+ const DEFAULT_BRANCHES = ['main', 'master'];
40
+ const PUSH_REJECTED = [
41
+ 'non-fast-forward',
42
+ 'failed to push some refs',
43
+ 'updates were rejected',
44
+ ];
28
45
 
29
46
  const oneLine = (text) => {
30
47
  const line = text.trim().split('\n')[0];
@@ -77,7 +94,15 @@ const runProc = (cmd, args, options = {}) =>
77
94
  let stderr = '';
78
95
  let settled = false;
79
96
  let timer = null;
80
- const child = spawn(cmd, args, spawnBase({ cwd, env }));
97
+ const spawnOpts = { cwd, env };
98
+ if (options.shell) spawnOpts.shell = true;
99
+ let child;
100
+ try {
101
+ child = spawn(cmd, args, spawnBase(spawnOpts));
102
+ } catch (error) {
103
+ resolve({ status: null, error, stdout: '', stderr: '' });
104
+ return;
105
+ }
81
106
  const finish = (result) => {
82
107
  if (settled) return;
83
108
  settled = true;
@@ -284,7 +309,7 @@ const runNpmJson = (top, args, timeout) => {
284
309
  const result = spawnSync(
285
310
  npmBin(),
286
311
  args,
287
- spawnBase({
312
+ npmOpts({
288
313
  cwd: top,
289
314
  encoding: GIT_ENCODING,
290
315
  maxBuffer: GIT_MAX_BUFFER,
@@ -302,6 +327,7 @@ const runNpmJsonAsync = async (top, args, timeout, signal) => {
302
327
  maxBuffer: GIT_MAX_BUFFER,
303
328
  timeout,
304
329
  signal,
330
+ shell: npmOpts().shell,
305
331
  });
306
332
  if (result.error) return '';
307
333
  return result.stdout || '';
@@ -456,7 +482,7 @@ const runNpm = (cwd, args) =>
456
482
  spawnSync(
457
483
  npmBin(),
458
484
  args,
459
- spawnBase({
485
+ npmOpts({
460
486
  cwd,
461
487
  encoding: GIT_ENCODING,
462
488
  maxBuffer: GIT_MAX_BUFFER,
@@ -464,6 +490,15 @@ const runNpm = (cwd, args) =>
464
490
  }),
465
491
  );
466
492
 
493
+ const runNpmAsync = (cwd, args) =>
494
+ runProc(npmBin(), args, {
495
+ cwd,
496
+ encoding: GIT_ENCODING,
497
+ maxBuffer: GIT_MAX_BUFFER,
498
+ timeout: INSTALL_MS,
499
+ shell: npmOpts().shell,
500
+ });
501
+
467
502
  const firstDepRel = (files, kind) => {
468
503
  for (const rel of files) {
469
504
  const meta = depFileMeta(rel);
@@ -472,7 +507,7 @@ const firstDepRel = (files, kind) => {
472
507
  return '';
473
508
  };
474
509
 
475
- const applyProposedUpdate = (top, item) => {
510
+ const proposedNpmPlan = (top, item) => {
476
511
  const change = depChange(item);
477
512
  const origin = item.origin;
478
513
  const files = depPaths(item);
@@ -483,7 +518,7 @@ const applyProposedUpdate = (top, item) => {
483
518
  const picked = lockOnly ? lockRel : manifest;
484
519
  const target = picked || files[0] || '';
485
520
  const meta = depFileMeta(target);
486
- if (!meta) return;
521
+ if (!meta) return null;
487
522
  let saved = '';
488
523
  if (!lockOnly && !removing && manifest) {
489
524
  const worktree = worktreeText(top, manifest);
@@ -503,9 +538,23 @@ const applyProposedUpdate = (top, item) => {
503
538
  let npmArgs = ['i'];
504
539
  if (removing) npmArgs = ['uninstall', change.name];
505
540
  else if (lockOnly) npmArgs = ['audit', 'fix'];
506
- const install = item.dep.install ?? ((dir) => runNpm(dir, npmArgs));
507
- const result = install(cwd);
508
541
  const fail = removing ? 'npm uninstall failed' : 'npm i failed';
542
+ const dirLock = meta.dir === '.' ? LOCKFILE : `${meta.dir}/${LOCKFILE}`;
543
+ const lockPath = lockRel || dirLock;
544
+ return {
545
+ cwd,
546
+ npmArgs,
547
+ saved,
548
+ manifest,
549
+ lockOnly,
550
+ lockPath,
551
+ fail,
552
+ runner: item.dep.install,
553
+ };
554
+ };
555
+
556
+ const finishProposedNpm = (top, plan, result) => {
557
+ const { saved, manifest, lockOnly, lockPath, fail } = plan;
509
558
  if (result && (result.error || result.status !== 0)) {
510
559
  if (saved && manifest) writeWorktreeFile(top, manifest, saved);
511
560
  if (result.error) {
@@ -517,12 +566,26 @@ const applyProposedUpdate = (top, item) => {
517
566
  }
518
567
  const toAdd = [];
519
568
  if (!lockOnly && manifest) toAdd.push(manifest);
520
- const dirLock = meta.dir === '.' ? LOCKFILE : `${meta.dir}/${LOCKFILE}`;
521
- const lockPath = lockRel || dirLock;
522
569
  if (fs.existsSync(path.join(top, lockPath))) toAdd.push(lockPath);
523
570
  if (toAdd.length) requireOk(runGit(['add', '--', ...toAdd], top));
524
571
  };
525
572
 
573
+ const applyProposedUpdate = (top, item) => {
574
+ const plan = proposedNpmPlan(top, item);
575
+ if (!plan) return;
576
+ const install = plan.runner ?? ((dir) => runNpm(dir, plan.npmArgs));
577
+ finishProposedNpm(top, plan, install(plan.cwd));
578
+ };
579
+
580
+ const applyProposedUpdateAsync = async (top, item) => {
581
+ const plan = proposedNpmPlan(top, item);
582
+ if (!plan) return;
583
+ const run = plan.runner
584
+ ? (dir) => Promise.resolve(plan.runner(dir))
585
+ : (dir) => runNpmAsync(dir, plan.npmArgs);
586
+ finishProposedNpm(top, plan, await run(plan.cwd));
587
+ };
588
+
526
589
  const applyDepChange = (top, item, mode) => {
527
590
  const change = depChange(item);
528
591
  if (change && change.propose) {
@@ -579,6 +642,15 @@ const commitDiffArgs = (commit, parent, pathArgs) => {
579
642
 
580
643
  const pathArgsOf = (paths) => (paths.length ? ['--', ...paths] : []);
581
644
 
645
+ const currentBranch = (top) => {
646
+ const args = ['rev-parse', '--abbrev-ref', 'HEAD'];
647
+ const result = runGit(args, top);
648
+ if (result.status !== 0) return '';
649
+ const name = result.stdout.trim();
650
+ if (!name || name === 'HEAD') return '';
651
+ return name;
652
+ };
653
+
582
654
  const snapshotWorktree = (top, stagedOut, unstagedOut, names) => {
583
655
  const stagedDiff = parseDiff(stagedOut);
584
656
  const unstagedDiff = parseDiff(unstagedOut);
@@ -587,14 +659,16 @@ const snapshotWorktree = (top, stagedOut, unstagedOut, names) => {
587
659
  const untracked = untrackedItemsFrom(top, names);
588
660
  const combined = mergeItems([staged, unstaged, untracked]);
589
661
  const parsed = dropReviewItems(combined);
590
- return { top, parsed };
662
+ const branch = currentBranch(top);
663
+ return { top, parsed, branch };
591
664
  };
592
665
 
593
666
  const snapshotCommit = (top, commit, files) => {
594
667
  const commitItems = itemsFromFiles(files, 'commit');
595
668
  const parsed = dropReviewItems(commitItems);
596
669
  const revShort = shortRev(top, commit);
597
- return { top, parsed, rev: commit, revShort };
670
+ const branch = currentBranch(top);
671
+ return { top, parsed, rev: commit, revShort, branch };
598
672
  };
599
673
 
600
674
  const loadCommit = (top, commit, extra) => {
@@ -746,6 +820,16 @@ const addItem = (top, item) => {
746
820
  applyPatch(top, item.patchAdd, ['--cached']);
747
821
  };
748
822
 
823
+ const addItemAsync = async (top, item) => {
824
+ if (item.origin === 'staged' || item.origin === 'commit') return;
825
+ const change = depChange(item);
826
+ if (change && change.propose) {
827
+ await applyProposedUpdateAsync(top, item);
828
+ return;
829
+ }
830
+ addItem(top, item);
831
+ };
832
+
749
833
  const unstageItem = (top, item) => {
750
834
  if (item.origin !== 'staged') return;
751
835
  if (depChange(item)) {
@@ -845,6 +929,203 @@ const revertItem = (top, item) => {
845
929
  }
846
930
  };
847
931
 
932
+ const lastMessage = (top) => {
933
+ const result = runGit(['log', '-1', '--format=%B'], top);
934
+ if (result.status !== 0) return '';
935
+ return (result.stdout ?? '').replace(/\s+$/, '');
936
+ };
937
+
938
+ const parseTrack = (text) => {
939
+ const raw = `${text ?? ''}`.trim().replace(/^\[|\]$/g, '');
940
+ if (!raw) return { ahead: 0, behind: 0, gone: false };
941
+ if (raw === 'gone') return { ahead: 0, behind: 0, gone: true };
942
+ const aheadMatch = TRACK_AHEAD.exec(raw);
943
+ const behindMatch = TRACK_BEHIND.exec(raw);
944
+ const ahead = aheadMatch ? Number(aheadMatch[1]) : 0;
945
+ const behind = behindMatch ? Number(behindMatch[1]) : 0;
946
+ return { ahead, behind, gone: false };
947
+ };
948
+
949
+ const parseBranchLine = (line) => {
950
+ const parts = line.split('\t');
951
+ if (parts.length < 2) return null;
952
+ const name = (parts[1] ?? '').trim();
953
+ if (!name) return null;
954
+ const mark = parts[0] ?? '';
955
+ const sha = (parts[2] ?? '').trim();
956
+ const date = (parts[3] ?? '').trim();
957
+ const track = parseTrack(parts[4]);
958
+ const subject = parts.slice(5).join('\t').trim();
959
+ return {
960
+ name,
961
+ current: mark.includes('*'),
962
+ isDefault: false,
963
+ sha,
964
+ date,
965
+ subject,
966
+ ...track,
967
+ };
968
+ };
969
+
970
+ const originHead = (top) => {
971
+ const args = ['symbolic-ref', '--short', ORIGIN_HEAD];
972
+ const result = runGit(args, top);
973
+ if (result.status !== 0) return '';
974
+ const ref = result.stdout.trim();
975
+ const prefix = 'origin/';
976
+ if (ref.startsWith(prefix)) return ref.slice(prefix.length);
977
+ return ref;
978
+ };
979
+
980
+ const defaultBranch = (top, names) => {
981
+ const remote = originHead(top);
982
+ if (names.has(remote)) return remote;
983
+ for (const name of DEFAULT_BRANCHES) {
984
+ if (names.has(name)) return name;
985
+ }
986
+ return '';
987
+ };
988
+
989
+ const listBranches = (top) => {
990
+ const args = ['for-each-ref', `--format=${BRANCH_FORMAT}`, 'refs/heads'];
991
+ const result = requireOk(runGit(args, top), 'branch failed');
992
+ const branches = [];
993
+ for (const line of result.stdout.split('\n')) {
994
+ if (!line) continue;
995
+ const entry = parseBranchLine(line);
996
+ if (entry) branches.push(entry);
997
+ }
998
+ const names = new Set();
999
+ for (const entry of branches) names.add(entry.name);
1000
+ const primary = defaultBranch(top, names);
1001
+ for (const entry of branches) {
1002
+ entry.isDefault = entry.name === primary;
1003
+ }
1004
+ return branches;
1005
+ };
1006
+
1007
+ const namedBranch = (name) => {
1008
+ const branch = `${name ?? ''}`.trim();
1009
+ if (!branch) throw new Error('empty branch name');
1010
+ return branch;
1011
+ };
1012
+
1013
+ const checkoutBranch = (top, name) => {
1014
+ const branch = namedBranch(name);
1015
+ requireOk(runGit(['checkout', branch], top), 'checkout failed');
1016
+ };
1017
+
1018
+ const checkoutBranchAsync = async (top, name) => {
1019
+ const branch = namedBranch(name);
1020
+ requireOk(await runGitAsync(['checkout', branch], top), 'checkout failed');
1021
+ };
1022
+
1023
+ const createBranch = (top, name) => {
1024
+ const branch = namedBranch(name);
1025
+ const args = ['checkout', '-b', branch];
1026
+ requireOk(runGit(args, top), 'checkout failed');
1027
+ };
1028
+
1029
+ const createBranchAsync = async (top, name) => {
1030
+ const branch = namedBranch(name);
1031
+ const args = ['checkout', '-b', branch];
1032
+ requireOk(await runGitAsync(args, top), 'checkout failed');
1033
+ };
1034
+
1035
+ const pullChanges = (top) => {
1036
+ requireOk(runGit(['pull'], top), 'pull failed');
1037
+ };
1038
+
1039
+ const pullChangesAsync = async (top) => {
1040
+ requireOk(await runGitAsync(['pull'], top), 'pull failed');
1041
+ };
1042
+
1043
+ const isRejectedPush = (msg) => {
1044
+ const text = `${msg ?? ''}`.toLowerCase();
1045
+ for (const token of PUSH_REJECTED) {
1046
+ if (text.includes(token)) return true;
1047
+ }
1048
+ return false;
1049
+ };
1050
+
1051
+ const pushError = (msg) => {
1052
+ const text = `${msg ?? ''}`;
1053
+ const rejected = isRejectedPush(text);
1054
+ const error = new Error(rejected ? 'push rejected' : oneLine(text));
1055
+ error.rejected = rejected;
1056
+ return error;
1057
+ };
1058
+
1059
+ const pushCmd = (force) => {
1060
+ if (force) return ['push', '--force-with-lease'];
1061
+ return ['push'];
1062
+ };
1063
+
1064
+ const pushUpstreamCmd = (force) => {
1065
+ if (force) return ['push', '--force-with-lease', '-u', 'origin', 'HEAD'];
1066
+ return ['push', '-u', 'origin', 'HEAD'];
1067
+ };
1068
+
1069
+ const finishPush = (result, force, run) => {
1070
+ if (result.status === 0) return;
1071
+ const msg = result.stderr || result.stdout || 'push failed';
1072
+ if (!msg.toLowerCase().includes('no upstream')) {
1073
+ throw pushError(msg);
1074
+ }
1075
+ const next = run(pushUpstreamCmd(force));
1076
+ if (next.status !== 0) {
1077
+ throw pushError(next.stderr || next.stdout || 'push failed');
1078
+ }
1079
+ };
1080
+
1081
+ const pushChanges = (top, force) => {
1082
+ const useForce = force === true;
1083
+ const result = runGit(pushCmd(useForce), top);
1084
+ finishPush(result, useForce, (args) => runGit(args, top));
1085
+ };
1086
+
1087
+ const pushChangesAsync = async (top, force) => {
1088
+ const useForce = force === true;
1089
+ const result = await runGitAsync(pushCmd(useForce), top);
1090
+ if (result.status === 0) return;
1091
+ const msg = result.stderr || result.stdout || 'push failed';
1092
+ if (!msg.toLowerCase().includes('no upstream')) {
1093
+ throw pushError(msg);
1094
+ }
1095
+ const next = await runGitAsync(pushUpstreamCmd(useForce), top);
1096
+ if (next.status !== 0) {
1097
+ throw pushError(next.stderr || next.stdout || 'push failed');
1098
+ }
1099
+ };
1100
+
1101
+ const hasStaged = (top) => {
1102
+ const result = runGit(['diff', '--cached', '--quiet'], top);
1103
+ return result.status === 1;
1104
+ };
1105
+
1106
+ const commitPlan = (kind, message) => {
1107
+ if (kind === 'fixup') {
1108
+ const target = (message ?? '').trim() || 'HEAD';
1109
+ return { args: ['commit', `--fixup=${target}`], body: undefined };
1110
+ }
1111
+ const args = ['commit'];
1112
+ if (kind === 'amend') args.push('--amend');
1113
+ args.push('-F', '-');
1114
+ const text = message ?? '';
1115
+ const body = text.endsWith('\n') ? text : `${text}\n`;
1116
+ return { args, body };
1117
+ };
1118
+
1119
+ const commitChanges = (top, kind, message) => {
1120
+ const plan = commitPlan(kind, message);
1121
+ requireOk(runGit(plan.args, top, plan.body));
1122
+ };
1123
+
1124
+ const commitChangesAsync = async (top, kind, message) => {
1125
+ const plan = commitPlan(kind, message);
1126
+ requireOk(await runGitAsync(plan.args, top, { input: plan.body }));
1127
+ };
1128
+
848
1129
  const revertFile = (top, rel, items) => {
849
1130
  const paths = new Set([rel]);
850
1131
  for (const item of items) {
@@ -864,9 +1145,24 @@ const createGitRepo = () => ({
864
1145
  toplevel,
865
1146
  resolveRev,
866
1147
  add: addItem,
1148
+ addAsync: addItemAsync,
867
1149
  unstage: unstageItem,
868
1150
  revert: revertItem,
869
1151
  revertFile,
1152
+ commit: commitChanges,
1153
+ commitAsync: commitChangesAsync,
1154
+ hasStaged,
1155
+ lastMessage,
1156
+ listBranches,
1157
+ currentBranch,
1158
+ checkout: checkoutBranch,
1159
+ checkoutAsync: checkoutBranchAsync,
1160
+ createBranch,
1161
+ createBranchAsync,
1162
+ pull: pullChanges,
1163
+ pullAsync: pullChangesAsync,
1164
+ push: pushChanges,
1165
+ pushAsync: pushChangesAsync,
870
1166
  });
871
1167
 
872
1168
  module.exports = {
@@ -876,5 +1172,19 @@ module.exports = {
876
1172
  addItem,
877
1173
  unstageItem,
878
1174
  revertItem,
1175
+ commitChanges,
1176
+ commitChangesAsync,
1177
+ hasStaged,
1178
+ currentBranch,
1179
+ lastMessage,
1180
+ listBranches,
1181
+ checkoutBranch,
1182
+ checkoutBranchAsync,
1183
+ createBranch,
1184
+ createBranchAsync,
1185
+ pullChanges,
1186
+ pullChangesAsync,
1187
+ pushChanges,
1188
+ pushChangesAsync,
879
1189
  createGitRepo,
880
1190
  };
@@ -0,0 +1,228 @@
1
+ 'use strict';
2
+
3
+ const { SQUOTE, emit, wordStyle, readUntilNl } = require('./core.js');
4
+
5
+ const langs = ['bash'];
6
+
7
+ const aliases = {
8
+ sh: 'bash',
9
+ shell: 'bash',
10
+ zsh: 'bash',
11
+ };
12
+
13
+ const BASH_QUOTE = `${SQUOTE}\``;
14
+
15
+ const KEYWORDS = [
16
+ 'if',
17
+ 'then',
18
+ 'else',
19
+ 'elif',
20
+ 'fi',
21
+ 'for',
22
+ 'while',
23
+ 'until',
24
+ 'do',
25
+ 'done',
26
+ 'case',
27
+ 'esac',
28
+ 'function',
29
+ 'select',
30
+ 'in',
31
+ 'time',
32
+ 'coproc',
33
+ ];
34
+
35
+ const BUILTINS = [
36
+ 'echo',
37
+ 'printf',
38
+ 'cd',
39
+ 'pwd',
40
+ 'exit',
41
+ 'export',
42
+ 'source',
43
+ 'alias',
44
+ 'unalias',
45
+ 'local',
46
+ 'declare',
47
+ 'typeset',
48
+ 'readonly',
49
+ 'unset',
50
+ 'shift',
51
+ 'read',
52
+ 'test',
53
+ 'true',
54
+ 'false',
55
+ 'exec',
56
+ 'eval',
57
+ 'set',
58
+ 'trap',
59
+ 'wait',
60
+ 'kill',
61
+ 'type',
62
+ 'command',
63
+ 'builtin',
64
+ 'return',
65
+ 'break',
66
+ 'continue',
67
+ 'pushd',
68
+ 'popd',
69
+ 'dirs',
70
+ 'getopts',
71
+ 'mapfile',
72
+ 'readarray',
73
+ 'let',
74
+ 'umask',
75
+ 'ulimit',
76
+ 'fg',
77
+ 'bg',
78
+ 'jobs',
79
+ 'hash',
80
+ 'help',
81
+ 'enable',
82
+ 'caller',
83
+ 'bind',
84
+ 'compgen',
85
+ 'complete',
86
+ ];
87
+
88
+ const WORD_STYLE = {
89
+ ...wordStyle(KEYWORDS, 'control'),
90
+ ...wordStyle(BUILTINS, 'function'),
91
+ };
92
+
93
+ const readBashVar = (source, i) => {
94
+ if (source[i + 1] === '{') {
95
+ let j = i + 2;
96
+ while (j < source.length && source[j] !== '}') j += 1;
97
+ if (j < source.length) j += 1;
98
+ return { text: source.slice(i, j), end: j };
99
+ }
100
+ if (/[0-9#?$!*@-]/.test(source[i + 1] || '')) {
101
+ return { text: source.slice(i, i + 2), end: i + 2 };
102
+ }
103
+ let j = i + 1;
104
+ while (j < source.length && /[A-Za-z0-9_]/.test(source[j])) j += 1;
105
+ if (j === i + 1) return { text: '$', end: i + 1 };
106
+ return { text: source.slice(i, j), end: j };
107
+ };
108
+
109
+ const takeQuoted = (source, i) => {
110
+ const q = source[i];
111
+ let j = i + 1;
112
+ const n = source.length;
113
+ while (j < n && source[j] !== q) j += 1;
114
+ if (j < n) j += 1;
115
+ return { text: source.slice(i, j), end: j };
116
+ };
117
+
118
+ const takeBashDouble = (source, i, out) => {
119
+ let j = i + 1;
120
+ let chunk = '"';
121
+ const n = source.length;
122
+ while (j < n && source[j] !== '"') {
123
+ if (source[j] === '\\' && j + 1 < n) {
124
+ chunk += source.slice(j, j + 2);
125
+ j += 2;
126
+ continue;
127
+ }
128
+ if (source[j] === '$') {
129
+ emit(out, 'string', chunk);
130
+ chunk = '';
131
+ const { text, end } = readBashVar(source, j);
132
+ emit(out, 'variable', text);
133
+ j = end;
134
+ continue;
135
+ }
136
+ chunk += source[j];
137
+ j += 1;
138
+ }
139
+ if (j < n) {
140
+ chunk += '"';
141
+ j += 1;
142
+ }
143
+ if (chunk) emit(out, 'string', chunk);
144
+ return j;
145
+ };
146
+
147
+ const bashWordStyle = (word, nextCh) => {
148
+ const known = WORD_STYLE[word];
149
+ if (known) return known;
150
+ if (nextCh === '=') return 'variable';
151
+ return 'plain';
152
+ };
153
+
154
+ const bashOpLen = (ch, next) => ('|&><'.includes(ch) && next === ch ? 2 : 1);
155
+
156
+ const highlight = (source) => {
157
+ const out = [];
158
+ let i = 0;
159
+ const n = source.length;
160
+ while (i < n) {
161
+ const ch = source[i];
162
+ const next = source[i + 1];
163
+
164
+ if (ch === '#' && (i === 0 || /\s/.test(source[i - 1]))) {
165
+ const { text, end } = readUntilNl(source, i);
166
+ emit(out, 'comment', text);
167
+ i = end;
168
+ continue;
169
+ }
170
+
171
+ if (BASH_QUOTE.includes(ch)) {
172
+ const { text, end } = takeQuoted(source, i);
173
+ emit(out, 'string', text);
174
+ i = end;
175
+ continue;
176
+ }
177
+
178
+ if (ch === '"') {
179
+ i = takeBashDouble(source, i, out);
180
+ continue;
181
+ }
182
+
183
+ if (ch === '$') {
184
+ const { text, end } = readBashVar(source, i);
185
+ emit(out, 'variable', text);
186
+ i = end;
187
+ continue;
188
+ }
189
+
190
+ if (ch === '-' && /[A-Za-z0-9]/.test(next || '')) {
191
+ let j = i + 1;
192
+ while (j < n && /[A-Za-z0-9_-]/.test(source[j])) j += 1;
193
+ emit(out, 'attr', source.slice(i, j));
194
+ i = j;
195
+ continue;
196
+ }
197
+
198
+ if (/[A-Za-z_]/.test(ch)) {
199
+ let j = i + 1;
200
+ while (j < n && /[A-Za-z0-9_]/.test(source[j])) j += 1;
201
+ const word = source.slice(i, j);
202
+ emit(out, bashWordStyle(word, source[j]), word);
203
+ i = j;
204
+ continue;
205
+ }
206
+
207
+ if (/[0-9]/.test(ch)) {
208
+ let j = i + 1;
209
+ while (j < n && /[0-9]/.test(source[j])) j += 1;
210
+ emit(out, 'number', source.slice(i, j));
211
+ i = j;
212
+ continue;
213
+ }
214
+
215
+ if ('|&;<>(){}'.includes(ch)) {
216
+ const j = i + bashOpLen(ch, next);
217
+ emit(out, 'operator', source.slice(i, j));
218
+ i = j;
219
+ continue;
220
+ }
221
+
222
+ emit(out, 'plain', ch);
223
+ i += 1;
224
+ }
225
+ return out;
226
+ };
227
+
228
+ module.exports = { langs, aliases, highlight };