forked from jonlabelle/SublimeJsPrettier
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathJsPrettier.py
777 lines (662 loc) · 26.5 KB
/
JsPrettier.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
# -*- coding: utf-8 -*-
import os
import platform
from re import sub
from subprocess import PIPE
from subprocess import Popen
import sublime
import sublime_plugin
#
# Monkey patch `sublime.Region` so it can be iterable:
sublime.Region.totuple = lambda self: (self.a, self.b)
sublime.Region.__iter__ = lambda self: self.totuple().__iter__()
PLUGIN_NAME = 'JsPrettier'
PLUGIN_PATH = os.path.join(sublime.packages_path(),
os.path.dirname(os.path.realpath(__file__)))
PLUGIN_CMD_NAME = 'js_prettier'
PROJECT_SETTINGS_KEY = PLUGIN_CMD_NAME
SETTINGS_FILE = '{0}.sublime-settings'.format(PLUGIN_NAME)
PRETTIER_OPTIONS_KEY = 'prettier_options'
PRETTIER_OPTION_CLI_MAP = [
{
'option': 'printWidth',
'cli': '--print-width',
'default': '80'
},
{
'option': 'singleQuote',
'cli': '--single-quote',
'default': 'false'
},
{
'option': 'trailingComma',
'cli': '--trailing-comma',
'default': 'none'
},
{
'option': 'bracketSpacing',
'cli': '--bracket-spacing',
'default': 'true'
},
{
'option': 'jsxBracketSameLine',
'cli': '--jsx-bracket-same-line',
'default': 'false'
},
{
'option': 'parser',
'cli': '--parser',
'default': 'babylon'
},
{
'option': 'semi',
'cli': '--semi',
'default': 'true'
}
]
ALLOWED_FILE_EXTENSIONS = [
'js',
'jsx',
'ts',
'tsx',
'css',
'scss',
'less'
]
IS_SUBLIME_TEXT_LATEST = int(sublime.version()) >= 3000
class JsPrettierCommand(sublime_plugin.TextCommand):
_error_message = None
@property
def debug(self):
return self.get_setting('debug', False)
@property
def has_error(self):
if not self._error_message:
return False
return True
@property
def error_message(self):
return self._error_message
@error_message.setter
def error_message(self, message=None):
self._error_message = message
@property
def proc_env(self):
env = None
if not self.is_windows():
env = os.environ.copy()
usr_path = ':/usr/local/bin'
if not self.env_path_exists(usr_path) \
and self.path_exists(usr_path):
env['PATH'] += usr_path
return env
@property
def prettier_cli_path(self):
"""The prettier cli path.
When the `prettier_cli_path` setting is empty (""),
the path is resolved by searching locations in the following order,
returning the first match of the prettier cli path...
- Locally installed prettier, relative to a Sublime Text Project
file's root directory, e.g.: `node_modules/.bin/prettier'.
- User's $HOME/node_modules directory.
- Look in the JsPrettier Sublime Text plug-in directory for
`node_modules/.bin/prettier`.
- Finally, check if prettier is installed globally,
e.g.: `yarn global add prettier`
or: `npm install -g prettier`
:return: The prettier cli path.
"""
user_prettier_path = self.get_setting('prettier_cli_path', '')
project_path = self.get_active_project_path()
if self.is_str_none_or_empty(user_prettier_path):
global_prettier_path = self.which('prettier')
project_prettier_path = os.path.join(
project_path, 'node_modules', '.bin', 'prettier')
plugin_prettier_path = os.path.join(
PLUGIN_PATH, 'node_modules', '.bin', 'prettier')
if os.path.exists(project_prettier_path):
return project_prettier_path
if os.path.exists(plugin_prettier_path):
return plugin_prettier_path
return global_prettier_path
# handle cases when the user specifies a prettier cli path that is
# relative to the working file or project:
if not os.path.isabs(user_prettier_path):
user_prettier_path = os.path.join(project_path, user_prettier_path)
return user_prettier_path
@property
def node_path(self):
return self.get_setting('node_path', None)
@property
def tab_size(self):
return int(self.view.settings().get('tab_size', 2))
@property
def use_tabs(self):
translate_tabs_to_spaces = self.view.settings().get(
'translate_tabs_to_spaces', True)
if not translate_tabs_to_spaces:
return True
return False
@property
def allow_inline_formatting(self):
return self.get_setting('allow_inline_formatting', False)
@property
def additional_cli_args(self):
return self.get_setting('additional_cli_args', {})
@property
def max_file_size_limit(self):
return int(self.get_setting('max_file_size_limit', -1))
def exceeds_max_file_size_limit(self, view):
if self.max_file_size_limit == -1:
return False
if os.path.getsize(view.file_name()) > self.max_file_size_limit:
return True
return False
def is_allowed_file_ext(self, view):
filename = view.file_name()
if not filename:
return False
file_ext = os.path.splitext(filename)[1][1:]
if file_ext in ALLOWED_FILE_EXTENSIONS:
return True
if file_ext in set(self.get_setting('custom_file_extensions', [])):
return True
return False
def run(self, edit, force_entire_file=False):
view = self.view
if view.file_name() is None:
#
# Handle file must first be saved:
if not IS_SUBLIME_TEXT_LATEST:
# sublime text 2x: limited dialog support, just show error:
return sublime.error_message(
'{0} Error\n\n'
'File must first be saved.'.format(PLUGIN_NAME))
else:
#
# sublime text 3+: show dialog that includes a save option:
result = sublime.yes_no_cancel_dialog(
'{0}\n\n'
'File must first be Saved.'.format(PLUGIN_NAME),
'Save...', "Don't Save")
if result == sublime.DIALOG_YES:
view.run_command('save')
#
# re-check if the file was saved here, incase user canceled or closed
# the save dialog:
if view.file_name() is None:
return sublime.set_timeout(lambda: sublime.status_message(
'{0}: File save canceled.'.format(PLUGIN_NAME)), 0)
prettier_cli_path = self.prettier_cli_path
if prettier_cli_path is None:
return sublime.error_message(
'{0} Error\n\n'
'The path to the Prettier cli executable could '
'not be found! Please ensure the path to prettier is '
'set in your PATH environment variable.'.format(PLUGIN_NAME))
if self.exceeds_max_file_size_limit(view):
return sublime.set_timeout(lambda: sublime.status_message(
'{0}: File ignored, max allowed file size '
'limit reached.'.format(PLUGIN_NAME)), 0)
prettier_args = self.parse_prettier_options(view)
node_path = self.node_path
# Format entire file:
if not self.has_selection(view) or force_entire_file is True:
region = sublime.Region(0, view.size())
source = view.substr(region)
if self.is_str_empty_or_whitespace_only(source):
return sublime.set_timeout(lambda: sublime.status_message(
'{0}: Nothing to format in file.'.format(PLUGIN_NAME)), 0)
transformed = self._exec_cmd(
source, node_path, prettier_cli_path, prettier_args)
if self.has_error:
self.show_console_error()
return self.show_status_bar_error()
# sanity check to ensure textual content was returned from cmd
# stdout, not necessarily caught in OSError try/catch
# exception handler
if self.is_str_empty_or_whitespace_only(transformed):
self.error_message = 'Empty content returned to stdout'
return self.show_status_bar_error()
file_changed = False
transformed = self.trim_trailing_ws_and_lines(transformed)
if transformed:
if transformed == self.trim_trailing_ws_and_lines(source):
if self.ensure_newline_at_eof(view, edit) is True:
# no formatting changes applied, however, a line
# break was needed/inserted at the end of the file:
file_changed = True
else:
view.replace(edit, region, transformed)
self.ensure_newline_at_eof(view, edit)
file_changed = True
else:
view.replace(edit, region, transformed)
self.ensure_newline_at_eof(view, edit)
file_changed = True
if file_changed is True:
sublime.set_timeout(lambda: sublime.status_message(
'{0}: File formatted.'.format(PLUGIN_NAME)), 0)
else:
sublime.set_timeout(lambda: sublime.status_message(
'{0}: File already formatted.'.format(PLUGIN_NAME)), 0)
return
# Format each selection:
for region in view.sel():
if region.empty():
continue
source = view.substr(region)
if self.is_str_empty_or_whitespace_only(source):
sublime.set_timeout(lambda: sublime.status_message(
'{0}: Nothing to format in selection.'.format(
PLUGIN_NAME)), 0)
continue
transformed = self._exec_cmd(
source, node_path, prettier_cli_path, prettier_args)
if self.has_error:
self.show_console_error()
return self.show_status_bar_error()
# sanity check to ensure textual content was returned from cmd
# stdout, not necessarily caught in OSError try/catch
# exception handler
if self.is_str_empty_or_whitespace_only(transformed):
self.error_message = 'Empty content returned to stdout'
return self.show_status_bar_error()
transformed = self.trim_trailing_ws_and_lines(transformed)
if transformed \
and transformed == self.trim_trailing_ws_and_lines(source):
sublime.set_timeout(lambda: sublime.status_message(
'{0}: Selection(s) already formatted.'.format(
PLUGIN_NAME)), 0)
else:
view.replace(edit, region, transformed)
sublime.set_timeout(lambda: sublime.status_message(
'{0}: Selection(s) formatted.'.format(PLUGIN_NAME)), 0)
def _exec_cmd(self, source, node_path, prettier_cli_path,
prettier_args):
self._error_message = None
if self.is_str_none_or_empty(node_path):
cmd = [prettier_cli_path] \
+ ['--stdin'] \
+ prettier_args
else:
cmd = [node_path] \
+ [prettier_cli_path] \
+ ['--stdin'] \
+ prettier_args
try:
self.show_debug_message(
'Prettier CLI Command', self.list_to_str(cmd))
proc = Popen(
cmd, stdin=PIPE,
stderr=PIPE,
stdout=PIPE,
env=self.proc_env,
shell=self.is_windows())
stdout, stderr = proc.communicate(input=source.encode('utf-8'))
if stderr or proc.returncode != 0:
self.format_error_message(
stderr.decode('utf-8'), str(proc.returncode))
return None
return stdout.decode('utf-8')
except OSError as ex:
sublime.error_message('{0} - {1}'.format(PLUGIN_NAME, ex))
raise
def should_show_plugin(self):
view = self.view
if self.allow_inline_formatting is True:
return True
if self.is_source_js(view) is True:
return True
if self.is_css(view) is True:
return True
if self.is_allowed_file_ext(view) is True:
return True
return False
def is_visible(self):
return self.should_show_plugin()
def is_enabled(self):
return self.should_show_plugin()
def get_setting(self, key, default_value=None):
settings = self.view.settings().get(PLUGIN_NAME)
if settings is None or settings.get(key) is None:
settings = sublime.load_settings(SETTINGS_FILE)
value = settings.get(key, default_value)
# check for project-level overrides:
project_value = self._get_project_setting(key)
if project_value is None:
return value
return project_value
def get_sub_setting(self, key=None):
settings = self.view.settings().get(PLUGIN_NAME)
if settings is None or settings.get(PRETTIER_OPTIONS_KEY).get(
key) is None:
settings = sublime.load_settings(SETTINGS_FILE)
value = settings.get(PRETTIER_OPTIONS_KEY).get(key)
# check for project-level overrides:
project_value = self._get_project_sub_setting(key)
if project_value is None:
return value
return project_value
def parse_prettier_options(self, view):
# TODO: optimize option parsing...
prettier_cli_args = []
is_css = self.is_css(view)
is_typescript = self.is_typescript(view)
for mapping in PRETTIER_OPTION_CLI_MAP:
option_name = mapping['option']
cli_option_name = mapping['cli']
option_value = self.get_sub_setting(option_name)
# internally override the 'parser' option for css
# and set the value to 'postcss':
if option_name == 'parser' and is_css:
prettier_cli_args.append(cli_option_name)
prettier_cli_args.append('postcss')
continue
# internally override the 'parser' for typescript
# and set the value to 'typescript':
if option_name == 'parser' and is_typescript:
prettier_cli_args.append(cli_option_name)
prettier_cli_args.append('typescript')
continue
if option_value is None or str(option_value) == '':
option_value = mapping['default']
option_value = str(option_value).strip()
if self.is_bool_str(option_value):
prettier_cli_args.append('{0}={1}'.format(
cli_option_name, option_value.lower()))
else:
prettier_cli_args.append(cli_option_name)
prettier_cli_args.append(option_value)
# set the `tabWidth` option based on the current view:
prettier_cli_args.append('--tab-width')
prettier_cli_args.append(str(self.tab_size))
# set the `useTabs` option based on the current view:
prettier_cli_args.append('{0}={1}'.format(
'--use-tabs', str(self.use_tabs).lower()))
# add the additional arguments from the settings file to the command:
if self.additional_cli_args and len(self.additional_cli_args) > 0:
for arg_key, arg_value in self.additional_cli_args.items():
arg_key = str(arg_key).strip()
arg_value = str(arg_value).strip()
# handle bool options
if arg_value != '' and self.is_bool_str(arg_value):
prettier_cli_args.append(
'{0}={1}'.format(arg_key, arg_value.lower()))
continue
prettier_cli_args.append(arg_key)
if arg_value != '':
prettier_cli_args.append(arg_value)
return prettier_cli_args
def which(self, executable, path=None):
if not self.is_str_none_or_empty(executable):
if os.path.isfile(executable):
return executable
if self.is_str_none_or_empty(path):
path = os.environ['PATH']
if not self.is_windows():
usr_path = ':/usr/local/bin'
if not self.env_path_exists(usr_path, path) \
and self.path_exists(usr_path):
path += usr_path
paths = path.split(os.pathsep)
if not os.path.isfile(executable):
for directory in paths:
exec_path = os.path.join(directory, executable)
if os.path.isfile(exec_path):
return exec_path
return None
return executable
def show_debug_message(self, label, message):
if not self.debug:
return
header = ' {0} DEBUG - {1} '.format(PLUGIN_NAME, label)
horizontal_rule = self.repeat_str('-', len(header))
print('\n{0}\n{1}\n{2}\n\n''{3}'.format(
horizontal_rule, header, horizontal_rule, message))
def show_console_error(self):
print('\n------------------\n {0} ERROR \n------------------\n\n'
'{1}'.format(PLUGIN_NAME, self.error_message))
def format_error_message(self, error_message, error_code):
self.error_message = 'Prettier reported the following ' \
'error:\n\n{0}\n' \
'Process finished with exit code {1}\n'\
.format(error_message, '{0}'
.format(error_code))
@staticmethod
def is_source_js(view):
return view.scope_name(0).startswith('source.js')
@staticmethod
def is_css(view):
filename = view.file_name()
if not filename:
return False
scopename = view.scope_name(0)
if scopename.startswith('source.css') or filename.endswith('.css'):
return True
if scopename.startswith('source.scss') or filename.endswith('.scss'):
return True
if scopename.startswith('source.less') or filename.endswith('.less'):
return True
return False
@staticmethod
def is_typescript(view):
filename = view.file_name()
if not filename:
return False
scopename = view.scope_name(0)
if scopename.startswith('source.ts') or filename.endswith('.ts'):
return True
if scopename.startswith('source.tsx') or filename.endswith('.tsx'):
return True
return False
@staticmethod
def get_active_project_path():
"""Get the active Sublime Text project path.
Original: https://gist.github.com/astronaughts/9678368
:return: The active Sublime Text project path.
"""
window = sublime.active_window()
folders = window.folders()
if len(folders) == 1:
return folders[0]
else:
active_view = window.active_view()
if active_view:
active_file_name = active_view.file_name()
else:
active_file_name = None
if not active_file_name:
return folders[0] if len(folders) else os.path.expanduser('~')
for folder in folders:
if active_file_name.startswith(folder):
return folder
return os.path.dirname(active_file_name)
@staticmethod
def show_status_bar_error():
sublime.set_timeout(lambda: sublime.status_message(
'{0}: Format failed! Open the console window to '
'view error details.'.format(PLUGIN_NAME)), 0)
@staticmethod
def _get_project_setting(key):
"""Get a project setting.
JsPrettier project settings are stored in the sublime project file
as a dictionary, e.g.:
"settings":
{
"js_prettier": { "key": "value", ... }
}
:param key: The project setting key.
:return: The project setting value.
:rtype: str
"""
project_settings = sublime.active_window().active_view().settings()
if not project_settings:
return None
js_prettier_settings = project_settings.get(PROJECT_SETTINGS_KEY)
if js_prettier_settings:
if key in js_prettier_settings:
return js_prettier_settings[key]
return None
@staticmethod
def _get_project_sub_setting(option):
project_settings = sublime.active_window().active_view().settings()
js_prettier_settings = project_settings.get(PROJECT_SETTINGS_KEY, None)
if not js_prettier_settings:
return None
prettier_options = js_prettier_settings.get(PRETTIER_OPTIONS_KEY, None)
if prettier_options:
if option in prettier_options:
return prettier_options.get(option, None)
return None
@staticmethod
def is_bool_str(val):
"""Determine if the specified string :val is 'true' or 'false'.
:param val: The value to check.
:return: True if if val: is a boolean string, otherwise False.
:rtype: bool
"""
if val is None:
return False
if type(val) == str:
val = val.lower().strip()
if val == 'true' or val == 'false':
return True
return False
@staticmethod
def is_str_none_or_empty(val):
"""Determine if the specified str val is None or an empty.
:param val: The str to check.
:return: True if if val: is None or an empty, otherwise False.
:rtype: bool
"""
if val is None:
return True
if type(val) == str:
val = val.strip()
if not val:
return True
return False
@staticmethod
def is_str_empty_or_whitespace_only(txt):
if not txt or len(txt) == 0:
return True
# strip all whitespace/invisible chars to determine textual content:
txt = sub(r'\s+', '', txt)
if not txt or len(txt) == 0:
return True
return False
@staticmethod
def list_to_str(list_to_convert):
"""Convert a list of values into string.
Each value will be seperated by a single space.
:param list_to_convert: The list to convert to a string.
:return: The list converted into a string.
"""
return ' '.join(str(l) for l in list_to_convert)
@staticmethod
def repeat_str(str_to_repeat, repeat_length):
"""Repeat a string to a certain length.
:param str_to_repeat: The string to repeat. Normally a single char.
:param repeat_length: The amount of times to repeat the string.
:return: The repeated string.
"""
quotient, remainder = divmod(repeat_length, len(str_to_repeat))
return str_to_repeat * quotient + str_to_repeat[:remainder]
@staticmethod
def trim_trailing_ws_and_lines(val):
"""Trim trailing whitespace and line-breaks at the end of a string.
:param val: The value to trim.
:return: The val with trailing whitespace and line-breaks removed.
"""
if val is None:
return val
val = sub(r'\s+\Z', '', val)
return val
@staticmethod
def ensure_newline_at_eof(view, edit):
new_line_inserted = False
if view.size() > 0 and view.substr(view.size() - 1) != '\n':
new_line_inserted = True
view.insert(edit, view.size(), '\n')
return new_line_inserted
@staticmethod
def has_selection(view):
for sel in view.sel():
start, end = sel
if start != end:
return True
return False
@staticmethod
def env_path_exists(find_path, env_path=None):
"""Check if the specified path is listed in OS enviornment path.
:param find_path: The path the search for.
:param env_path: The environment path str.
:return: True if the find_path exists in the env_path.
:rtype: bool
"""
if not find_path:
return False
if not env_path:
env_path = os.environ['PATH']
find_path = str.replace(find_path, os.pathsep, '')
paths = env_path.split(os.pathsep)
for path in paths:
if path == find_path:
return True
return False
@staticmethod
def path_exists(path):
if not path:
return False
if os.path.exists(str.replace(path, os.pathsep, '')):
return True
return False
@staticmethod
def is_mac_os():
return platform.system() == 'Darwin'
@staticmethod
def is_windows():
return platform.system() == 'Windows' or os.name == 'nt'
class CommandOnSave(sublime_plugin.EventListener):
def on_pre_save(self, view):
if self.is_allowed(view) and self.is_enabled(view):
view.run_command(PLUGIN_CMD_NAME, {'force_entire_file': True})
def auto_format_on_save(self, view):
return self.get_setting(view, 'auto_format_on_save', False)
def custom_file_extensions(self, view):
return self.get_setting(view, 'custom_file_extensions', [])
def is_allowed(self, view):
return self.is_allowed_file_ext(view)
def is_enabled(self, view):
return self.auto_format_on_save(view)
def is_allowed_file_ext(self, view):
filename = view.file_name()
if not filename:
return False
file_ext = os.path.splitext(filename)[1][1:]
if file_ext in ALLOWED_FILE_EXTENSIONS:
return True
if file_ext in set(self.custom_file_extensions(view)):
return True
return False
def get_setting(self, view, key, default_value=None):
settings = view.settings().get(PLUGIN_NAME)
if settings is None or settings.get(key) is None:
settings = sublime.load_settings(SETTINGS_FILE)
value = settings.get(key, default_value)
# check for project-level overrides:
project_value = self._get_project_setting(key)
if project_value is None:
return value
return project_value
@staticmethod
def _get_project_setting(key):
settings = sublime.active_window().active_view().settings()
if not settings:
return None
jsprettier = settings.get(PROJECT_SETTINGS_KEY)
if jsprettier:
if key in jsprettier:
return jsprettier[key]
return None