twig.js icon indicating copy to clipboard operation
twig.js copied to clipboard

TwigException "Unexpected end of object" when re-render compiled template

Open dmitry-k-asb opened this issue 1 year ago • 2 comments

I have template and run "test" function 2 times. First time it work, second time i have TwigException "Unexpected end of object". What i doing wrong?

Test function:

function test()
{   
    var task = {}
    task.id = 'TEST_TASK_ID'
    task.status = TASK_STATUS_SUCCESS
    task.name = 'TEST TASK NAME'
    task.result_count = 0
    task.create_at = 1725445537
    task.start_at = 1725445537
    task.end_at = 1725448000
    task.random = 123456789
    task.node_info = 'localhost'
    task.description = 'TEST DESCRIPTION'
    task.important = 1

    var env = {
        'page_type': 'tools',
        'select': 1,
        'can_select': 1,
        'status_onclick': 'test_status_onclick_func()'
    }

    var _DATA = {
        'task': task,
        'env':  env
    }

    TwigTemplateStorage.render('history_task_item', _DATA).then(rendered => console.log(rendered))
}

My class:

class TwigTemplateStorage {

    static _config = {
        'history_task_item': '/templates/task_item.twig',

    }

    static templates = {}

    static async _load(template_id, path)
    {
        const response = await fetch(path)
        const text = await response.text()

        TwigTemplateStorage.templates[template_id] = Twig.twig({
            'data': text.trim(),
        });
    };

    static async render(template_id, data)
    {
        if (!(template_id in TwigTemplateStorage.templates)) {
            if (template_id in TwigTemplateStorage._config) {     
                let path = TwigTemplateStorage._config[template_id]
                await TwigTemplateStorage._load(template_id, path)
            } else {
                throw new Error(template_id + ' not registered in TwigTemplateStorage _config')
            }
        }

        var compiled = TwigTemplateStorage.templates[template_id]
        var out = compiled.render({ '_DATA': data })
        return out.trim()
    }
}

My template:

{% set TASK_STATUS_WAIT    = 0  %}
{% set TASK_STATUS_RUN     = 1  %}
{% set TASK_STATUS_FAIL    = 9  %}
{% set TASK_STATUS_SUCCESS = 10 %}

{% set TASK_CAN_SELECT    = 0 %}
{% set TASK_SELECTED      = 1 %}
{% set TASK_CANNOT_SELECT = 2 %}

{% set status_icon_map = {
	'cases': {
		(TASK_STATUS_WAIT): {
			(TASK_CANNOT_SELECT): 'si si-clock',
			(TASK_CAN_SELECT): 'si si-plus',
			(TASK_SELECTED): 'si si-check'
		},
		(TASK_STATUS_RUN): {
			(TASK_CANNOT_SELECT): 'si si-clock',
			(TASK_CAN_SELECT): 'si si-plus',
			(TASK_SELECTED): 'si si-check'
		},
		(TASK_STATUS_FAIL): {
			(TASK_CANNOT_SELECT): 'icon-exclamation'
		},
		(TASK_STATUS_SUCCESS): {
			(TASK_CANNOT_SELECT): 'si si-check',
			(TASK_CAN_SELECT): 'si si-plus',
			(TASK_SELECTED): 'si si-check'
		},
	},
	'tools': {
		(TASK_STATUS_WAIT): {
			(TASK_CANNOT_SELECT): 'si si-clock',
			(TASK_CAN_SELECT): 'si si-plus',
			(TASK_SELECTED): 'si si-close'
		},
		(TASK_STATUS_RUN): {
			(TASK_CANNOT_SELECT): 'si si-clock',
			(TASK_CAN_SELECT): 'si si-plus',
			(TASK_SELECTED): 'si si-close'
		},
		(TASK_STATUS_FAIL): {
			(TASK_CANNOT_SELECT): 'icon-exclamation',
		},
		(TASK_STATUS_SUCCESS): {
			(TASK_CANNOT_SELECT): 'si si-check',
			(TASK_CAN_SELECT): 'si si-plus',
			(TASK_SELECTED): 'si si-close'
		},
	},
	'my_tasks': {
		(TASK_STATUS_WAIT): {
			(TASK_CANNOT_SELECT): 'si si-clock',
		},
		(TASK_STATUS_RUN): {
			(TASK_CANNOT_SELECT): 'si si-clock',
		},
		(TASK_STATUS_FAIL): {
			(TASK_CANNOT_SELECT): 'icon-exclamation'
		},
		(TASK_STATUS_SUCCESS): {
			(TASK_CANNOT_SELECT): 'si si-check',
		},
	},
} %}
{% if status_icon_map[_DATA.env.page_type][_DATA.task.status][_DATA.env.select] is empty %}
	{% set icon_class = 'icon-question' %}
{% else %}
	{% set icon_class = status_icon_map[_DATA.env.page_type][_DATA.task.status][_DATA.env.select] %}
{% endif %}


