Walkthrough: Creating and Running Text Templates

You can use the toolkit in Domain-Specific Language Tools to transform text templates. By transforming (also known as running) text templates, you can generate text artifacts that combine text blocks with information supplied from input sources, such as models. For more information, see Generating Artifacts By Using Text Templates.

This walkthrough shows you how to use Domain-Specific Language Tools to create and transform a text template. You will use the Class Diagrams solution template as a starting point for a domain-specific language that you create.

Tasks illustrated in this walkthrough include:

  • Creating a domain-specific language

  • Creating a custom text template

  • Adding code to a text template

  • Transforming a text template

  • Generating classes based on a model

  • Adding class features to a text template

  • Generating code from relationships

  • Generating code from relationship properties

  • Splitting a text template into multiple files

  • Generating text from a class feature

  • Validating the model before code generation

  • Accessing multiple models from a text template

    Note

    If errors occur in this walkthrough, see Common Validation Errors and Warnings in Text Templates.

Prerequisites

To complete this walkthrough, you will need:

Creating a Domain-Specific Language

Create a domain-specific language solution that has the following features:

  • Name: ClassDiagramExample

  • Solution template: Class Diagrams

  • File extension: .testcd

  • Company name: Fabrikam

For more information about creating a domain-specific language solution, see Walkthrough: Creating a Domain-Specific Language Solution.

Creating a Custom Text Template

Now that you have created the project, you can build it and run it in the experimental build. You will use the Debugging project in the experimental build to create a custom text template. For more information about the experimental build, see Experimental Build.

Later, you will use this text template to return information about the Sample.testcd model.

To create a text template

  1. Build the project and start debugging. (On the Build menu click Rebuild Solution, then on the Debug menu click Start Debugging.) A new instance of Visual Studio opens the Debugging project.

  2. In the Debugging project, open the Sample.testcd file. This model contains the following classes:

    • Member

    • Library

    • Reservation

    • Loan

    • Title

    • Item

  3. Add a text file named LibraryCode.tt to the Debugging project.

  4. Select LibraryCode.tt in Solution Explorer, and then, in the Properties window, make sure that the Custom Tool property is set to TextTemplatingFileGenerator.

  5. Right-click LibraryCode.tt, and then click Run Custom Tool.

    The system generates the file LibraryCode.cs. To display this file, expand the LibraryCode.tt node.

    Note

    By default, the file name extension of the generated file is .cs. However, you can change the file name extension of the file that you just generated and also the default file name extension for files that you generate later. For more information, see How to: Specify File Output Types in Text Templates.

Adding Code to a Text Template

With the blank text template open, you can add directives, statements, and expressions. In text templates, you indicate directives by using <#@ tags, statements with <# tags, and expressions with <#= tags. For more information, see Adding Code to Text Templates.

In this procedure, you add directives and statements that generate a list of the classes in the Sample.testcd model.

To add code to a text template

  1. Copy the following two directives to LibraryCode.tt:

    Note

    By adding the two directives at the top, template and ClassDiagramExample, you can use the elements of the Sample.testcd model as classes in the text template code. For more information, see Accessing Models from Text Templates.

    <#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#>
    <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #>
    
  2. Copy the following statement and expression lines to LibraryCode.tt. This code generates a list of the classes that are in the Sample.testcd model:

    <#
        foreach(ModelType type in this.ModelRoot.Types)
        {
    #>
        <#= type.Name#>
    <#
        }
    #>
    

Transforming a Text Template

With the text template complete, the next step is to transform LibraryCode.tt. This transformation generates an output file that contains a simple list of the classes in the Sample.testcd model.

