@@ -891,6 +891,10 @@ def bind(cls):
891891 cls .ellipsis <<= attach (cls .ellipsis_tokens , cls .method ("ellipsis_handle" ))
892892 cls .f_string <<= attach (cls .f_string_tokens , cls .method ("f_string_handle" ))
893893 cls .t_string <<= attach (cls .t_string_tokens , cls .method ("t_string_handle" ))
894+ cls .d_string <<= attach (cls .d_string_tokens , cls .method ("d_string_handle" ))
895+ cls .db_string <<= attach (cls .db_string_tokens , cls .method ("db_string_handle" ))
896+ cls .df_string <<= attach (cls .df_string_tokens , cls .method ("df_string_handle" ))
897+ cls .dt_string <<= attach (cls .dt_string_tokens , cls .method ("dt_string_handle" ))
894898 cls .funcname_typeparams <<= attach (cls .funcname_typeparams_tokens , cls .method ("funcname_typeparams_handle" ))
895899
896900 # standard handlers of the form name <<= attach(name_ref, method("name_handle"))
@@ -4778,6 +4782,187 @@ def t_string_handle(self, original, loc, tokens):
47784782 """Process Python 3.14 template strings."""
47794783 return self .f_string_handle (original , loc , tokens , is_t = True )
47804784
4785+ @staticmethod
4786+ def _d_string_dedent (text , loc ):
4787+ """Apply PEP 822 dedentation to string contents.
4788+ The text must start with a newline (the required newline after opening quotes)."""
4789+ if not text .startswith ("\n " ):
4790+ raise CoconutDeferredSyntaxError ("d-string contents must start with a newline after opening quotes" , loc )
4791+ text = text [1 :] # remove leading newline (not included in result)
4792+
4793+ lines = text .split ("\n " )
4794+
4795+ # determine common indentation
4796+ # blank lines are ignored except the last line (closing quotes line)
4797+ indent = None
4798+ for i , line in enumerate (lines ):
4799+ is_last = (i == len (lines ) - 1 )
4800+ if not is_last and line .strip () == "" :
4801+ continue
4802+ stripped = line .lstrip ()
4803+ line_indent = line [:len (line ) - len (stripped )]
4804+ if indent is None :
4805+ indent = line_indent
4806+ else :
4807+ common = ""
4808+ for a , b in zip (indent , line_indent ):
4809+ if a == b :
4810+ common += a
4811+ else :
4812+ break
4813+ indent = common
4814+
4815+ if indent is None :
4816+ indent = ""
4817+
4818+ # apply dedentation
4819+ result_lines = []
4820+ for i , line in enumerate (lines ):
4821+ is_last = (i == len (lines ) - 1 )
4822+ if line .strip () == "" and not is_last :
4823+ result_lines .append ("" )
4824+ elif line .startswith (indent ):
4825+ result_lines .append (line [len (indent ):])
4826+ elif indent .startswith (line ) and line .strip () == "" :
4827+ result_lines .append ("" )
4828+ else :
4829+ raise CoconutDeferredSyntaxError ("inconsistent indentation in d-string" , loc )
4830+
4831+ return "\n " .join (result_lines )
4832+
4833+ def d_string_handle (self , original , loc , tokens ):
4834+ """Process PEP 822 d-strings (dedented strings)."""
4835+ string , = tokens
4836+
4837+ # strip raw r
4838+ raw = string .startswith ("r" )
4839+ if raw :
4840+ string = string [1 :]
4841+
4842+ # unwrap string ref
4843+ internal_assert (string .startswith (strwrapper ) and string .endswith (unwrapper ), "invalid d string item" , string )
4844+ text , strchar = self .get_ref ("str" , string [1 :- 1 ])
4845+
4846+ # must be triple-quoted
4847+ if len (strchar ) == 1 :
4848+ raise CoconutDeferredSyntaxError ("d-string prefix requires triple-quoted string" , loc )
4849+
4850+ # apply dedentation
4851+ text = self ._d_string_dedent (text , loc )
4852+
4853+ return ("r" if raw else "" ) + self .wrap_str (text , strchar [0 ], multiline = True )
4854+
4855+ def db_string_handle (self , original , loc , tokens ):
4856+ """Process d-string with b prefix."""
4857+ string , = tokens
4858+
4859+ # strip raw r and b prefix
4860+ raw = False
4861+ if string .startswith ("r" ):
4862+ raw = True
4863+ string = string [1 :]
4864+ has_b = string .startswith ("b" ) or string .startswith ("B" )
4865+ if has_b :
4866+ string = string [1 :]
4867+ if string .startswith ("r" ):
4868+ raw = True
4869+ string = string [1 :]
4870+
4871+ # unwrap string ref
4872+ internal_assert (string .startswith (strwrapper ) and string .endswith (unwrapper ), "invalid db string item" , string )
4873+ text , strchar = self .get_ref ("str" , string [1 :- 1 ])
4874+
4875+ # must be triple-quoted
4876+ if len (strchar ) == 1 :
4877+ raise CoconutDeferredSyntaxError ("d-string prefix requires triple-quoted string" , loc )
4878+
4879+ # apply dedentation
4880+ text = self ._d_string_dedent (text , loc )
4881+
4882+ return "b" + ("r" if raw else "" ) + self .wrap_str (text , strchar [0 ], multiline = True )
4883+
4884+ def df_string_handle (self , original , loc , tokens ):
4885+ """Process d-string with f prefix."""
4886+ return self ._d_f_string_handle (original , loc , tokens , is_t = False )
4887+
4888+ def dt_string_handle (self , original , loc , tokens ):
4889+ """Process d-string with t prefix."""
4890+ return self ._d_f_string_handle (original , loc , tokens , is_t = True )
4891+
4892+ def _d_f_string_handle (self , original , loc , tokens , is_t = False ):
4893+ """Process d-string combined with f or t prefix."""
4894+ string , = tokens
4895+
4896+ # strip raw r
4897+ raw = string .startswith ("r" )
4898+ if raw :
4899+ string = string [1 :]
4900+
4901+ # unwrap f-string ref
4902+ internal_assert (string .startswith (strwrapper ) and string .endswith (unwrapper ), "invalid df string item" , string )
4903+ strchar , string_parts , exprs = self .get_ref ("f_str" , string [1 :- 1 ])
4904+
4905+ # must be triple-quoted
4906+ if len (strchar ) == 1 :
4907+ raise CoconutDeferredSyntaxError ("d-string prefix requires triple-quoted string" , loc )
4908+
4909+ # apply dedentation to the f-string parts
4910+ # reconstruct with placeholders for expressions
4911+ placeholder = "\x00 "
4912+ full_text = placeholder .join (string_parts )
4913+
4914+ if not full_text .startswith ("\n " ):
4915+ raise CoconutDeferredSyntaxError ("d-string contents must start with a newline after opening quotes" , loc )
4916+ full_text = full_text [1 :]
4917+
4918+ lines = full_text .split ("\n " )
4919+
4920+ # determine common indentation (treat placeholders as non-whitespace)
4921+ indent = None
4922+ for i , line in enumerate (lines ):
4923+ is_last = (i == len (lines ) - 1 )
4924+ line_no_ph = line .replace (placeholder , "X" )
4925+ if not is_last and line_no_ph .strip () == "" :
4926+ continue
4927+ stripped = line_no_ph .lstrip ()
4928+ line_indent = line_no_ph [:len (line_no_ph ) - len (stripped )]
4929+ if indent is None :
4930+ indent = line_indent
4931+ else :
4932+ common = ""
4933+ for a , b in zip (indent , line_indent ):
4934+ if a == b :
4935+ common += a
4936+ else :
4937+ break
4938+ indent = common
4939+
4940+ if indent is None :
4941+ indent = ""
4942+
4943+ # apply dedentation
4944+ result_lines = []
4945+ for i , line in enumerate (lines ):
4946+ is_last = (i == len (lines ) - 1 )
4947+ line_no_ph = line .replace (placeholder , "X" )
4948+ if line_no_ph .strip () == "" and not is_last :
4949+ result_lines .append ("" )
4950+ elif line .startswith (indent ):
4951+ result_lines .append (line [len (indent ):])
4952+ elif indent .startswith (line_no_ph ) and line_no_ph .strip () == "" :
4953+ result_lines .append ("" )
4954+ else :
4955+ raise CoconutDeferredSyntaxError ("inconsistent indentation in d-string" , loc )
4956+
4957+ dedented = "\n " .join (result_lines )
4958+ new_parts = dedented .split (placeholder )
4959+
4960+ # now delegate to f_string_handle with modified parts
4961+ # re-wrap as f-string ref and call f_string_handle
4962+ new_ref = self .wrap_f_str (strchar , new_parts , exprs )
4963+ new_token = ("r" if raw else "" ) + new_ref
4964+ return self .f_string_handle (original , loc , [new_token ], is_t = is_t )
4965+
47814966 def decorators_handle (self , loc , tokens ):
47824967 """Process decorators."""
47834968 defs = []
0 commit comments