{% if _DATA.task.status == TASK_STATUS_SUCCESS and _DATA.task.result_count > 0 %}
    {% set group = 'ok' %}
{% elseif _DATA.task.status == TASK_STATUS_SUCCESS and _DATA.task.result_count == 0 %}
    {% set group = 'empty' %}
{% elseif _DATA.task.status == TASK_STATUS_FAIL %}
	{% set group = 'fail' %}
{% elseif _DATA.task.status == TASK_STATUS_RUN or _DATA.task.status == TASK_STATUS_WAIT %}
	{% set group = 'wait' %}
{% endif %}

{% set status_group_map = {
	'ok': {
		'icon_color_class': 'task2-status-blue-icon',
		'history2_group': 'history2-description-status-ok'
	},
	'empty': {
		'icon_color_class': 'task2-status-red-icon',
		'history2_group': 'history2-description-status-empty'
	},
	'fail': {
		'icon_color_class': 'task2-status-red-icon',
		'history2_group': 'history2-description-status-fail'
	},
	'wait': {
		'icon_color_class': 'task2-status-grey-icon',
		'history2_group': 'history2-description-status-wait'
	}
} %}
{% if status_group_map[group] is empty %}
	{% set icon_color_class = 'task2-status-red-icon' %}
	{% set history2_group = 'history2-description-status-fail' %}
{% else %}
	{% set icon_color_class = status_group_map[group]['icon_color_class'] %}
	{% set history2_group = status_group_map[group]['history2_group'] %}
{% endif %}