To transform the text template

  1. In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.

    Note

    You can transform all of the text templates in your solution by clicking Transform All Templates on the Solution Explorer toolbar. Also, you transform a single text template every time that you save the file.

  2. In Solution Explorer, double-click LibraryCode.cs.

    The file opens with the following output:

    Item

    Title

    Book

    Member

    Library

    Loan

    MultipleAssociation1

    Reservation

    MultipleAssociation2

    Note

    At this point, you will see an error in the Error List window. You did not cause this error by running the custom tool. Because the generated text file has an extension of .cs, Visual Studio is trying to compile it. At this intermediate stage, the generated text will not compile. You can ignore this error.

  3. Delete the contents of LibraryCode.cs, and then repeat step 1.

    The system regenerates the file.

    Note   You can also delete the output file itself, not just the contents of the file. You regenerate the output file every time that you transform the text template.

Generating Classes Based on a Model

This next procedure modifies the text template to generate Visual C# classes in LibraryCode.cs, which is based on the Sample.testcd model.

To generate Visual C# classes in the output file

  1. Replace the code in LibraryCode.tt with the following code.

    The changes are highlighted.

    <#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#>
    <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #>
    
    <#
        foreach(ModelType type in this.ModelRoot.Types)
        {
    #>
    <#
            if(type is ModelClass)
            {
    #>
    public class <#= type.Name#>
    {
    }
    <#
            }
    #>
    <#
        }
    #>
    

    Text without text template tags (<# and #>) is a text block and will be generated in the output file exactly as you type it. In this case, the text block is public class and its corresponding opening and closing brackets. The expression statement <#= type.Name#> adds the class name to the output file LibraryCode.cs. For more information, see Adding Code to Text Templates.

    The output file reflects the format of the text template. The text public class <#= type.Name> and the opening and closing parentheses for the class appear on the left side of LibraryCode.tt. Because these items appear on the left side, the system formats the output file LibraryCode.cs with consistent indentations. This technique is used in various areas in the rest of this walkthrough.

  2. In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.

    Note

    The compilation error from previous steps no longer appears.

  3. Open LibraryCode.cs.

    The system now generates the classes as Visual C# classes instead of as a simple list:

    public class Item
    {
    }
    public class Title
    {
    }
    public class Book
    {
    }
    public class Member
    {
    }
    public class Library
    {
    }
    public class Loan
    {
    }
    public class Reservation
    {
    }
    

Adding Class Features to a Text Template

Your C# code takes its class names, such as library, from the names in your model. Your text template will generate faulty code if you type a name into a model class that is not a valid C# identifier.

In the following procedure, you create a helper function that translates the name from the model into a valid C# identifier.

You create helper functions in text templates by adding them to class feature blocks. In text templates, you indicate class feature blocks with <#+ tags. For more information, see Class Feature Syntax.

You also call the Warning method, which you can use to add custom messages to the Error List window. The custom warning tells the user that the name of a model class contains no valid characters.

To add helper functions to the text template

  1. Replace the code in LibraryCode.tt with the following code.

    The changes are highlighted.

    <#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#>
    <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #>
    <#@ import namespace = "System.Text.RegularExpressions" #>
    <#
        foreach(ModelType type in this.ModelRoot.Types)
        {
            if(type is ModelClass)
            {
                ModelClass classType = (ModelClass)type;
    
                string className = MakeValidName(type.Name);
    
                if(string.IsNullOrEmpty(className))
                {
                    // Generate a validation constraint
                    Warning (String.Format("ClassName '{0}' is not a valid class name", type.Name));
                }
                else
                {
    #>
    public class <#= className#> <# if(classType.Superclass != null) { #>: <#= classType.Superclass.Name#> <# } #>
    
    {
    }
    
    <#
                } 
            }
        }
    #>
    
    <#+
    private static string MakeValidName(string typeName)
    {
        //Use the System.Text.RegularExpressions namespace specified in the import directive
        //Remove non-alpha characters
        string fixedName = Regex.Replace(typeName,"[^a-zA-Z]","");
        return fixedName;
    }
    #>
    
  2. In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.

  3. Open LibraryCode.cs.

    The output looks like this:

    public class Item 
    {
    }
    
    public class Title 
    {
    }
    
    public class Book : Item 
    {
    }
    
    public class Member 
    {
    }
    
    public class Library 
    {
    }
    
    public class Loan 
    {
    }
    
    public class Reservation 
    {
    }
    

Generating Code from Relationships

In the next procedure, you extend LibraryCode.tt to generate code based on the domain relationships in the model file Sample.testcd. In each ModelClass, you generate code for the ModelAttributes, which are linked through the ClassHasAttributes relationship. From the domain-specific language definition, you can see whether this relationship generates the Attributes property for the domain class, ModelClass.

To add private fields to the classes in the generated text output file

  1. Replace the code in LibraryCode.tt with the following code.

    The changes are highlighted.

    <#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#>
    <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #>
    <#@ import namespace = "System.Text.RegularExpressions" #>
    <#
        foreach(ModelType type in this.ModelRoot.Types)
        {
            if(type is ModelClass)
            {
                ModelClass classType = (ModelClass)type;
    
                string className = MakeValidName(type.Name);
                if (string.IsNullOrEmpty(className))
                {
                   // Generate a validation constraint
                   Warning(String.Format("ClassName '{0}' is not a valid class name", type.Name));
                }
                else
                {
    #>public class <#= className#> <# if(classType.Superclass != null) { #>: <#= classType.Superclass.Name#> <# } #>
    
    {
    <#
                foreach(ModelAttribute attribute in classType.Attributes)
                {
    #>
        <#= MakeValidName(attribute.Type)#> <#= MakeValidName(attribute.Name)#>;
    <#
                }
    #>
    }
    
    <#
                }
            }
        }
    #>
    
    <#+
    private static string MakeValidName(string typeName)
    {
        //Use the System.Text.RegularExpressions namespace specified in the import directive
        //Remove non-alpha characters
        string fixedName = Regex.Replace(typeName,"[^a-zA-Z]","");
        return fixedName;
    }
    #>
    
  2. In Solution Explorer, right-click LibraryCode.tt, and click Run Custom Tool.

  3. Open LibraryCode.cs.

    The classes are now generated as Visual C# classes that have data types included in them. The output looks like this:

    public class Item 
    {
    }
    
    public class Title 
    {
        string name;
    }
    
    public class Book : Item 
    {
    }
    
    public class Member 
    {
    }
    
    public class Library 
    {
    }
    
    public class Loan 
    {
        Date commenced;
    }
    
    public class Reservation 
    {
        Date made;
       }
    

Generating Code from Relationship Properties

In this procedure, you add code to implement bidirectional associations by using a pair of properties. There is one property in the class at each end of the relationship.

To perform this task, you must obtain the names that the user has given to each end of the relationship. These properties are for the relationship itself, so you must navigate to individual instance links of the relationship by using its GetLinksTo… static methods.

To generate the bidirectional associations in the output file

  1. Replace the code in LibraryCode.tt with the following code.

    The changes are highlighted.

    <#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#>
    <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #>
    <#@ import namespace = "System.Text.RegularExpressions" #>
    <#
        foreach(ModelType type in this.ModelRoot.Types)
        {
            if(type is ModelClass)
            {
                ModelClass classType = (ModelClass)type;
    
                string className = MakeValidName(type.Name);
                if (string.IsNullOrEmpty(className))
                {
                   // Generate a validation constraint
                   Warning(String.Format("ClassName '{0}' is not a valid class name", type.Name));
                }
                else
                {
    #>
    public class <#= className#> <# if(classType.Superclass != null) { #>: <#= classType.Superclass.Name#> <# } #>
    
    {
    <#
                foreach(ModelAttribute attribute in classType.Attributes)
                {
    #>
        <#= attribute.Type#> <#= attribute.Name#>;
    <#
                }
    
                foreach(BidirectionalAssociation association in BidirectionalAssociation.GetLinksToBidirectionalTargets(classType))
                {
                    ModelClass associatedClass = association.BidirectionalTarget;
                    // Note that we use the Role name here.
                    if(!string.IsNullOrEmpty(association.SourceRoleName) && !string.IsNullOrEmpty(association.TargetRoleName))
                    {
    #>
    
        private <#= associatedClass.Name#> <#= association.TargetRoleName#>Value;
        public <#= associatedClass.Name#> <#= association.TargetRoleName#>
        {
            get{ return <#= association.TargetRoleName#>Value; }
            set
            {
                          if (<#= association.TargetRoleName#> != value)
               {
    if (<#= association.TargetRoleName#> != null) 
    <#= association.TargetRoleName#>.<#=association.SourceRoleName#> = null;
    <#= association.TargetRoleName#>Value = value;
    if (value != null) 
    {
                <#= association.TargetRoleName#>Value.<#=association.SourceRoleName#>=this;
    }
        }
            }
        }
    <#
                    }
                    else
                    {
                        Warning(String.Format("Ignoring BidirectionalAssociation from {0} to {1} because its SourceRoleName or TargetRoleName is not defined", classType.Name, associatedClass.Name));
                    }
                }
    #>
    <#
                foreach(BidirectionalAssociation association in BidirectionalAssociation.GetLinksToBidirectionalSources(classType))
                {
                    ModelClass associatedClass = association.BidirectionalSource;
    
                    if(!string.IsNullOrEmpty(association.SourceRoleName) && !string.IsNullOrEmpty(association.TargetRoleName))
                    {
    #>
    
        private <#= associatedClass.Name#> <#= association.SourceRoleName#>Value;
        public <#= associatedClass.Name#> <#= association.SourceRoleName#>
        {
            get {return <#= association.SourceRoleName#>Value; }
            set
            {
                            if (<#= association.SourceRoleName#> != value)
    {
    if (<#= association.SourceRoleName#> != null) 
     <#= association.SourceRoleName#>.<#= association.TargetRoleName#> = null;
    <#= association.SourceRoleName#>Value = value;
    if (value != null)
    {
    <#= association.SourceRoleName#>Value.<#= association.TargetRoleName#>=this;
    }
         }        
           }
        }
    <#
                    }
                    else
                    {
                        Warning(String.Format("Ignoring BidirectionalAssociation from {0} to {1} because its SourceRoleName or TargetRoleName is not defined", classType.Name, associatedClass.Name));
                    }
                }
    #>
    }
    
    <#
              }
            }
        }
    #>
    
    <#+
    private static string MakeValidName(string typeName)
    {
        //Use the System.Text.RegularExpressions namespace specified in the import directive
        //Remove non-alpha characters
        string fixedName = Regex.Replace(typeName,"[^a-zA-Z]","");
        return fixedName;
    }
    #>
    
  2. In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.

  3. Open LibraryCode.cs.

    Bidirectional associations appear in the Reservation and Item classes as properties, as shown in the following example:

    public class Item 
    {
        private Title titleValue;
        public Title title
        {
            get {return titleValue; }
            set
            {
               if (title != value)
      {
          if (title != null) 
         title.stock = null;
    titleValue = value;
    if (value != null)
    {
        titleValue.stock=this;
    }
             }        
            }
        }
    }
    
    public class Title 
    {
        string name;
    
        private Item stockValue;
        public Item stock
        {
            get{ return stockValue; }
            set
            {
                if (stock != value)
                {
           if (stock != null) 
         stock.title = null;
     stockValue = value;
     if (value != null) 
     {
                     stockValue.title=this;
     }
             }
            }
        }
    }
    
    public class Book : Item 
    {
    }
    
    public class Member 
    {
    }
    
    public class Library 
    {
    }
    
    public class Loan 
    {
        Date commenced;
    }
    
    public class Reservation 
    {
        Date made;
    }
    

Splitting a Text Template into Multiple Files

If a text template becomes too large to manage effectively, you can split it into multiple files. This makes it easier to manage and it allows you to reuse common code. For more information, see How to: Split Text Templates into Multiple Files.

In this example you will move the helper function, MakeValidName, in LibraryCode.tt to a separate file. You use the include directive in the original text template to access the helper function in the new file.

To split a text template into multiple files

  1. To the Debugging project, add a new text file named HelperFunctions.ttinclude.

  2. Copy the import directive and the helper function from LibraryCode.tt into HelperFunctions.ttinclude.

    HelperFunctions.ttinclude now looks like this:

    <#@ import namespace = "System.Text.RegularExpressions" #>
    <#+
    private static string MakeValidName(string typeName)
    {
        //Use the System.Text.RegularExpressions namespace specified in the import directive
        //Remove non-alpha characters
        string fixedName = Regex.Replace(typeName,"[^a-zA-Z]","");        return fixedName;}
    #>
    
  3. Save the file.

  4. Delete the copied import directive and helper function from LibraryCode.tt.

  5. Add the following directive to LibraryCode.tt:

    <#@ include file = "HelperFunctions.ttinclude"#>
    
  6. On the File menu, click Save LibraryCode.tt.

  7. In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.

  8. Open LibraryCode.cs. The output should not have changed.

Generating Text from a Class Feature

You can generate text from a helper function in a class feature. In this example, you generate code from the source and target ends of the BidirectionalAssociation. Then you move that code into a helper function and call it from the main template.

To generate text from a class feature

  1. Open HelperFunctions.ttinclude.

  2. Add this function after the existing function.

    Note

    The function contains several blocks, but they are all class feature blocks (“<#+”). Also, MakeValidName has been applied to the role names.

    <#+
    private void CreateAccessor(ModelClass classType, ModelClass associatedClass, string fromRoleName, string toRoleName)
    {
                    if(!string.IsNullOrEmpty(toRoleName) && !string.IsNullOrEmpty(fromRoleName))
                    {
                       string validClassName = MakeValidName(associatedClass.Name);
                       string validFromName = MakeValidName(fromRoleName);
                       string validToName = MakeValidName(toRoleName);
    #>
    
        private <#= validClassName#> <#= validFromName#>Value;
        public <#= validClassName#> <#= validFromName#>
        {
            get{ return <#= validFromName #>Value; }
            set
            {
               if (<#= validFromName#> != value)
               {
    if (<#= validFromName#> != null) 
    <#= validFromName#>.<#=validToName#> = null;
    <#= validFromName#>Value = value;
    if (value != null) 
    {
                <#= validFromName#>Value.<#=validToName#>=this;
    }
       }
            }
        }
    <#+
                    }
                    else
                    {
                        Warning(String.Format("Ignoring BidirectionalAssociation from {0} to {1} because its SourceRoleName or TargetRoleName is not defined", classType.Name, associatedClass.Name));
                    }
    }
    #>
    
  3. Save the file.

  4. Replace the code in LibraryCode.tt with the following code.

    The changes are highlighted.

    <#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"#>
    <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #>
    <#@ include file = "HelperFunctions.ttinclude"#>
    <#
        foreach(ModelType type in this.ModelRoot.Types)
        {
            if(type is ModelClass)
            {
                ModelClass classType = (ModelClass)type;
    
                string className = MakeValidName(type.Name);
                if (string.IsNullOrEmpty(className))
                {
                   // We should also generate a proper validation constraint for this
                   Warning(String.Format("ClassName '{0}' is not a valid class name",
                           type.Name));
                }
                else
                {
    
    #>
    public class <#= className#> <# if(classType.Superclass != null) { #>: <#= classType.Superclass.Name#> <# } #>
    
    {
    <#
                foreach(ModelAttribute attribute in classType.Attributes)
                {
    #>
        <#= attribute.Type#> <#= attribute.Name#>;
    <#
                }
                foreach(ClassOperation operation in classType.Operations) 
                {
    #>
        public void <#= operation.Name#>()
        {
        }
    <#
                }
    #>
    <#
                foreach(BidirectionalAssociation association in BidirectionalAssociation.GetLinksToBidirectionalTargets(classType))
                {
                    CreateAccessor(classType, association.BidirectionalTarget, association.TargetRoleName, association.SourceRoleName);
                }
    #>
    <#
                foreach(BidirectionalAssociation association in BidirectionalAssociation.GetLinksToBidirectionalSources(classType))
                {
    CreateAccessor(classType, association.BidirectionalSource, association.SourceRoleName, association.TargetRoleName);
                }
    #>
    }
    
    <#
              }
            }
        }
    #>
    

    In Solution Explorer, right-click LibraryCode.tt, and then click Run Custom Tool.

Validating the Model Before Code Generation

Certain characteristics of the model in Sample.testcd will cause your generator to generate incorrect code. These cases include:

  • Multiple ModelClasses that have the same name (or that have the same name after illegal characters and digits are removed).

  • Multiple ModelAttributes and Association roles with the same name in the same ModelClass.

  • Empty type name for a ModelAttribute.

  • Loops in the graph of inheritance relationships

As you develop your code generator, you will find other examples of incorrect code. To discourage the language user from creating models that have these errors, you should create appropriate validation constraints that will be applied when the user saves the model file.

In general, you should write validation constraints whenever you create templates or any other tools that process models. Using validation constraints ensures that the models that are passed to your tools and templates can be used correctly . You can display error messages when the language user validates the model or when a tool or template generates code from the model. However, you should display error messages as early as possible so that the language user can easily identify the elements to which the errors refer. For more information, see Walkthrough: Adding Validation to a Domain Model.

Accessing Multiple Models from a Text Template

You can access more than one model from the same text template. In this section, you create a second model, and then you use one text template for both of them. After you transform the text template, TestMultiple.tt, the code for both models will appear in the output file, TestMultiple.txt.

Note

For more information about how to use multiple models with text templates, see Accessing Models from Text Templates.

To add a second model to your project

  1. Add a new ClassDiagramExample model to the Debugging project. In the Add New Item dialog box, in the My Templates section, select ClassDiagramExample.

  2. In Solution Explorer, open Second.testcd.

  3. Drag two class objects to the model designer from the Toolbox.

  4. Save the new model.

To access multiple models from a text template

  1. To the Debugging project, add a new text file named TestMultiple.tt.

  2. Make sure that the Custom Tool property is set the to TextTemplatingFileGenerator.

  3. Add the following directives to the file.

    <#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation" #>
    <#@ output extension=".txt" #>
    
  4. Add the following directives.

    These directives allow you to access the models.

    Note

    The provides parameter for the Second.testcd model changes the name of the ModelRoot property to SecondModelRoot. This change prevents a conflict with the name of the ModelRoot property for the Sample.testcd model.

    <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Sample.testcd'" #>
    <#@ ClassDiagramExample processor="ClassDiagramExampleDirectiveProcessor" requires="fileName='Second.testcd'" provides="ModelRoot=SecondModelRoot"#>
    
  5. Add the following code, which generates a list of the classes that are in each of the two models.

    Model 1:
    <#
        foreach(ModelType type in this.ModelRoot.Types)
        {
    #>
            <#= type.Name#>
    <#
        }
    #>
    
    Model 2:
    <#
        foreach(ModelType type in this.SecondModelRoot.Types)
        {
    #>
            <#= type.Name#>
    <#
        }
    #> 
    
  6. In Solution Explorer, right-click TestMultiple.tt, and then click Run Custom Tool.

  7. In Solution Explorer, open TestMultiple.txt, which should resemble the following:

    Model 1:
            Library
            Member
            Reservation
            Book
            Item
            CD
            Title
            MultipleAssociation 1
            Loan Relation
    
    Model 2:
            ModelClass 1
            ModelClass 2
    

Security

For more information, see Security of Text Templates.

Next Steps

This walkthrough does not provide a complete description of all functionality for text templates. For example, you can debug text templates, create custom directive processors, specify culture, and include assemblies in text templates. For more information, see Generating Artifacts By Using Text Templates.

See Also

Concepts

Architecture of the Text Template Transformation Process

Other Resources

Generating Artifacts By Using Text Templates

Using Built-in Directives in Text Templates

Adding Code to Text Templates

Domain-Specific Language Tools Glossary