Conditional import possible?

I’ve got a template setup for our project generating a pdf output, and have recently started exploring what’s needed to adapt it for html output. For this, I need to change quite a few styling options. How I’ve achieved this so far is to have a conditional setup in my template file:

#context {
    if target() == "paged" {
        set page(...)
        // few other setup blocks for pdf output
    } else {
        html.elem("div", attrs: (class: "my-container"))[
            // few other setup blocks for html output
        ]
    }
}

However, since the template is quite large, I have to repeat this if/else clause several time throughout the file, which is quite cumbersome.

Instead, what I though I may be able to do was some sort of function overloading and have all the styling for paged output in one file, and styling for html in another file.

Here’s an example:

paged-defaults.typ

#let _page_defaults(title: none, doc) = {
    set text(...)
    set page(...)
    show raw: set text(...)
    // and lots more

    doc
}

html-defaults.typ

#let _page_defaults(title: none, doc) = {
    html.elem("style")[#read("assets/html-style.css")]
    
    show math.equation: it => html.frame(it)
    show math.equation.where(block: false): box

    html.elem("div", attrs: (class: "my-container"))[
        // and lots more

        doc
    ]
}

And from the main template file I do the following:

#context {
    if target() == "paged" {
        import "paged-defaults.typ": *
    } else {
        import "html-defaults.typ": *
    }
}

// lots more template setup

#let my_document(my_title, doc) = {
    #set document(title: my_title)
    #show: _paged_defaults()

    doc
}

However, with the above code, the compiler throws:

error: unknown variable: _page_defaults

I’ve tried wrapping the _paged_defaults() call in a context block but still no luck.

I’ve also tried replacing the use of target() with a sys.input variable set in the command line:

#if sys.inputs.at("type", default: "paged") == "paged" {
    import "paged-defaults.typ": *
} else {
    import "html-defaults.typ": *
}

but still no luck. Perhaps this form of function overloading or conditional import is simply not possible, but I just wanted to check and see if anyone has done something similar. Or if folks have suggestions on alternate ways to achieve something like this, I would appreciate any feedback, thanks!

You are basically doing a conditional import already, but you’re now facing the problem that this import is limited in scope: your imports are only visible inside the { ... } block where it happens.

Since target() requires context and you can’t ever get anything out of a context expression (see here), this is a dead end. The inputs approach could be made to work, though (maybe at the cost of good autocomplete, because we’re doing a dynamic import):

#let defaults-module-name = if sys.inputs.at("type", default: "paged") == "paged" {
    "paged-defaults.typ"
} else {
    "html-defaults.typ"
}
#import defaults-module-name: *

Or less direct, but imo also a bit less cursed:

#let defaults-module = if sys.inputs.at("type", default: "paged") == "paged" {
    import "paged-defaults.typ"
    paged-defaults
} else {
    import "html-defaults.typ"
    html-defaults
}
#import defaults-module: *

Instead of using a dynamic string and importing from that, this imports the module, and then does a wildcard import from that module.


However, even though that’s the answer to the literal question you asked, I don’t think that’s what you want or need. You don’t show it in full, but your template probably started out like this:

#show: doc => context {
    if target() == "paged" {
        set page(...)
        // few other setup blocks for pdf output
        doc
    } else {
        html.elem("div", attrs: (class: "my-container"))[
            // few other setup blocks for html output
        ]
        doc
    }
}

Your problem is that the if and else got too large, and you’re already halfway there by creating separate modules:

#show: doc => context {
    if target() == "paged" {
        import "paged-defaults.typ": *
        show: _paged_defaults()
        doc
    } else {
        import "html-defaults.typ": *
        show: _paged_defaults()
        doc
    }
}

Similar things could be done for other parts of your template. If you have a foo function with two separate implementations, you could write

#let foo(..args) = context {
    if target() == "paged" {
        import "paged-defaults.typ": *
        foo(..args)
    } else {
        import "html-defaults.typ": *
        foo(..args)
    }
}

@SillyFreak thanks for the response.

Indeed I did wonder if scoping might be an issue here, but when I tried

{
    #import "paged-defaults.typ": *
}

show: _page_defaults()

as a quick test, compilation succeeded, suggesting that the import above was not scoped within the { }. Do you know why this worked?

Yes, my template is quite a bit more complicated and there are several functions similar to _page_defaults that are defined differently for paged and html output. To complicate matters, I do have definitions for several different document types with different structurings that I’ve not listed here. This caused a proliferation if/else clauses that I was trying to avoid.

However, the following snippet you suggested does exactly what I want:

#let defaults-module = if sys.inputs.at("type", default: "paged") == "paged" {
    "paged-defaults.typ"
} else {
    "html-defaults.typ"
}
#import defaults-module: *

I now have a way forward with the conditional import being correctly scoped, thank you!

1 Like

(I just added a bit to my answer since I realized there was room for improvement, particularly for that import)

I think you can manage the proliferation of ifs by encapsulating that in a function. should you go the target() route, i.e. requiring context, you could end up with e.g.

#show: doc => context {
    import get-the-module(): *
    show: _paged_defaults()
    doc
}

#let foo(..args) = context {
    import get-the-module(): *
    foo(..args)
}

No idea; it really shouldn’t, maybe there was something else that masked the error in this line, or a forgotten import? I’d guess if you tried it again in a clean setup, it would fail.

1 Like