{% if _DATA.task.end_at is not empty and _DATA.task.start_at is not empty %}
	{% set work_time  = _DATA.task.end_at - _DATA.task.start_at %}
	{% set work_hours = work_time > 3600 ? (work_time // 3600) : 0 %}
	{% set work_mins  = (work_time % 3600) // 60 %}
	{% set work_secs  = (work_time % 3600)  % 60 %}

	{% set work_hours = "%02d"|format(work_hours) %}
	{% set work_mins  = "%02d"|format(work_mins)  %}
	{% set work_secs  = "%02d"|format(work_secs)  %}

	{% set work_time = (work_hours > 0) ? work_hours ~ ':' ~ work_mins ~ ':' ~ work_secs : work_mins ~ ':' ~ work_secs %}
{% else %}
	{% set work_time = 'err' %}
{% endif %}

{% if _DATA.task.create_at is not empty %}
	{% set create_at      =  _DATA.task.create_at|date('d.m.Y') %}
	{% set create_at_full =  _DATA.task.create_at|date('H:i d.m.Y') %}
{% else %}
	{% set create_at      =  'err' %}
	{% set create_at_full =  'err' %}
{% endif %}

<div class="history2-one-task" data-id="{{_DATA.task.id}}">
	<div class="history2-card-and-toolbar">
		<div class="history2-card">
			<div class="history2-base-row">
				<span
                    class="task-icon {{icon_color_class}} task2-status-tooltip"
                    data-info="{}"
                    data-status="{{_DATA.task.status}}"
                    data-id="{{_DATA.task.id}}"
                    data-output=""
                    data-time="{{create_at_full}}"
                    data-name="{{_DATA.task.name}}"
                    data-count="{{_DATA.task.result_count|default('err')}}"
                    data-select="{{_DATA.env.select}}"
                    onclick="{{_DATA.env.status_onclick}}"
                    data-history2-group="{{history2_group}}"
					{% if _DATA.env.can_select == 1 %}
                    	style="cursor: pointer"
					{% endif %}
                >
					<i class="{{ icon_class }}"></i>
				</span>
				<div class="history2-name">
					{% if _DATA.task.status == TASK_STATUS_SUCCESS and _DATA.task.result_count > 0 %}
						<a class="link-primary" onclick="view.open('vk_users_array_id','{{_DATA.task.id}}','')">{{_DATA.task.name}}</a>
					{% else %}
						{{_DATA.task.name}}
					{% endif %}
				</div>
			</div>
			<div class="history2-info-row" data-id="{{_DATA.task.id}}">

				{% if _DATA.task.status in [TASK_STATUS_RUN, TASK_STATUS_WAIT] %}
					<div class="task-description" data-id="{{_DATA.task.id}}">{{_DATA.task.description|default('err')}}</div>
				{% else %}
					<span title="{{create_at_full}}">
						<i class="si si-calendar" style="padding-right: 2px;"></i>
						{{create_at}}
					</span>
					<div>
						<i class="si si-docs"></i>
						{{_DATA.task.result_count |default('err')}}
					</div>
					<div title="Время выполнения задачи">
						<i class="si si-reload"></i>
						{{work_time}}
					</div>
				{% endif %}
			</div>
			{% if _DATA.task.node_info is not empty %}
				{{_DATA.task.id}} / {{_DATA.task.node_info}}
			{% endif %}
		</div>
		<div class="history2-toolbar">
			{% if _DATA.task.important == 0 %}
				<i title="Пометить как важную" class="si si-star history2-toolbar-items" onclick="task.important(this,'{{_DATA.task.id}}');"></i>
			{% else %}
				<i title="Важная задача" class="fa fa-star task-important history2-toolbar-items" onclick="task.important(this,'{{_DATA.task.id}}');"></i>
			{% endif %}
			<i class="si si-equalizer history2-toolbar-items" title="Параметры задачи" onclick="task.params('{{_DATA.task.id}}');"></i>
			<i class="si si-reload history2-toolbar-items" title="Перезагрузить" onclick="task.restart('{{_DATA.task.id}}');"></i>
			<i class="si si-trash history2-toolbar-items" title="Удалить задачу" onclick="task.delete('{{_DATA.task.id}}');"></i>
		</div>
	</div>
	<div class="history2-progress-bar">
		<div class="progress progress-mini">
			<div class="progress-bar task-progress" data-id="{{_DATA.task.id}}" data-random="{{_DATA.task.random}}"></div>
		</div>
	</div>
</div>

dmitry-k-asb avatar Sep 05 '24 12:09 dmitry-k-asb

Are you able to simplify this to a minimal example that still causes the error?

willrowe avatar Sep 05 '24 15:09 willrowe

After studying the problem, it turned out that the error occurs when trying to use a variable with a value of 0 as a map key.

The class is the same as in the starting topic.

Test function:

function test()
{   
    TwigTemplateStorage.render('history_task_item', {'key': 10}).then(rendered => console.log(rendered))
}

Short test pattern causing problem on 2nd run:

{% set TEST_VAR_A_AS_MAP_KEY = 0  %}
{% set TEST_VAR_B_AS_MAP_KEY = 10  %}

{% set test_map = {
    (TEST_VAR_A_AS_MAP_KEY): 'test_value_A',
    (TEST_VAR_B_AS_MAP_KEY): 'test_value_B',
} %}

{{ dump(test_map) }}

<div>{{ test_map[_DATA.key] }}</div>

Dump result when running the test function for the first time:

object(2) {
  [10]=> 
  string(12) "test_value_B"
  [_keys]=> 
    object(1) {
    [0]=> 
    number(10)
  }
}

Render result when running the test function for the first time:

<div>test_value_B</div>

Dump result when running the test function for the second time:

undefined

Render result when running the test function for the second time:

Uncaught (in promise) Object { message: "Unexpected end of object.", name: "TwigException", type: "TwigException", file: undefined }

If you don't use 0 it works:

{% set TEST_VAR_A_AS_MAP_KEY = 1  %}
{% set TEST_VAR_B_AS_MAP_KEY = 10  %}

{% set test_map = {
    (TEST_VAR_A_AS_MAP_KEY): 'test_value_A',
    (TEST_VAR_B_AS_MAP_KEY): 'test_value_B',
} %}

{{ dump(test_map) }}

<div>{{ test_map[_DATA.key] }}</div>

Dump result for any number of runs:

 object(3) {
  [1]=> 
  string(12) "test_value_A"
  [10]=> 
  string(12) "test_value_B"
  [_keys]=> 
    object(2) {
    [0]=> 
    number(1)
    [1]=> 
    number(10)
  }
}

Render result for any number of runs:

<div>test_value_B</div>

There is no error, but the key values ​​are not taken from the variables:

{% set TEST_VAR_A_AS_MAP_KEY = 0  %}
{% set TEST_VAR_B_AS_MAP_KEY = 10  %}

{% set test_map = {
    TEST_VAR_A_AS_MAP_KEY: 'test_value_A',
    TEST_VAR_B_AS_MAP_KEY: 'test_value_B',
} %}

{{ dump(test_map) }}

<div>{{ test_map[_DATA.key] }}</div>

Dump result for any number of runs:

object(3) {
  [TEST_VAR_B_AS_MAP_KEY]=> 
  string(12) "test_value_B"
  [_keys]=> 
    object(2) {
    [0]=> 
    string(21) "TEST_VAR_A_AS_MAP_KEY"
    [1]=> 
    string(21) "TEST_VAR_B_AS_MAP_KEY"
  }
  [TEST_VAR_A_AS_MAP_KEY]=> 
  string(12) "test_value_A"
}

This works for any number of runs:

{% set TEST_VAR_A_AS_MAP_KEY = 0  %}
{% set TEST_VAR_B_AS_MAP_KEY = 10  %}

{% set test_map = {
    0: 'test_value_A',
    10: 'test_value_B',
} %}

{{ dump(test_map) }}

<div>{{ test_map[_DATA.key] }}</div>

This works for any number of runs, but the key type is "string":

{% set TEST_VAR_A_AS_MAP_KEY = 0  %}
{% set TEST_VAR_B_AS_MAP_KEY = 10  %}

{% set test_map = {
    (TEST_VAR_A_AS_MAP_KEY|number_format): 'test_value_A',
    (TEST_VAR_B_AS_MAP_KEY|number_format): 'test_value_B',
} %}

{{ dump(test_map) }}

<div>{{ test_map[_DATA.key] }}</div>

This causes an error on the second run:

{% set TEST_VAR_A_AS_MAP_KEY = 0  %}
{% set TEST_VAR_B_AS_MAP_KEY = 10  %}

{% set test_map = {
    (TEST_VAR_A_AS_MAP_KEY + 0): 'test_value_A',
    (TEST_VAR_B_AS_MAP_KEY + 0): 'test_value_B',
} %}

{{ dump(test_map) }}

<div>{{ test_map[_DATA.key] }}</div>

Results as in example 1.
With a zero value, the brackets work fine when dumping a variable, the error occurs specifically when working with the map:

{% set TEST_VAR_A_AS_MAP_KEY = 0  %}
{% set TEST_VAR_B_AS_MAP_KEY = 10  %}

{% set test_map = {
    (TEST_VAR_A_AS_MAP_KEY): 'test_value_A',
    (TEST_VAR_B_AS_MAP_KEY): 'test_value_B',
} %}

{{ dump(TEST_VAR_A_AS_MAP_KEY) }}
{{ dump((TEST_VAR_A_AS_MAP_KEY)) }}
{{ dump(test_map) }}

<div>{{ test_map[_DATA.key] }}</div>

Dump result:

number(0)

number(0)

object(2) {
  [10]=> 
  string(12) "test_value_B"
  [_keys]=> 
    object(1) {
    [0]=> 
    number(10)
  }
}

dmitry-k-asb avatar Sep 06 '24 08:09 dmitry-k-asb