diff --git a/.env.example b/.env.example index 07d85c9..c617b21 100644 --- a/.env.example +++ b/.env.example @@ -83,25 +83,21 @@ WEBLATE_SECURE_PROXY_SSL_HEADER=HTTP_X_FORWARDED_PROTO,https # --------------------------------------------------------------------------- # PostgreSQL (host) # --------------------------------------------------------------------------- -# CD compose pins POSTGRES_HOST and POSTGRES_PORT in docker-compose.cd.yml -# (host.docker.internal, default 5432). Values below apply via env_file for user/db. +# CD compose defaults (override in .env if host Postgres/Redis differ). +POSTGRES_HOST=host.docker.internal +POSTGRES_PORT=5432 POSTGRES_USER=weblate_app # Canonical name per Weblate Docker docs (POSTGRES_DATABASE is an alias some images accept). POSTGRES_DB=weblate_db POSTGRES_DATABASE=weblate_db -# Ignored in CD (compose pins host/port). Documented for reference / non-CD stacks. -# POSTGRES_HOST=host.docker.internal -# POSTGRES_PORT=5432 # Optional: POSTGRES_PASSWORD_FILE=/run/secrets/db_password # Logical DB 1 avoids clashing with other apps on the same Redis (default for Weblate is 1). REDIS_DB=1 - -# Reference only for CI / local overrides (not used by docker-compose.cd.yml): -# REDIS_HOST=redis -# REDIS_PORT=6379 +REDIS_HOST=redis +REDIS_PORT=6379 # --------------------------------------------------------------------------- # Mail server (production: required for notifications; env_file only) diff --git a/docker/docker-compose.cd.yml b/docker/docker-compose.cd.yml index b498e08..7771ee5 100644 --- a/docker/docker-compose.cd.yml +++ b/docker/docker-compose.cd.yml @@ -20,9 +20,9 @@ services: environment: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env} WEBLATE_ADMIN_PASSWORD: ${WEBLATE_ADMIN_PASSWORD:?set in .env} - POSTGRES_HOST: host.docker.internal + POSTGRES_HOST: ${POSTGRES_HOST:-host.docker.internal} POSTGRES_PORT: ${POSTGRES_PORT:-5432} - REDIS_HOST: redis + REDIS_HOST: ${REDIS_HOST:-redis} REDIS_PORT: ${REDIS_PORT:-6379} healthcheck: test: [CMD, curl, -sf, 'http://localhost:8080${WEBLATE_URL_PREFIX:-}/healthz/'] diff --git a/src/boost_weblate/utils/quickbook.py b/src/boost_weblate/utils/quickbook.py index e618d29..ce55817 100644 --- a/src/boost_weblate/utils/quickbook.py +++ b/src/boost_weblate/utils/quickbook.py @@ -483,6 +483,12 @@ def _parse_qbk( break continue + # Whitespace after inline bracket markup on the same line (e.g. "[@url] prose"). + if ch in {" ", "\t"}: + while i < stop and content[i] in {" ", "\t"}: + i += 1 + continue + if content[i : i + 3] == "'''": i += 3 while i < stop and content[i : i + 3] != "'''": @@ -507,128 +513,138 @@ def _parse_qbk( i = end + 1 if kw in _SKIP_KEYWORDS or kw in _SKIP_SINGLE_CHARS: - continue - - raw_inner = block_text[content_off:-1] - lstrip_n = len(raw_inner) - len(raw_inner.lstrip()) - rstrip_n = len(raw_inner) - len(raw_inner.rstrip()) - inner = raw_inner.strip() - if not inner: - continue - - inner_abs_start = bracket_start + content_off + lstrip_n - inner_abs_end = bracket_start + len(block_text) - 1 - rstrip_n - inner_multiline = "\n" in inner - - if kw in _HEADING_KEYWORDS: - ctx = f"heading {kw[1]}" if kw != "heading" else "heading" - segments.append( - _Seg( - inner_abs_start, - inner_abs_end, - bracket_line, - "heading", - inner, - no_wrap=True, - context=ctx, + eol = end + 1 + while eol < stop and content[eol] != "\n": + eol += 1 + if _has_prose(content[end + 1 : eol]): + i = bracket_start + else: + continue + else: + raw_inner = block_text[content_off:-1] + lstrip_n = len(raw_inner) - len(raw_inner.lstrip()) + rstrip_n = len(raw_inner) - len(raw_inner.rstrip()) + inner = raw_inner.strip() + if not inner: + continue + + inner_abs_start = bracket_start + content_off + lstrip_n + inner_abs_end = bracket_start + len(block_text) - 1 - rstrip_n + inner_multiline = "\n" in inner + + if kw in _HEADING_KEYWORDS: + ctx = f"heading {kw[1]}" if kw != "heading" else "heading" + segments.append( + _Seg( + inner_abs_start, + inner_abs_end, + bracket_line, + "heading", + inner, + no_wrap=True, + context=ctx, + ) ) - ) - continue - - if kw == "section": - if inner_multiline: - nl_pos = inner.index("\n") - raw_title_line = inner[:nl_pos] - title = raw_title_line.strip() - if title: - title_lead = raw_title_line.index(title) - title_abs_start = inner_abs_start + title_lead - title_abs_end = title_abs_start + len(title) + continue + + if kw == "section": + if inner_multiline: + nl_pos = inner.index("\n") + raw_title_line = inner[:nl_pos] + title = raw_title_line.strip() + if title: + title_lead = raw_title_line.index(title) + title_abs_start = inner_abs_start + title_lead + title_abs_end = title_abs_start + len(title) + segments.append( + _Seg( + title_abs_start, + title_abs_end, + bracket_line, + "section-title", + title, + no_wrap=True, + context="section title", + ) + ) + body_abs_start = inner_abs_start + nl_pos + 1 + if body_abs_start < inner_abs_end: + segments.extend( + _parse_qbk( + content, body_abs_start, inner_abs_end, _depth + 1 + ) + ) + else: segments.append( _Seg( - title_abs_start, - title_abs_end, + inner_abs_start, + inner_abs_end, bracket_line, "section-title", - title, + inner, no_wrap=True, context="section title", ) ) - body_abs_start = inner_abs_start + nl_pos + 1 - if body_abs_start < inner_abs_end: + continue + + if kw in _ADMONITION_KEYWORDS: + if inner_multiline: segments.extend( _parse_qbk( - content, body_abs_start, inner_abs_end, _depth + 1 + content, inner_abs_start, inner_abs_end, _depth + 1 ) ) - else: - segments.append( - _Seg( - inner_abs_start, - inner_abs_end, - bracket_line, - "section-title", - inner, - no_wrap=True, - context="section title", + else: + segments.append( + _Seg( + inner_abs_start, + inner_abs_end, + bracket_line, + "admonition", + inner, + no_wrap=False, + context=kw, + ) ) - ) - continue + continue - if kw in _ADMONITION_KEYWORDS: - if inner_multiline: - segments.extend( - _parse_qbk(content, inner_abs_start, inner_abs_end, _depth + 1) - ) - else: - segments.append( - _Seg( - inner_abs_start, - inner_abs_end, - bracket_line, - "admonition", - inner, - no_wrap=False, - context=kw, + if kw == ":": + if inner_multiline: + segments.extend( + _parse_qbk( + content, inner_abs_start, inner_abs_end, _depth + 1 + ) ) - ) - continue + else: + segments.append( + _Seg( + inner_abs_start, + inner_abs_end, + bracket_line, + "blockquote", + inner, + no_wrap=False, + context="blockquote", + ) + ) + continue - if kw == ":": - if inner_multiline: + if kw in {"table", "variablelist"}: segments.extend( - _parse_qbk(content, inner_abs_start, inner_abs_end, _depth + 1) - ) - else: - segments.append( - _Seg( + _parse_table_inner( + content, inner_abs_start, inner_abs_end, bracket_line, - "blockquote", - inner, - no_wrap=False, - context="blockquote", + kw, + _depth, ) ) - continue + continue - if kw in {"table", "variablelist"}: - segments.extend( - _parse_table_inner( - content, - inner_abs_start, - inner_abs_end, - bracket_line, - kw, - _depth, - ) - ) continue - continue - para_start = i para_line = line diff --git a/tests/utils/test_quickbook.py b/tests/utils/test_quickbook.py index 3266b04..3736be9 100644 --- a/tests/utils/test_quickbook.py +++ b/tests/utils/test_quickbook.py @@ -351,6 +351,19 @@ def test_parse_qbk_paragraph_breaks_on_soft_wrap_space_line() -> None: assert segs[0].msgid == "first line" +def test_parse_qbk_inline_url_followed_by_prose_on_same_line() -> None: + segs = _parse_qbk("[@https://example.com/page HEAD 请求] 方法表示客户端。\n") + assert len(segs) == 1 + assert segs[0].seg_type == "paragraph" + assert segs[0].msgid == "[@https://example.com/page HEAD 请求] 方法表示客户端。" + + segs = _parse_qbk("[@https://example.com x] prose.\n") + assert len(segs) == 1 + assert segs[0].msgid == "[@https://example.com x] prose." + + assert _parse_qbk("[@https://example.com x]\n") == [] + + def test_parse_qbk_paragraph_unclosed_bracket_then_para_break() -> None: text = "intro\n[note not closed here\n[h2 real]\n" segs = _parse_qbk(text)