v0.5.3
- Мелкие исправления увеличивающие работу конвертера
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB |
@@ -1,78 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2400" height="1700" viewBox="0 0 2400 1700">
|
||||
<rect width="100%" height="100%" fill="#fff"/>
|
||||
<rect x="16.0" y="16.0" width="2368.0" height="1668.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<rect x="22.4" y="22.4" width="2355.2" height="1655.2" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="22.4" y1="214.4" x2="2377.6" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="22.4" y1="1549.6" x2="2377.6" y2="1549.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="363.9" y1="22.4" x2="363.9" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1200.0" y1="22.4" x2="1200.0" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1612.2" y1="22.4" x2="1612.2" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1953.7" y1="22.4" x2="1953.7" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1200.0" y1="66.4" x2="1612.2" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1426.7" y1="22.4" x2="1426.7" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<text x="34.4" y="32.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">USED AT:</text>
|
||||
<text x="375.9" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">AUTHOR:</text>
|
||||
<text x="487.9" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">Кинзябулатов Рамиль</text>
|
||||
<text x="375.9" y="66.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">PROJECT:</text>
|
||||
<text x="503.9" y="66.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">Разработка универсальной модели торгового предприятия</text>
|
||||
<text x="375.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NOTES:</text>
|
||||
<text x="479.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">1</text>
|
||||
<text x="507.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">2</text>
|
||||
<text x="535.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">3</text>
|
||||
<text x="563.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">4</text>
|
||||
<text x="591.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">5</text>
|
||||
<text x="619.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">6</text>
|
||||
<text x="647.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">7</text>
|
||||
<text x="675.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">8</text>
|
||||
<text x="703.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">9</text>
|
||||
<text x="731.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">10</text>
|
||||
<text x="1210.0" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">DATE:</text>
|
||||
<text x="1284.0" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">08.02.2022</text>
|
||||
<text x="1434.7" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">REV:</text>
|
||||
<text x="1498.7" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">31.03.2022</text>
|
||||
<rect x="1212.0" y="80.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="1236.0" y="78.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">WORKING</text>
|
||||
<rect x="1212.0" y="112.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="1236.0" y="110.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">DRAFT</text>
|
||||
<rect x="1212.0" y="144.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="1236.0" y="142.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">RECOMMENDED</text>
|
||||
<rect x="1212.0" y="176.4" width="16.0" height="16.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="1236.0" y="174.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">PUBLICATION</text>
|
||||
<text x="1624.2" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">READER</text>
|
||||
<line x1="1612.2" y1="66.4" x2="1953.7" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<text x="1624.2" y="74.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">DATE</text>
|
||||
<text x="1965.7" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">CONTEXT:</text>
|
||||
<text x="2165.6" y="134.4" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="700" fill="#000000">TOP</text>
|
||||
<line x1="446.3" y1="1549.6" x2="446.3" y2="1677.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1906.6" y1="1549.6" x2="1906.6" y2="1677.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<text x="34.4" y="1557.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NODE:</text>
|
||||
<text x="234.4" y="1625.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="40.0" font-weight="700" fill="#000000">A-0</text>
|
||||
<text x="458.3" y="1557.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">TITLE:</text>
|
||||
<text x="1176.4" y="1625.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="32.0" font-weight="700" fill="#000000">Распорядиться товаром</text>
|
||||
<text x="1918.6" y="1557.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NUMBER:</text>
|
||||
<text x="2142.1" y="1621.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="40.0" font-weight="700" fill="#000000">1</text>
|
||||
<rect x="2341.6" y="1641.6" width="36.0" height="36.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="2359.6" y="1659.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">1</text>
|
||||
<line x1="38.4" y1="870.0" x2="888.0" y2="870.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="888.0,870.0 872.0,863.3 872.0,876.7" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1208.0" y1="230.4" x2="1208.0" y2="732.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="1208.0,732.0 1201.3,716.0 1214.7,716.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1528.0" y1="870.0" x2="2361.6" y2="870.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="2361.6,870.0 2345.6,863.3 2345.6,876.7" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1208.0" y1="1533.6" x2="1208.0" y2="1008.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="1208.0,1008.0 1201.3,1024.0 1214.7,1024.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<rect x="406.6" y="799.9" width="79.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="446.2" y="814.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Спрос</text>
|
||||
<rect x="1007.8" y="438.7" width="168.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1091.9" y="452.9" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Нормативная</text>
|
||||
<text x="1091.9" y="479.8" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">документация</text>
|
||||
<rect x="1889.0" y="799.9" width="78.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1928.1" y="814.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Товар</text>
|
||||
<rect x="1056.8" y="1262.3" width="119.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1116.4" y="1276.5" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Персонал</text>
|
||||
<rect x="896.0" y="740.0" width="640.0" height="276.0" fill="#c0c0c0" stroke="none" stroke-width="0.00"/>
|
||||
<rect x="888.0" y="732.0" width="640.0" height="276.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
||||
<polygon points="888.0,732.0 904.0,732.0 888.0,748.0" fill="#000000" stroke="#000000" stroke-width="1.00"/>
|
||||
<text x="1208.0" y="864.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="26.0" font-weight="400" fill="#000000">Распорядиться товаром</text>
|
||||
<text x="1516.0" y="998.0" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="400" fill="#000000">A0</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB |
@@ -1,166 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2800" height="1840" viewBox="0 0 2800 1840">
|
||||
<rect width="100%" height="100%" fill="#fff"/>
|
||||
<rect x="16.0" y="16.0" width="2768.0" height="1808.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<rect x="22.4" y="22.4" width="2755.2" height="1795.2" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="22.4" y1="214.4" x2="2777.6" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="22.4" y1="1689.6" x2="2777.6" y2="1689.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="421.9" y1="22.4" x2="421.9" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1400.0" y1="22.4" x2="1400.0" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1882.2" y1="22.4" x2="1882.2" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="2281.7" y1="22.4" x2="2281.7" y2="214.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1400.0" y1="66.4" x2="1882.2" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1665.2" y1="22.4" x2="1665.2" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<text x="34.4" y="32.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">USED AT:</text>
|
||||
<text x="433.9" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">AUTHOR:</text>
|
||||
<text x="545.9" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">Кинзябулатов Рамиль</text>
|
||||
<text x="433.9" y="66.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">PROJECT:</text>
|
||||
<text x="561.9" y="66.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">Разработка универсальной модели торгового предприятия</text>
|
||||
<text x="433.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NOTES:</text>
|
||||
<text x="537.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">1</text>
|
||||
<text x="565.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">2</text>
|
||||
<text x="593.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">3</text>
|
||||
<text x="621.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">4</text>
|
||||
<text x="649.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">5</text>
|
||||
<text x="677.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">6</text>
|
||||
<text x="705.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">7</text>
|
||||
<text x="733.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">8</text>
|
||||
<text x="761.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">9</text>
|
||||
<text x="789.9" y="130.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">10</text>
|
||||
<text x="1410.0" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">DATE:</text>
|
||||
<text x="1484.0" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">08.02.2022</text>
|
||||
<text x="1673.2" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">REV:</text>
|
||||
<text x="1737.2" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="22.0" font-weight="400" fill="#000000">31.03.2022</text>
|
||||
<rect x="1412.0" y="80.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="1436.0" y="78.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">WORKING</text>
|
||||
<rect x="1412.0" y="112.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="1436.0" y="110.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">DRAFT</text>
|
||||
<rect x="1412.0" y="144.4" width="16.0" height="16.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="1436.0" y="142.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="400" fill="#000000">RECOMMENDED</text>
|
||||
<rect x="1412.0" y="176.4" width="16.0" height="16.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="1436.0" y="174.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">PUBLICATION</text>
|
||||
<text x="1894.2" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">READER</text>
|
||||
<line x1="1882.2" y1="66.4" x2="2281.7" y2="66.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<text x="1894.2" y="74.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">DATE</text>
|
||||
<text x="2293.7" y="30.4" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">CONTEXT:</text>
|
||||
<rect x="2475.6" y="90.4" width="116.0" height="56.0" fill="#c4c4c4" stroke="none" stroke-width="0.00"/>
|
||||
<rect x="2471.6" y="86.4" width="116.0" height="56.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="2579.6" y="136.4" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">A-0</text>
|
||||
<line x1="518.3" y1="1689.6" x2="518.3" y2="1817.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="2226.6" y1="1689.6" x2="2226.6" y2="1817.6" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<text x="34.4" y="1697.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NODE:</text>
|
||||
<text x="270.4" y="1765.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="40.0" font-weight="700" fill="#000000">A0</text>
|
||||
<text x="530.3" y="1697.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">TITLE:</text>
|
||||
<text x="1372.4" y="1765.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="32.0" font-weight="700" fill="#000000">Распорядиться товаром</text>
|
||||
<text x="2238.6" y="1697.6" dx="0" dy="0.9em" text-anchor="start" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="18.0" font-weight="400" fill="#000000">NUMBER:</text>
|
||||
<text x="2502.1" y="1761.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="40.0" font-weight="700" fill="#000000">2</text>
|
||||
<rect x="2741.6" y="1781.6" width="36.0" height="36.0" fill="none" stroke="#000000" stroke-width="2.00"/>
|
||||
<text x="2759.6" y="1799.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="19.0" font-weight="700" fill="#000000">2</text>
|
||||
<line x1="38.4" y1="652.0" x2="696.0" y2="652.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="696.0,652.0 680.0,645.3 680.0,658.7" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="38.4" y1="891.8" x2="1208.0" y2="891.8" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="1208.0,891.8 1192.0,885.1 1192.0,898.6" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1296.0" y1="230.4" x2="1296.0" y2="278.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1296.0" y1="278.4" x2="784.0" y2="278.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="784.0" y1="278.4" x2="784.0" y2="566.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1296.0" y1="230.4" x2="1296.0" y2="854.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1296.0" y1="278.4" x2="1808.0" y2="278.4" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1808.0" y1="278.4" x2="1808.0" y2="1142.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="784.0,566.0 777.3,550.0 790.7,550.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<polygon points="1296.0,854.0 1289.3,838.0 1302.7,838.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<polygon points="1808.0,1142.0 1801.3,1126.0 1814.7,1126.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1008.0" y1="230.4" x2="1008.0" y2="566.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="1008.0,566.0 1001.3,550.0 1014.7,550.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="2032.0" y1="230.4" x2="2032.0" y2="1142.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="2032.0,1142.0 2025.3,1126.0 2038.7,1126.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1096.0" y1="603.8" x2="1152.0" y2="603.8" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1152.0" y1="603.8" x2="1152.0" y2="988.2" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1152.0" y1="988.2" x2="1208.0" y2="988.2" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="1208.0,988.2 1192.0,981.4 1192.0,994.9" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1096.0" y1="700.2" x2="1216.0" y2="700.2" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1216.0" y1="700.2" x2="1216.0" y2="798.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1216.0" y1="798.0" x2="1520.0" y2="798.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1520.0" y1="798.0" x2="1520.0" y2="854.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="1520.0,854.0 1513.3,838.0 1526.7,838.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1608.0" y1="940.0" x2="1664.0" y2="940.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1664.0" y1="940.0" x2="1664.0" y2="1228.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1664.0" y1="1228.0" x2="1720.0" y2="1228.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="1720.0,1228.0 1704.0,1221.3 1704.0,1234.7" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="2120.0" y1="1179.8" x2="2761.6" y2="1179.8" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="2761.6,1179.8 2745.6,1173.1 2745.6,1186.6" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="2120.0" y1="1276.2" x2="2761.6" y2="1276.2" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="2761.6,1276.2 2745.6,1269.4 2745.6,1282.9" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1408.0" y1="1673.6" x2="1408.0" y2="1362.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1408.0" y1="1362.0" x2="896.0" y2="1362.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="896.0" y1="1362.0" x2="896.0" y2="738.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1408.0" y1="1362.0" x2="1296.0" y2="1362.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1296.0" y1="1362.0" x2="1296.0" y2="1026.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1408.0" y1="1362.0" x2="1920.0" y2="1362.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<line x1="1920.0" y1="1362.0" x2="1920.0" y2="1314.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="896.0,738.0 889.3,754.0 902.7,754.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<polygon points="1296.0,1026.0 1289.3,1042.0 1302.7,1042.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<polygon points="1920.0,1314.0 1913.3,1330.0 1926.7,1330.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<line x1="1520.0" y1="1673.6" x2="1520.0" y2="1026.0" stroke="#000" stroke-width="2.00" stroke-linecap="square"/>
|
||||
<polygon points="1520.0,1026.0 1513.3,1042.0 1526.7,1042.0" fill="#000000" stroke="#000000" stroke-width="2.00"/>
|
||||
<rect x="42.4" y="627.0" width="23.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="54.1" y="637.7" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">I1</text>
|
||||
<rect x="42.4" y="866.8" width="23.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="54.1" y="877.5" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">I2</text>
|
||||
<rect x="1306.0" y="234.4" width="31.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1321.7" y="245.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">C1</text>
|
||||
<rect x="1018.0" y="234.4" width="31.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1033.7" y="245.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">C2</text>
|
||||
<rect x="2042.0" y="234.4" width="31.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="2057.7" y="245.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">C3</text>
|
||||
<rect x="2724.2" y="1154.8" width="33.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="2740.9" y="1165.5" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">O1</text>
|
||||
<rect x="2724.2" y="1251.2" width="33.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="2740.9" y="1261.9" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">O2</text>
|
||||
<rect x="1418.0" y="1648.2" width="34.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1435.2" y="1658.9" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">M1</text>
|
||||
<rect x="1530.0" y="1648.2" width="34.4" height="21.4" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1547.2" y="1658.9" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="700" fill="#000000">M2</text>
|
||||
<rect x="314.4" y="581.9" width="79.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="354.0" y="596.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Спрос</text>
|
||||
<rect x="518.2" y="794.9" width="163.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="599.8" y="809.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Предложения</text>
|
||||
<text x="599.8" y="836.0" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">поставщиков</text>
|
||||
<rect x="1211.9" y="295.0" width="168.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1296.0" y="309.3" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Нормативная</text>
|
||||
<text x="1296.0" y="336.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">документация</text>
|
||||
<rect x="1040.0" y="392.6" width="195.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1137.6" y="406.8" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Ассортиментная</text>
|
||||
<text x="1137.6" y="433.7" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">политика</text>
|
||||
<rect x="1889.8" y="635.5" width="110.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1944.9" y="649.7" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Условия</text>
|
||||
<text x="1944.9" y="676.6" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">поставки</text>
|
||||
<rect x="1122.9" y="533.8" width="155.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1200.5" y="548.0" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">план закупок</text>
|
||||
<rect x="1165.6" y="603.2" width="110.2" height="65.0" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1220.7" y="617.4" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">график</text>
|
||||
<text x="1220.7" y="644.3" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">поставок</text>
|
||||
<rect x="1634.9" y="869.9" width="192.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1731.0" y="884.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">товар к отгрузке</text>
|
||||
<rect x="2388.9" y="1109.8" width="78.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="2428.0" y="1124.0" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Товар</text>
|
||||
<rect x="2352.9" y="1206.1" width="150.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="2428.0" y="1220.3" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Акт отгрузки</text>
|
||||
<rect x="1256.8" y="1505.0" width="119.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1316.4" y="1519.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Персонал</text>
|
||||
<rect x="1407.8" y="1279.0" width="80.2" height="38.1" fill="#ffffff" stroke="none" stroke-width="0.00"/>
|
||||
<text x="1447.9" y="1293.2" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="24.0" font-weight="400" fill="#000000">Склад</text>
|
||||
<rect x="704.0" y="574.0" width="400.0" height="172.0" fill="#c0c0c0" stroke="none" stroke-width="0.00"/>
|
||||
<rect x="696.0" y="566.0" width="400.0" height="172.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
||||
<polygon points="696.0,566.0 712.0,566.0 696.0,582.0" fill="#000000" stroke="#000000" stroke-width="1.00"/>
|
||||
<text x="896.0" y="646.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="26.0" font-weight="400" fill="#000000">Принять и оценить спрос</text>
|
||||
<text x="1084.0" y="728.0" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="400" fill="#000000">A1</text>
|
||||
<rect x="1216.0" y="862.0" width="400.0" height="172.0" fill="#c0c0c0" stroke="none" stroke-width="0.00"/>
|
||||
<rect x="1208.0" y="854.0" width="400.0" height="172.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
||||
<polygon points="1208.0,854.0 1224.0,854.0 1208.0,870.0" fill="#000000" stroke="#000000" stroke-width="1.00"/>
|
||||
<text x="1408.0" y="934.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="26.0" font-weight="400" fill="#000000">Закупить и хранить</text>
|
||||
<text x="1596.0" y="1016.0" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="400" fill="#000000">A2</text>
|
||||
<rect x="1728.0" y="1150.0" width="400.0" height="172.0" fill="#c0c0c0" stroke="none" stroke-width="0.00"/>
|
||||
<rect x="1720.0" y="1142.0" width="400.0" height="172.0" fill="#ffffff" stroke="#000000" stroke-width="2.00"/>
|
||||
<polygon points="1720.0,1142.0 1736.0,1142.0 1720.0,1158.0" fill="#000000" stroke="#000000" stroke-width="1.00"/>
|
||||
<text x="1920.0" y="1222.1" dx="0" dy="0.35em" text-anchor="middle" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="26.0" font-weight="400" fill="#000000">Отгрузить товар</text>
|
||||
<text x="2108.0" y="1304.0" dx="0" dy="0" text-anchor="end" dominant-baseline="alphabetic" font-family="Arial, DejaVu Sans, sans-serif" font-size="20.0" font-weight="400" fill="#000000">A3</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 24 KiB |
@@ -112,6 +112,7 @@ def test_write_dfd_outputs_uses_local_or_kroki(tmp_path):
|
||||
return True
|
||||
|
||||
with patch("md2gost.dfd._compile_dot", return_value=(fake_dot, False)), \
|
||||
patch("md2gost.dfd._render_dot_local_combined", return_value=set()), \
|
||||
patch("md2gost.dfd._render_dot_local", side_effect=fake_local), \
|
||||
patch("md2gost.dfd._render_dot_kroki", return_value=False):
|
||||
assert write_dfd_outputs(SAMPLE_LEVEL0, png, svg, scale=1)
|
||||
@@ -119,6 +120,64 @@ def test_write_dfd_outputs_uses_local_or_kroki(tmp_path):
|
||||
assert "Клиент" in svg.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_write_dfd_outputs_one_graphviz_run_for_both_formats(tmp_path):
|
||||
"""PNG + SVG come from a single Graphviz process; no per-format retry."""
|
||||
png = tmp_path / "a.png"
|
||||
svg = tmp_path / "a.svg"
|
||||
single = []
|
||||
|
||||
def fake_combined(dot, targets, *, context):
|
||||
for out_path, fmt in targets:
|
||||
Path(out_path).write_bytes(
|
||||
b"\x89PNG\r\n\x1a\n" + b"\x00" * 32 if fmt == "png" else b"<svg/>"
|
||||
)
|
||||
return {fmt for _, fmt in targets}
|
||||
|
||||
with patch("md2gost.dfd._compile_dot", return_value=("digraph G {}", False)), \
|
||||
patch("md2gost.dfd._render_dot_local_combined", side_effect=fake_combined), \
|
||||
patch("md2gost.dfd._render_dot_local", side_effect=lambda *a, **k: single.append(a) or True), \
|
||||
patch("md2gost.dfd._render_dot_kroki", return_value=False):
|
||||
assert write_dfd_outputs(SAMPLE_LEVEL0, png, svg, scale=1)
|
||||
assert single == []
|
||||
assert png.read_bytes().startswith(b"\x89PNG")
|
||||
assert svg.read_bytes().startswith(b"<svg")
|
||||
|
||||
|
||||
def test_write_dfd_outputs_retries_format_missed_by_combined(tmp_path):
|
||||
"""If the combined run only produced PNG, SVG still goes through the old path."""
|
||||
png = tmp_path / "a.png"
|
||||
svg = tmp_path / "a.svg"
|
||||
retried = []
|
||||
|
||||
def fake_combined(dot, targets, *, context):
|
||||
for out_path, fmt in targets:
|
||||
if fmt == "png":
|
||||
Path(out_path).write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 32)
|
||||
return {"png"}
|
||||
|
||||
def fake_local(dot, out_path, fmt, *, context):
|
||||
retried.append(fmt)
|
||||
Path(out_path).write_bytes(b"<svg/>")
|
||||
return True
|
||||
|
||||
with patch("md2gost.dfd._compile_dot", return_value=("digraph G {}", False)), \
|
||||
patch("md2gost.dfd._render_dot_local_combined", side_effect=fake_combined), \
|
||||
patch("md2gost.dfd._render_dot_local", side_effect=fake_local), \
|
||||
patch("md2gost.dfd._render_dot_kroki", return_value=False):
|
||||
assert write_dfd_outputs(SAMPLE_LEVEL0, png, svg, scale=1)
|
||||
assert retried == ["svg"]
|
||||
assert svg.read_bytes().startswith(b"<svg")
|
||||
|
||||
|
||||
def test_render_dot_local_combined_single_target_is_noop():
|
||||
from md2gost.dfd import _render_dot_local_combined
|
||||
|
||||
with patch("md2gost.dfd.resolve_graphviz_engine") as resolve:
|
||||
assert _render_dot_local_combined("digraph G {}", [(Path("a.png"), "png")],
|
||||
context=False) == set()
|
||||
resolve.assert_not_called()
|
||||
|
||||
|
||||
def test_write_dfd_outputs_falls_back_to_kroki(tmp_path):
|
||||
png = tmp_path / "a.png"
|
||||
|
||||
|
||||
@@ -16,11 +16,18 @@ from md2gost.diagram_includes import (
|
||||
)
|
||||
from md2gost.diagram_renderer import (
|
||||
DIAGRAM_LANGS,
|
||||
PLANTUML_CACHE_PREFIX,
|
||||
PLANTUML_VERSION,
|
||||
_cache_paths,
|
||||
_PumlJob,
|
||||
_plantuml_run,
|
||||
configure_diagrams,
|
||||
diagram_engine_status,
|
||||
is_diagram_lang,
|
||||
prepare_source,
|
||||
register_pending_diagram,
|
||||
render_diagram,
|
||||
reset_diagram_batch,
|
||||
resolve_plantuml_jar,
|
||||
)
|
||||
from md2gost.diagram_schemes import (
|
||||
@@ -558,6 +565,222 @@ def test_attach_svg_blip():
|
||||
assert len(image_rels) >= 2
|
||||
|
||||
|
||||
class _FakeProc:
|
||||
returncode = 0
|
||||
stdout = ""
|
||||
stderr = ""
|
||||
|
||||
|
||||
def _fake_java(*, out_stem: str = "src", fail: bool = False):
|
||||
"""Stand-in for `java -jar plantuml.jar`: writes what the real jar would."""
|
||||
calls: list[list[str]] = []
|
||||
|
||||
def run(cmd, **kwargs):
|
||||
calls.append(list(cmd))
|
||||
if fail:
|
||||
proc = _FakeProc()
|
||||
proc.returncode = 1
|
||||
proc.stderr = "boom"
|
||||
return proc
|
||||
fmt = next(a[2:] for a in cmd if a.startswith("-t"))
|
||||
payload = (
|
||||
b"\x89PNG\r\n\x1a\n" + b"\x00" * 32 if fmt == "png"
|
||||
else b"<svg xmlns='http://www.w3.org/2000/svg'/>"
|
||||
)
|
||||
for arg in cmd:
|
||||
if not str(arg).endswith(".puml"):
|
||||
continue
|
||||
out_dir = Path(arg).parent / "out"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
(out_dir / f"{out_stem}.{fmt}").write_bytes(payload)
|
||||
return _FakeProc()
|
||||
|
||||
return run, calls
|
||||
|
||||
|
||||
def _dummy_jar(tmp_path: Path) -> Path:
|
||||
jar = tmp_path / "plantuml.jar"
|
||||
jar.write_bytes(b"PK" + b"\x00" * 2000)
|
||||
return jar
|
||||
|
||||
|
||||
def test_plantuml_run_batches_sources_into_one_jvm(tmp_path):
|
||||
jar = _dummy_jar(tmp_path)
|
||||
jobs = [_PumlJob(f"@startuml\nA{i} -> B{i}\n@enduml", tmp_path / f"{i}.png")
|
||||
for i in range(3)]
|
||||
run, calls = _fake_java()
|
||||
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||
done = _plantuml_run(jobs, str(jar), "png")
|
||||
assert done == [True, True, True]
|
||||
assert len(calls) == 1
|
||||
assert sum(1 for a in calls[0] if str(a).endswith(".puml")) == 3
|
||||
for job in jobs:
|
||||
assert job.out_path.read_bytes().startswith(b"\x89PNG")
|
||||
|
||||
|
||||
def test_plantuml_run_keeps_named_diagrams_apart(tmp_path):
|
||||
"""`@startuml Name` renames the output file — each source needs its own -o dir."""
|
||||
jar = _dummy_jar(tmp_path)
|
||||
jobs = [_PumlJob("@startuml Name\nA -> B\n@enduml", tmp_path / f"{i}.png")
|
||||
for i in range(2)]
|
||||
run, calls = _fake_java(out_stem="Name")
|
||||
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||
done = _plantuml_run(jobs, str(jar), "png")
|
||||
assert done == [True, True]
|
||||
assert calls[0].count("-o") == 1
|
||||
src_dirs = {Path(a).parent for a in calls[0] if str(a).endswith(".puml")}
|
||||
assert len(src_dirs) == 2
|
||||
assert all(job.out_path.is_file() for job in jobs)
|
||||
|
||||
|
||||
def test_plantuml_run_failure_reports_every_job(tmp_path):
|
||||
jar = _dummy_jar(tmp_path)
|
||||
jobs = [_PumlJob("@startuml\nA -> B\n@enduml", tmp_path / f"{i}.png")
|
||||
for i in range(2)]
|
||||
run, _calls = _fake_java(fail=True)
|
||||
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||
done = _plantuml_run(jobs, str(jar), "png")
|
||||
assert done == [False, False]
|
||||
assert not any(job.out_path.exists() for job in jobs)
|
||||
|
||||
|
||||
def test_plantuml_run_without_java_never_spawns(tmp_path):
|
||||
jar = _dummy_jar(tmp_path)
|
||||
run, calls = _fake_java()
|
||||
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||
patch("md2gost.diagram_renderer._java_available", return_value=False):
|
||||
done = _plantuml_run([_PumlJob("@startuml\nA -> B\n@enduml", tmp_path / "a.png")],
|
||||
str(jar), "png")
|
||||
assert done == [False]
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_batch_prepass_draws_whole_document_in_one_jvm(tmp_path):
|
||||
jar = _dummy_jar(tmp_path)
|
||||
configure_diagrams(
|
||||
fallback="off", cache_dir=str(tmp_path), plantuml_jar=str(jar), diagram_scale=1,
|
||||
)
|
||||
register_pending_diagram("uml", "A -> B")
|
||||
register_pending_diagram("uml", "C -> D")
|
||||
run, calls = _fake_java()
|
||||
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||
first = render_diagram("uml", "A -> B")
|
||||
second = render_diagram("uml", "C -> D")
|
||||
assert len(calls) == 1
|
||||
assert first.png_path != second.png_path
|
||||
for result in (first, second):
|
||||
assert Path(result.png_path).read_bytes().startswith(b"\x89PNG")
|
||||
|
||||
|
||||
def test_batch_prepass_svg_mode_uses_two_jvms_not_four(tmp_path):
|
||||
jar = _dummy_jar(tmp_path)
|
||||
configure_diagrams(
|
||||
fallback="off", cache_dir=str(tmp_path), plantuml_jar=str(jar),
|
||||
diagram_scale=1, diagram_format="svg",
|
||||
)
|
||||
register_pending_diagram("uml", "A -> B")
|
||||
register_pending_diagram("uml", "C -> D")
|
||||
run, calls = _fake_java()
|
||||
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||
first = render_diagram("uml", "A -> B")
|
||||
second = render_diagram("uml", "C -> D")
|
||||
assert sorted(next(a for a in c if a.startswith("-t")) for c in calls) == \
|
||||
["-tpng", "-tsvg"]
|
||||
assert first.svg_path and second.svg_path
|
||||
assert Path(first.svg_path).read_bytes().startswith(b"<svg")
|
||||
|
||||
|
||||
def test_batch_prepass_skips_cached_diagrams(tmp_path):
|
||||
jar = _dummy_jar(tmp_path)
|
||||
configure_diagrams(
|
||||
fallback="off", cache_dir=str(tmp_path), plantuml_jar=str(jar), diagram_scale=1,
|
||||
)
|
||||
cached_src, _ = prepare_source("uml", "A -> B")
|
||||
cached_png, _ = _cache_paths(
|
||||
cached_src, str(tmp_path), cache_prefix=PLANTUML_CACHE_PREFIX,
|
||||
)
|
||||
cached_png.write_bytes(b"\x89PNG\r\n\x1a\n" + b"cached")
|
||||
register_pending_diagram("uml", "A -> B")
|
||||
register_pending_diagram("uml", "C -> D")
|
||||
run, calls = _fake_java()
|
||||
with patch("md2gost.diagram_renderer.subprocess.run", side_effect=run), \
|
||||
patch("md2gost.diagram_renderer._java_available", return_value=True):
|
||||
hit = render_diagram("uml", "A -> B")
|
||||
render_diagram("uml", "C -> D")
|
||||
assert len(calls) == 1
|
||||
assert sum(1 for a in calls[0] if str(a).endswith(".puml")) == 1
|
||||
assert Path(hit.png_path).read_bytes().endswith(b"cached")
|
||||
|
||||
|
||||
def test_batch_prepass_not_triggered_for_explicit_overrides(tmp_path):
|
||||
configure_diagrams(fallback="remote", cache_dir=str(tmp_path))
|
||||
register_pending_diagram("uml", "A -> B")
|
||||
|
||||
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
|
||||
out_path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 20)
|
||||
return True
|
||||
|
||||
with patch("md2gost.diagram_renderer._flush_plantuml_batch") as flush, \
|
||||
patch("md2gost.diagram_renderer._render_plantuml_jar", return_value=False), \
|
||||
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
|
||||
render_diagram("uml", "A -> B", cache_dir=str(tmp_path))
|
||||
flush.assert_not_called()
|
||||
reset_diagram_batch()
|
||||
|
||||
|
||||
def test_diagram_figure_registers_for_batch():
|
||||
from docx import Document
|
||||
from md2gost import package_dir
|
||||
import md2gost.diagram_renderer as dr
|
||||
import os
|
||||
|
||||
reset_diagram_batch()
|
||||
doc = Document(os.path.join(package_dir(), "Template.docx"))
|
||||
DiagramFigure(doc._body, "uml", "A -> B")
|
||||
DiagramFigure(doc._body, "uml", "A -> B")
|
||||
DiagramFigure(doc._body, "mermaid", "flowchart LR\nA-->B")
|
||||
assert dr._PENDING == [("uml", "A -> B"), ("mermaid", "flowchart LR\nA-->B")]
|
||||
reset_diagram_batch()
|
||||
assert dr._PENDING == []
|
||||
|
||||
|
||||
def test_plantuml_cache_key_carries_jar_version(tmp_path):
|
||||
assert PLANTUML_VERSION in PLANTUML_CACHE_PREFIX
|
||||
prepared, dtype = prepare_source("uml", "A -> B")
|
||||
assert dtype == "plantuml"
|
||||
versioned, _ = _cache_paths(
|
||||
prepared, str(tmp_path), cache_prefix=PLANTUML_CACHE_PREFIX,
|
||||
)
|
||||
legacy, _ = _cache_paths(prepared, str(tmp_path))
|
||||
assert versioned != legacy
|
||||
|
||||
|
||||
def test_stale_unversioned_cache_is_a_miss_not_a_crash(tmp_path):
|
||||
"""Files cached before the jar version entered the key must be re-rendered."""
|
||||
configure_diagrams(fallback="remote", cache_dir=str(tmp_path), diagram_scale=1)
|
||||
prepared, _ = prepare_source("uml", "A -> B")
|
||||
legacy, _ = _cache_paths(prepared, str(tmp_path))
|
||||
legacy.write_bytes(b"\x89PNG\r\n\x1a\n" + b"stale")
|
||||
|
||||
def fake_kroki(source, diagram_type, base_url, out_path, fmt="png"):
|
||||
out_path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"fresh")
|
||||
return True
|
||||
|
||||
with patch("md2gost.diagram_renderer._render_plantuml_jar", return_value=False), \
|
||||
patch("md2gost.diagram_renderer._render_kroki", side_effect=fake_kroki):
|
||||
result = render_diagram(
|
||||
"uml", "A -> B", cache_dir=str(tmp_path), diagram_scale=1,
|
||||
)
|
||||
assert Path(result.png_path) != legacy
|
||||
assert Path(result.png_path).read_bytes().endswith(b"fresh")
|
||||
assert legacy.read_bytes().endswith(b"stale")
|
||||
|
||||
|
||||
def test_resolve_plantuml_jar_explicit(tmp_path):
|
||||
jar = tmp_path / "plantuml.jar"
|
||||
jar.write_bytes(b"PK" + b"\x00" * 2000)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Пустой заголовок: конвертер не падает, но и не молчит.
|
||||
|
||||
Методичка МИРЭА (`_method_extract.txt`, строка 156): «…разделы, пункты …
|
||||
имеют заголовки, четко и кратко отражающие их содержание». Заголовок без
|
||||
текста — нарушение, а не валидный ввод: он даст в документе пронумерованный,
|
||||
но безымянный раздел, который попадёт в автособираемое содержание. Значит,
|
||||
парсер обязан такую строку пережить, а checker — сообщить о ней.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from md2gost.checker import check_markdown
|
||||
from md2gost.extended_markdown.heading import Heading
|
||||
|
||||
|
||||
MD_EMPTY_HEADING = """# *СОДЕРЖАНИЕ
|
||||
[TOC]
|
||||
|
||||
#
|
||||
|
||||
Текст раздела без заголовка.
|
||||
"""
|
||||
|
||||
MD_OK_HEADINGS = """# *СОДЕРЖАНИЕ
|
||||
[TOC]
|
||||
|
||||
# Анализ предметной области
|
||||
|
||||
Текст раздела.
|
||||
"""
|
||||
|
||||
|
||||
def _heading(source: str) -> Heading:
|
||||
match = Heading.pattern.match(source)
|
||||
assert match is not None, f"pattern did not match {source!r}"
|
||||
return Heading(match)
|
||||
|
||||
|
||||
class TestEmptyHeadingParsing(unittest.TestCase):
|
||||
def test_bare_hash_does_not_raise(self):
|
||||
heading = _heading("#\n")
|
||||
|
||||
self.assertEqual("", heading.inline_body)
|
||||
self.assertTrue(heading.numbered)
|
||||
self.assertEqual(1, heading.level)
|
||||
|
||||
def test_bare_hashes_of_deeper_level(self):
|
||||
heading = _heading("###\n")
|
||||
|
||||
self.assertEqual("", heading.inline_body)
|
||||
self.assertTrue(heading.numbered)
|
||||
self.assertEqual(3, heading.level)
|
||||
|
||||
def test_asterisk_without_text_is_unnumbered(self):
|
||||
heading = _heading("# *\n")
|
||||
|
||||
self.assertEqual("", heading.inline_body)
|
||||
self.assertFalse(heading.numbered)
|
||||
|
||||
def test_normal_headings_are_unaffected(self):
|
||||
numbered = _heading("# Анализ предметной области\n")
|
||||
self.assertEqual("Анализ предметной области", numbered.inline_body)
|
||||
self.assertTrue(numbered.numbered)
|
||||
|
||||
unnumbered = _heading("## *ВВЕДЕНИЕ\n")
|
||||
self.assertEqual("ВВЕДЕНИЕ", unnumbered.inline_body)
|
||||
self.assertFalse(unnumbered.numbered)
|
||||
|
||||
|
||||
class TestEmptyHeadingChecker(unittest.TestCase):
|
||||
def test_empty_heading_is_reported(self):
|
||||
issues = check_markdown(MD_EMPTY_HEADING, "coursework")
|
||||
|
||||
found = [i for i in issues if i.id == "heading.empty"]
|
||||
self.assertEqual(1, len(found))
|
||||
self.assertEqual("warning", found[0].severity)
|
||||
self.assertEqual(4, found[0].line)
|
||||
|
||||
def test_normal_headings_produce_no_issue(self):
|
||||
issues = check_markdown(MD_OK_HEADINGS, "coursework")
|
||||
|
||||
self.assertEqual([], [i for i in issues if i.id == "heading.empty"])
|
||||
@@ -0,0 +1,62 @@
|
||||
"""The mml2omml stylesheet is now compiled once and shared between formulas.
|
||||
|
||||
These tests pin the two properties that makes safe: the output must match a
|
||||
freshly compiled transform, and reusing the compiled object must not let one
|
||||
formula affect the next.
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import latex2mathml.converter
|
||||
from lxml import etree
|
||||
|
||||
from md2gost import package_dir
|
||||
from md2gost.latex_math import latex_to_omml
|
||||
|
||||
_FORMULAS = [
|
||||
"x^2 + y^2 = z^2",
|
||||
r"\frac{a}{b}",
|
||||
r"\frac{a + b}{c + d}",
|
||||
r"\sum_{i=1}^{n} i",
|
||||
r"\sqrt{x+1}",
|
||||
r"\int_0^1 f(x)dx",
|
||||
r"\alpha \beta \gamma",
|
||||
r"E = mc^2",
|
||||
]
|
||||
|
||||
|
||||
def _freshly_compiled(latex_equation: str) -> bytes:
|
||||
"""Pre-optimization behaviour: parse and compile the stylesheet per call."""
|
||||
mathml = latex2mathml.converter.convert(latex_equation)
|
||||
tree = etree.fromstring(mathml)
|
||||
transform = etree.XSLT(etree.parse(os.path.join(package_dir(), "mml2omml")))
|
||||
return etree.tostring(transform(tree).getroot())
|
||||
|
||||
|
||||
class TestLatexToOmml(unittest.TestCase):
|
||||
def test_matches_freshly_compiled_transform(self):
|
||||
for formula in _FORMULAS:
|
||||
with self.subTest(formula=formula):
|
||||
self.assertEqual(_freshly_compiled(formula),
|
||||
etree.tostring(latex_to_omml(formula)))
|
||||
|
||||
def test_repeated_calls_are_identical(self):
|
||||
for formula in _FORMULAS:
|
||||
with self.subTest(formula=formula):
|
||||
outputs = {etree.tostring(latex_to_omml(formula)) for _ in range(5)}
|
||||
self.assertEqual(1, len(outputs))
|
||||
|
||||
def test_interleaved_formulas_do_not_interfere(self):
|
||||
expected = {f: etree.tostring(latex_to_omml(f)) for f in _FORMULAS}
|
||||
for _ in range(3):
|
||||
for formula in reversed(_FORMULAS):
|
||||
self.assertEqual(expected[formula], etree.tostring(latex_to_omml(formula)))
|
||||
|
||||
def test_invalid_formula_still_raises_value_error(self):
|
||||
with self.assertRaises(ValueError):
|
||||
latex_to_omml(r"\frac{")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Дефис внутри гиперссылки: текст не должен дублироваться.
|
||||
|
||||
Методичка МИРЭА (`_method_extract.txt`, строки 373-376): знак дефиса «-» не
|
||||
отделяется пробелами и используется в сложных словах (во-первых,
|
||||
физико-математический, кто-либо) и в интервалах (100-150 Мб). Поэтому слово не
|
||||
должно рваться по дефису при переносе строки — вместо обычного дефиса в текст
|
||||
пишется отдельный элемент `w:noBreakHyphen`, как это уже делает
|
||||
`Paragraph.add_run`.
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import docx
|
||||
from docx.oxml.ns import qn
|
||||
|
||||
from md2gost import package_dir
|
||||
from md2gost.renderable.paragraph import Link, Paragraph
|
||||
from md2gost.styles import apply_mirea_styles
|
||||
|
||||
|
||||
def _texts(element) -> list[str]:
|
||||
return [t.text or "" for t in element.iter(qn("w:t"))]
|
||||
|
||||
|
||||
class TestLinkHyphen(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._document = docx.Document(os.path.join(package_dir(), "Template.docx"))
|
||||
self._document._body.clear_content()
|
||||
apply_mirea_styles(self._document)
|
||||
|
||||
def _hyperlink(self, text: str):
|
||||
paragraph = Paragraph(self._document._body)
|
||||
link = Link("https://example.com", paragraph._docx_paragraph)
|
||||
link.add_run(text)
|
||||
return link.element
|
||||
|
||||
def test_compound_word_is_split_around_no_break_hyphen(self):
|
||||
hyperlink = self._hyperlink("во-первых")
|
||||
|
||||
runs = hyperlink.findall(qn("w:r"))
|
||||
self.assertEqual(3, len(runs))
|
||||
self.assertEqual(
|
||||
1, len(hyperlink.findall(f'{qn("w:r")}/{qn("w:noBreakHyphen")}'))
|
||||
)
|
||||
self.assertEqual("вопервых", "".join(_texts(hyperlink)))
|
||||
for run in runs:
|
||||
self.assertNotIn("во-первых", "".join(_texts(run)))
|
||||
|
||||
def test_interval_is_split_around_no_break_hyphen(self):
|
||||
hyperlink = self._hyperlink("100-150")
|
||||
|
||||
self.assertEqual(["100", "150"], [t for t in _texts(hyperlink) if t])
|
||||
|
||||
def test_several_hyphens_produce_one_run_per_part(self):
|
||||
hyperlink = self._hyperlink("физико-математический-факультет")
|
||||
|
||||
self.assertEqual(5, len(hyperlink.findall(qn("w:r"))))
|
||||
self.assertEqual(
|
||||
"физикоматематическийфакультет", "".join(_texts(hyperlink))
|
||||
)
|
||||
|
||||
def test_text_without_hyphen_stays_one_run(self):
|
||||
hyperlink = self._hyperlink("пример")
|
||||
|
||||
self.assertEqual(1, len(hyperlink.findall(qn("w:r"))))
|
||||
self.assertEqual("пример", "".join(_texts(hyperlink)))
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Оформление листинга и флаг подсветки синтаксиса.
|
||||
|
||||
Методичка МИРЭА, табл. 4.1 (`_method_extract.txt`, строки 716-737), блок
|
||||
«Содержание листинга»: шрифт Courier New, начертание Обычный, размер 10 пт,
|
||||
цвет текста Авто (чёрный), выравнивание по левому краю, отступы слева/справа
|
||||
0 см, первая строка 0 см, интервал перед и после 0 мм, междустрочный
|
||||
одинарный.
|
||||
|
||||
Отсюда два независимых требования:
|
||||
|
||||
* с выключенной подсветкой (дефолт) содержимое листинга не должно нести
|
||||
никакого прямого форматирования на уровне run'а;
|
||||
* если пользователь всё же включил `--syntax-highlighting`, флаг обязан
|
||||
работать честно — курсив из стиля Pygments должен доезжать до XML, а checker
|
||||
обязан предупредить об отклонении от табл. 4.1.
|
||||
"""
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from contextlib import contextmanager
|
||||
|
||||
import docx
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Pt
|
||||
|
||||
from md2gost import package_dir
|
||||
from md2gost.checker import check_markdown
|
||||
from md2gost.renderable.caption import CaptionInfo
|
||||
from md2gost.renderable.listing import Listing
|
||||
from md2gost.styles import apply_mirea_styles
|
||||
|
||||
|
||||
CODE = '# комментарий\nprint("hello")\n'
|
||||
|
||||
MD_WITH_LISTING = """# *СОДЕРЖАНИЕ
|
||||
[TOC]
|
||||
|
||||
# Раздел
|
||||
|
||||
%code1 Пример кода
|
||||
|
||||
```python
|
||||
print(1)
|
||||
```
|
||||
|
||||
См. @Листинг:code1.
|
||||
"""
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _syntax_highlighting(enabled: bool):
|
||||
previous = os.environ.get("SYNTAX_HIGHLIGHTING")
|
||||
if enabled:
|
||||
os.environ["SYNTAX_HIGHLIGHTING"] = "1"
|
||||
else:
|
||||
os.environ.pop("SYNTAX_HIGHLIGHTING", None)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if previous is None:
|
||||
os.environ.pop("SYNTAX_HIGHLIGHTING", None)
|
||||
else:
|
||||
os.environ["SYNTAX_HIGHLIGHTING"] = previous
|
||||
|
||||
|
||||
class _ListingTestCase(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._document = docx.Document(os.path.join(package_dir(), "Template.docx"))
|
||||
self._document._body.clear_content()
|
||||
apply_mirea_styles(self._document)
|
||||
|
||||
def _listing(self, highlighting: bool) -> Listing:
|
||||
listing = Listing(self._document._body, "python", CaptionInfo("code1", "Пример"))
|
||||
with _syntax_highlighting(highlighting):
|
||||
listing.set_text(CODE)
|
||||
return listing
|
||||
|
||||
@staticmethod
|
||||
def _runs(listing: Listing):
|
||||
for paragraph in listing.paragraphs:
|
||||
for run in paragraph._docx_paragraph.runs:
|
||||
yield run
|
||||
|
||||
|
||||
class TestListingWithoutHighlighting(_ListingTestCase):
|
||||
"""Табл. 4.1: содержимое листинга — Обычный, чёрный, Courier New 10 пт."""
|
||||
|
||||
def test_runs_carry_no_direct_formatting(self):
|
||||
listing = self._listing(highlighting=False)
|
||||
|
||||
runs = list(self._runs(listing))
|
||||
self.assertTrue(runs)
|
||||
for run in runs:
|
||||
rPr = run._element.find(qn("w:rPr"))
|
||||
if rPr is None:
|
||||
continue
|
||||
self.assertIsNone(rPr.find(qn("w:b")), run.text)
|
||||
self.assertIsNone(rPr.find(qn("w:i")), run.text)
|
||||
self.assertIsNone(rPr.find(qn("w:color")), run.text)
|
||||
|
||||
def test_code_style_matches_table_4_1(self):
|
||||
style = self._document.styles["Code"]
|
||||
|
||||
self.assertEqual("Courier New", style.font.name)
|
||||
self.assertEqual(Pt(10), style.font.size)
|
||||
self.assertFalse(style.font.bold)
|
||||
self.assertFalse(style.font.italic)
|
||||
|
||||
pf = style.paragraph_format
|
||||
self.assertEqual(WD_ALIGN_PARAGRAPH.LEFT, pf.alignment)
|
||||
self.assertAlmostEqual(0, pf.first_line_indent.cm, places=3)
|
||||
self.assertAlmostEqual(0, pf.left_indent.cm, places=3)
|
||||
self.assertAlmostEqual(0, pf.space_before.mm, places=3)
|
||||
self.assertAlmostEqual(0, pf.space_after.mm, places=3)
|
||||
self.assertEqual(WD_LINE_SPACING.SINGLE, pf.line_spacing_rule)
|
||||
|
||||
|
||||
class TestListingWithHighlighting(_ListingTestCase):
|
||||
"""Включённый флаг обязан реально давать курсив там, где его задаёт Pygments."""
|
||||
|
||||
def test_italic_token_reaches_xml(self):
|
||||
listing = self._listing(highlighting=True)
|
||||
|
||||
italic_runs = [run for run in self._runs(listing) if run.italic]
|
||||
self.assertTrue(
|
||||
italic_runs,
|
||||
"ни один run не получил курсив, хотя стиль Pygments задаёт italic "
|
||||
"для Token.Comment",
|
||||
)
|
||||
self.assertTrue(
|
||||
any("комментарий" in run.text for run in italic_runs),
|
||||
[run.text for run in italic_runs],
|
||||
)
|
||||
|
||||
|
||||
class TestSyntaxHighlightingChecker(unittest.TestCase):
|
||||
"""Подсветка противоречит табл. 4.1 — checker обязан об этом сказать."""
|
||||
|
||||
def test_no_issue_when_flag_is_off(self):
|
||||
issues = check_markdown(MD_WITH_LISTING, "coursework")
|
||||
|
||||
self.assertEqual(
|
||||
[], [i for i in issues if i.id == "listing.syntax_highlighting"]
|
||||
)
|
||||
|
||||
def test_warning_when_flag_is_on(self):
|
||||
issues = check_markdown(
|
||||
MD_WITH_LISTING, "coursework", syntax_highlighting=True
|
||||
)
|
||||
|
||||
found = [i for i in issues if i.id == "listing.syntax_highlighting"]
|
||||
self.assertEqual(1, len(found))
|
||||
self.assertEqual("warning", found[0].severity)
|
||||
|
||||
def test_error_under_strict(self):
|
||||
issues = check_markdown(
|
||||
MD_WITH_LISTING, "coursework", syntax_highlighting=True, strict=True
|
||||
)
|
||||
|
||||
found = [i for i in issues if i.id == "listing.syntax_highlighting"]
|
||||
self.assertEqual(1, len(found))
|
||||
self.assertEqual("error", found[0].severity)
|
||||
@@ -226,7 +226,7 @@ class TestParagraphSizer(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, ps.count_lines(paragraph.runs, self._max_width - LISTING_OFFSET, paragraph.style.font, 0, True))
|
||||
|
||||
def test_count_lines_courier7(self):
|
||||
def test_count_lines_courier7_2(self):
|
||||
paragraph = self._document.add_paragraph(style="Code")
|
||||
paragraph.add_run(
|
||||
""" # and ((min(2, height_data.lines) - 1) * height_data.line_spacing + 1) * height_data.line_height\\""")
|
||||
|
||||
@@ -0,0 +1,462 @@
|
||||
"""Equivalence tests for the ParagraphSizer hot-path optimizations.
|
||||
|
||||
`_LegacyFont` and `_legacy_count_lines` are verbatim copies of the
|
||||
pre-optimization implementations. Every test here asserts that the current
|
||||
implementation produces bit-identical results, so the caching and the
|
||||
per-word (instead of per-character) width measurement cannot silently move
|
||||
line breaks.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from functools import cached_property
|
||||
from math import ceil
|
||||
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.shared import Cm, Pt
|
||||
from docx.text.font import Font as DocxFont
|
||||
from docx.text.run import Run
|
||||
from freetype import Face
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
from md2gost.renderable.find_font import find_font
|
||||
from md2gost.renderable.listing import LISTING_OFFSET
|
||||
from md2gost.renderable.paragraph_sizer import (
|
||||
Font,
|
||||
ParagraphSizer,
|
||||
_get_font,
|
||||
_merge_objects,
|
||||
measure_paragraph,
|
||||
)
|
||||
|
||||
from . import _create_test_document
|
||||
|
||||
|
||||
class _LegacyFont:
|
||||
"""Verbatim copy of Font before the metrics snapshot was introduced."""
|
||||
|
||||
def __init__(self, name: str, bold: bool, italic: bool, size_pt: int):
|
||||
path = find_font(name, bold, italic)
|
||||
self._freetypefont = ImageFont.truetype(path, size_pt)
|
||||
self._draw = ImageDraw.Draw(Image.new("RGB", (1000, 1000)))
|
||||
|
||||
self._face = Face(path)
|
||||
self._face.set_char_size(int(size_pt * 64))
|
||||
|
||||
def get_text_width(self, text: str):
|
||||
if not self.is_mono:
|
||||
bbox = self._draw.textbbox((0, 0), text, self._freetypefont)
|
||||
return Pt(bbox[2] - bbox[0])
|
||||
else:
|
||||
return Pt(len(text) * self._face.glyph.advance.x / 64)
|
||||
|
||||
def get_line_height(self):
|
||||
if "Times" in str(self._face.family_name) and self._freetypefont.size == 14:
|
||||
return Pt(16.05)
|
||||
if "Courier" in str(self._face.family_name) and self._freetypefont.size == 12:
|
||||
return Pt(13.61)
|
||||
else:
|
||||
return Pt(self._face.size.height / 64)
|
||||
|
||||
@cached_property
|
||||
def is_mono(self):
|
||||
self._face.load_char("i")
|
||||
i_width = self._face.glyph.advance.x
|
||||
self._face.load_char("m")
|
||||
return i_width == self._face.glyph.advance.x
|
||||
|
||||
|
||||
def _legacy_count_lines(runs, max_width, docx_font: DocxFont, first_line_indent, is_mono: bool = False):
|
||||
"""Verbatim copy of ParagraphSizer.count_lines before the optimization."""
|
||||
lines = 1
|
||||
line_width = first_line_indent
|
||||
|
||||
space_width = _LegacyFont(
|
||||
docx_font.name, docx_font.bold, docx_font.italic, docx_font.size.pt).get_text_width(" ")
|
||||
if not is_mono:
|
||||
space_width *= 0.81
|
||||
|
||||
word_part = ""
|
||||
word_parts_widths = [0]
|
||||
spaces = 0
|
||||
for i, run in enumerate(runs):
|
||||
if word_part:
|
||||
word_part = ""
|
||||
word_parts_widths.append(0)
|
||||
|
||||
run_docx_font = _merge_objects(docx_font, run.font)
|
||||
font = _LegacyFont(run_docx_font.name, run_docx_font.bold,
|
||||
run_docx_font.italic, run_docx_font.size.pt)
|
||||
|
||||
run_text = run.text
|
||||
if run_text == "" and run._element.xpath("w:noBreakHyphen"):
|
||||
run_text = "-"
|
||||
if i == len(runs) - 1:
|
||||
run_text += " "
|
||||
|
||||
for c in run_text:
|
||||
if c == " ":
|
||||
if any(word_parts_widths):
|
||||
width = spaces * space_width + sum(word_parts_widths)
|
||||
if width <= max_width - line_width:
|
||||
line_width += width
|
||||
elif width > max_width - first_line_indent:
|
||||
if lines == 1 and line_width == first_line_indent and not spaces:
|
||||
lines += ceil((width - (max_width - first_line_indent)) / max_width)
|
||||
line_width = (width - (max_width - first_line_indent)) % max_width
|
||||
else:
|
||||
lines += ceil(width / max_width)
|
||||
line_width = width % max_width
|
||||
else:
|
||||
lines += 1
|
||||
line_width = sum(word_parts_widths)
|
||||
|
||||
word_part = ""
|
||||
word_parts_widths = [0]
|
||||
spaces = 1
|
||||
else:
|
||||
spaces += 1
|
||||
else:
|
||||
word_part += c
|
||||
word_parts_widths[-1] = font.get_text_width(word_part)
|
||||
|
||||
return int(lines)
|
||||
|
||||
|
||||
_BODY = (
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam lacinia fringilla lectus, "
|
||||
"nec euismod odio convallis sed. Nunc ac libero ultricies, condimentum neque et, "
|
||||
"fermentum urna. Donec feugiat diam sed nulla rutrum, sit amet accumsan odio tempor."
|
||||
)
|
||||
|
||||
# (name, list of run texts). Single-element lists are single-run paragraphs.
|
||||
_SINGLE_RUN_CASES = [
|
||||
("body", _BODY),
|
||||
("empty", ""),
|
||||
("one_space", " "),
|
||||
("only_spaces", " "),
|
||||
("one_word", "word"),
|
||||
("leading_spaces", " leading spaces after indent"),
|
||||
("trailing_spaces", "trailing spaces before end "),
|
||||
("double_spaces", "double spaces between words"),
|
||||
("long_word", "verylongword" * 13),
|
||||
("word_then_long_word", "someword " + "verylongword" * 13),
|
||||
("long_word_then_word", "verylongword" * 13 + " someword"),
|
||||
("cyrillic", "Электроэнцефалографический анализ показал устойчивую динамику показателей."),
|
||||
("cyrillic_long", "Электроэнцефалографический " * 8),
|
||||
("punctuation_only", ". , ; : ! ?"),
|
||||
("em_dash_tail", "OpenAI is a leading research organization. Hello world —"),
|
||||
("code_line", " table._cells[0]._element.append(paragraph_rendered_info.docx_element._element)"),
|
||||
("code_short", " continuation_paragraph = Paragraph(self.parent)"),
|
||||
("code_ooo", "ooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo"),
|
||||
("code_comment", " # and ((min(2, height_data.lines) - 1) * height_data.line_spacing + 1)\\"),
|
||||
("tabs_as_spaces", " def add_link(self, text: str, url: str, is_bold: bool = None):"),
|
||||
]
|
||||
|
||||
_MULTI_RUN_CASES = [
|
||||
("split_mid_word", ["verylong", "wordthat", "spansruns and then more text follows here"]),
|
||||
("split_mid_word_long", ["verylongword" * 6, "verylongword" * 7]),
|
||||
("empty_runs_interleaved", ["", "hello", "", " ", "", "world", ""]),
|
||||
("space_owned_by_own_run", ["hello", " ", "world", " ", "again"]),
|
||||
("code_tokens", ["", " ", "run", " ", "=", " ", "DocxRun", "(", "create_element",
|
||||
"(", '"', "w:r", '"', ")", ",", " ", "self", ".", "_docx_paragraph", ")"]),
|
||||
("code_tokens2", ["", " ", "-", ">", " ", "Generator", "[", "RenderedInfo", " ",
|
||||
"|", " ", "Renderable", ",", " ", "None", ",", " ", "None", "]", ":"]),
|
||||
("trailing_empty_run", ["some words here", ""]),
|
||||
("trailing_space_run", ["some words here", " "]),
|
||||
("all_empty", ["", "", ""]),
|
||||
("word_ends_exactly_at_run_end", ["complete", " ", "words", " ", "only"]),
|
||||
("many_fragments_one_word", list("supercalifragilisticexpialidocious")),
|
||||
("long_text_split_in_three", [_BODY[:90], _BODY[90:180], _BODY[180:]]),
|
||||
]
|
||||
|
||||
# (bold, italic) per run, cycled - exercises the per-run font merge path
|
||||
_STYLE_CYCLE = [(None, None), (True, None), (None, True), (True, True)]
|
||||
|
||||
|
||||
class TestCountLinesEquivalence(unittest.TestCase):
|
||||
"""New count_lines must return exactly what the legacy implementation returned."""
|
||||
|
||||
def setUp(self):
|
||||
self._document, self._max_height, self._max_width = _create_test_document()
|
||||
|
||||
def _assert_same(self, paragraph, max_width, first_line_indent, is_mono):
|
||||
sizer = ParagraphSizer(paragraph, None, max_width)
|
||||
runs = paragraph.runs
|
||||
docx_font = paragraph.style.font
|
||||
expected = _legacy_count_lines(runs, max_width, docx_font, first_line_indent, is_mono)
|
||||
actual = sizer.count_lines(runs, max_width, docx_font, first_line_indent, is_mono)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
def test_single_run_normal_style(self):
|
||||
for name, text in _SINGLE_RUN_CASES:
|
||||
for indent in (0, Cm(1.25)):
|
||||
with self.subTest(case=name, indent=indent):
|
||||
paragraph = self._document.add_paragraph()
|
||||
paragraph.add_run(text)
|
||||
self._assert_same(paragraph, self._max_width, indent, False)
|
||||
|
||||
def test_single_run_code_style(self):
|
||||
max_width = self._max_width - LISTING_OFFSET
|
||||
for name, text in _SINGLE_RUN_CASES:
|
||||
for is_mono in (False, True):
|
||||
with self.subTest(case=name, is_mono=is_mono):
|
||||
paragraph = self._document.add_paragraph(style="Code")
|
||||
paragraph.add_run(text)
|
||||
self._assert_same(paragraph, max_width, 0, is_mono)
|
||||
|
||||
def test_single_run_narrow_width(self):
|
||||
"""Narrow columns push almost every word onto its own line."""
|
||||
for name, text in _SINGLE_RUN_CASES:
|
||||
with self.subTest(case=name):
|
||||
paragraph = self._document.add_paragraph()
|
||||
paragraph.add_run(text)
|
||||
self._assert_same(paragraph, Cm(3), 0, False)
|
||||
|
||||
def test_multi_run_normal_style(self):
|
||||
for name, texts in _MULTI_RUN_CASES:
|
||||
for indent in (0, Cm(1.25)):
|
||||
with self.subTest(case=name, indent=indent):
|
||||
paragraph = self._document.add_paragraph()
|
||||
for text in texts:
|
||||
paragraph.add_run(text)
|
||||
self._assert_same(paragraph, self._max_width, indent, False)
|
||||
|
||||
def test_multi_run_code_style(self):
|
||||
max_width = self._max_width - LISTING_OFFSET
|
||||
for name, texts in _MULTI_RUN_CASES:
|
||||
with self.subTest(case=name):
|
||||
paragraph = self._document.add_paragraph(style="Code")
|
||||
for text in texts:
|
||||
paragraph.add_run(text)
|
||||
self._assert_same(paragraph, max_width, 0, True)
|
||||
|
||||
def test_multi_run_mixed_bold_italic(self):
|
||||
"""Different fonts per run: word fragments must be measured with their own font."""
|
||||
for name, texts in _MULTI_RUN_CASES:
|
||||
with self.subTest(case=name):
|
||||
paragraph = self._document.add_paragraph()
|
||||
for i, text in enumerate(texts):
|
||||
run = paragraph.add_run(text)
|
||||
run.font.bold, run.font.italic = _STYLE_CYCLE[i % len(_STYLE_CYCLE)]
|
||||
self._assert_same(paragraph, self._max_width, Cm(1.25), False)
|
||||
|
||||
def test_no_break_hyphen_runs(self):
|
||||
"""An empty run carrying w:noBreakHyphen is measured as a literal '-'."""
|
||||
for texts in (["во", "", "первых"],
|
||||
["физико", "", "математический анализ данных"],
|
||||
["", "", ""],
|
||||
["интервал 100", "", "150 Мб и далее по тексту"]):
|
||||
with self.subTest(texts=texts):
|
||||
paragraph = self._document.add_paragraph()
|
||||
for text in texts:
|
||||
run = paragraph.add_run(text)
|
||||
if text == "":
|
||||
run._element.append(OxmlElement("w:noBreakHyphen"))
|
||||
self._assert_same(paragraph, self._max_width, Cm(1.25), False)
|
||||
|
||||
def test_widths_measured_once_per_word(self):
|
||||
"""Guards the optimization itself: no per-character re-measurement."""
|
||||
paragraph = self._document.add_paragraph()
|
||||
text = "alpha bravo charlie delta echo foxtrot"
|
||||
paragraph.add_run(text)
|
||||
|
||||
calls = []
|
||||
original = Font.get_text_width
|
||||
|
||||
def counting(self, value):
|
||||
calls.append(value)
|
||||
return original(self, value)
|
||||
|
||||
Font.get_text_width = counting
|
||||
try:
|
||||
ParagraphSizer(paragraph, None, self._max_width).count_lines(
|
||||
paragraph.runs, self._max_width, paragraph.style.font, Cm(1.25))
|
||||
finally:
|
||||
Font.get_text_width = original
|
||||
|
||||
words = text.split(" ")
|
||||
# one call for the space width, then one per whole word
|
||||
self.assertEqual([" "] + words, calls)
|
||||
self.assertLess(len(calls), len(text.replace(" ", "")))
|
||||
|
||||
|
||||
class TestCalculateHeightEquivalence(unittest.TestCase):
|
||||
"""Full calculate_height results for a representative document must not move."""
|
||||
|
||||
def setUp(self):
|
||||
self._document, self._max_height, self._max_width = _create_test_document()
|
||||
|
||||
def _result_tuple(self, paragraph, max_width):
|
||||
result = ParagraphSizer(paragraph, None, max_width).calculate_height()
|
||||
return (result.lines, int(result.line_height), result.line_spacing,
|
||||
int(result.before), int(result.after), int(result.base), int(result.full))
|
||||
|
||||
def test_repeated_calls_are_stable(self):
|
||||
"""Shared Font instances must not accumulate state across calls."""
|
||||
paragraph = self._document.add_paragraph()
|
||||
paragraph.add_run(_BODY)
|
||||
first = self._result_tuple(paragraph, self._max_width)
|
||||
for _ in range(5):
|
||||
self.assertEqual(first, self._result_tuple(paragraph, self._max_width))
|
||||
|
||||
def test_interleaved_fonts_do_not_interfere(self):
|
||||
"""Alternating between cached fonts must not change either one's output."""
|
||||
body = self._document.add_paragraph()
|
||||
body.add_run(_BODY)
|
||||
code = self._document.add_paragraph(style="Code")
|
||||
code.add_run(" table._cells[0]._element.append(rendered_info.docx_element._element)")
|
||||
|
||||
body_first = self._result_tuple(body, self._max_width)
|
||||
code_first = self._result_tuple(code, self._max_width - LISTING_OFFSET)
|
||||
|
||||
for _ in range(3):
|
||||
self.assertEqual(code_first, self._result_tuple(code, self._max_width - LISTING_OFFSET))
|
||||
self.assertEqual(body_first, self._result_tuple(body, self._max_width))
|
||||
|
||||
|
||||
class TestFontCache(unittest.TestCase):
|
||||
def test_same_descriptor_returns_same_instance(self):
|
||||
self.assertIs(_get_font("Times New Roman", False, False, 14),
|
||||
_get_font("Times New Roman", False, False, 14))
|
||||
|
||||
def test_different_descriptors_return_different_instances(self):
|
||||
base = _get_font("Times New Roman", False, False, 14)
|
||||
self.assertIsNot(base, _get_font("Times New Roman", True, False, 14))
|
||||
self.assertIsNot(base, _get_font("Times New Roman", False, True, 14))
|
||||
self.assertIsNot(base, _get_font("Times New Roman", False, False, 12))
|
||||
self.assertIsNot(base, _get_font("Courier New", False, False, 14))
|
||||
|
||||
def test_cached_font_matches_fresh_font(self):
|
||||
descriptors = [
|
||||
("Times New Roman", False, False, 14),
|
||||
("Times New Roman", True, False, 14),
|
||||
("Times New Roman", False, True, 14),
|
||||
("Times New Roman", True, True, 14),
|
||||
("Courier New", False, False, 12),
|
||||
("Calibri", False, False, 14),
|
||||
]
|
||||
samples = ["", " ", "i", "m", "hello", "in", "Электроэнцефалографический",
|
||||
" ", "a b c", "verylongword" * 5, "()[]{}", "0123456789"]
|
||||
for descriptor in descriptors:
|
||||
legacy = _LegacyFont(*descriptor)
|
||||
cached = _get_font(*descriptor)
|
||||
with self.subTest(descriptor=descriptor):
|
||||
self.assertEqual(legacy.is_mono, cached.is_mono)
|
||||
self.assertEqual(legacy.get_line_height(), cached.get_line_height())
|
||||
for sample in samples:
|
||||
self.assertEqual(legacy.get_text_width(sample),
|
||||
cached.get_text_width(sample),
|
||||
msg=f"{descriptor} {sample!r}")
|
||||
|
||||
def test_mono_width_is_independent_of_glyph_slot(self):
|
||||
"""get_text_width must not depend on whichever glyph was loaded last."""
|
||||
font = _get_font("Courier New", False, False, 12)
|
||||
self.assertTrue(font.is_mono)
|
||||
expected = font.get_text_width("hello world")
|
||||
for char in "iMW._@":
|
||||
font._face.load_char(char)
|
||||
self.assertEqual(expected, font.get_text_width("hello world"))
|
||||
|
||||
def test_font_holds_no_per_call_state(self):
|
||||
"""Repeated and interleaved measurements return identical values."""
|
||||
font = _get_font("Times New Roman", False, False, 14)
|
||||
other = _get_font("Courier New", False, False, 12)
|
||||
widths = {text: font.get_text_width(text) for text in ("a", "ab", "abc", "hello world")}
|
||||
for _ in range(3):
|
||||
other.get_text_width("hello world")
|
||||
for text, width in widths.items():
|
||||
self.assertEqual(width, font.get_text_width(text))
|
||||
|
||||
def test_public_font_constructor_still_works(self):
|
||||
"""Font itself stays constructible - existing tests instantiate it directly."""
|
||||
font = Font("Times New Roman", False, False, 14)
|
||||
self.assertFalse(font.is_mono)
|
||||
self.assertGreater(font.get_text_width("hello"), 0)
|
||||
|
||||
|
||||
class TestMeasureParagraphCache(unittest.TestCase):
|
||||
"""measure_paragraph() decides page breaks, so a hit must equal a fresh run."""
|
||||
|
||||
def setUp(self):
|
||||
self._document, self._max_height, self._max_width = _create_test_document()
|
||||
|
||||
def _fresh(self, paragraph, previous, max_width):
|
||||
return ParagraphSizer(paragraph, previous, max_width).calculate_height()
|
||||
|
||||
def test_hit_equals_fresh_measurement(self):
|
||||
cache = {}
|
||||
paragraph = self._document.add_paragraph()
|
||||
paragraph.add_run(_BODY)
|
||||
first = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||
for _ in range(3):
|
||||
self.assertEqual(self._fresh(paragraph, None, self._max_width), first)
|
||||
self.assertEqual(first, measure_paragraph(cache, paragraph, None, self._max_width))
|
||||
|
||||
def test_edited_text_is_remeasured(self):
|
||||
cache = {}
|
||||
paragraph = self._document.add_paragraph()
|
||||
run = paragraph.add_run("short")
|
||||
short = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||
run.text = _BODY
|
||||
long = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||
self.assertGreater(long.lines, short.lines)
|
||||
self.assertEqual(self._fresh(paragraph, None, self._max_width), long)
|
||||
|
||||
def test_edited_spacing_is_remeasured(self):
|
||||
cache = {}
|
||||
paragraph = self._document.add_paragraph()
|
||||
paragraph.add_run(_BODY)
|
||||
before = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||
paragraph.paragraph_format.space_before = Pt(12)
|
||||
after = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||
self.assertNotEqual(int(before.before), int(after.before))
|
||||
self.assertEqual(self._fresh(paragraph, None, self._max_width), after)
|
||||
|
||||
def test_width_is_part_of_the_key(self):
|
||||
cache = {}
|
||||
paragraph = self._document.add_paragraph()
|
||||
paragraph.add_run(_BODY)
|
||||
wide = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||
narrow = measure_paragraph(cache, paragraph, None, self._max_width // 2)
|
||||
self.assertGreater(narrow.lines, wide.lines)
|
||||
|
||||
def test_predecessor_spacing_is_part_of_the_key(self):
|
||||
cache = {}
|
||||
previous = self._document.add_paragraph()
|
||||
previous.add_run("previous")
|
||||
paragraph = self._document.add_paragraph()
|
||||
paragraph.paragraph_format.space_before = Pt(12)
|
||||
paragraph.add_run(_BODY)
|
||||
alone = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||
collapsed = measure_paragraph(cache, paragraph, previous, self._max_width)
|
||||
self.assertNotEqual(int(alone.before), int(collapsed.before))
|
||||
self.assertEqual(self._fresh(paragraph, previous, self._max_width), collapsed)
|
||||
|
||||
def test_result_is_a_private_copy(self):
|
||||
"""Paragraph.render() zeroes .before on a new page; the cache must not keep that."""
|
||||
cache = {}
|
||||
paragraph = self._document.add_paragraph()
|
||||
paragraph.paragraph_format.space_before = Pt(12)
|
||||
paragraph.add_run(_BODY)
|
||||
first = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||
self.assertGreater(int(first.before), 0)
|
||||
first.before = 0
|
||||
second = measure_paragraph(cache, paragraph, None, self._max_width)
|
||||
self.assertGreater(int(second.before), 0)
|
||||
|
||||
def test_attaching_the_predecessor_still_hits(self):
|
||||
"""A figure is measured detached, then again after the Renderer attaches it."""
|
||||
cache = {}
|
||||
previous = self._document.add_paragraph()
|
||||
previous.add_run("previous")
|
||||
detached = self._document.add_paragraph()
|
||||
detached.add_run(_BODY)
|
||||
measure_paragraph(cache, detached, previous, self._max_width)
|
||||
size = len(cache)
|
||||
self._document._body._element.append(previous._p)
|
||||
measure_paragraph(cache, detached, previous, self._max_width)
|
||||
self.assertEqual(size, len(cache))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -3,6 +3,8 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from md2gost.dnd import first_markdown, normalize_drop_paths, parse_tkdnd_files
|
||||
from md2gost.pipeline import (
|
||||
ConvertRequest,
|
||||
@@ -80,9 +82,8 @@ def test_parser_gui_and_optional_file():
|
||||
assert req.filename == "report.md"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="win32-only drag&drop API")
|
||||
def test_win32_drop_api_pointer_width():
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
import ctypes
|
||||
from md2gost.dnd import _win32_drop_api
|
||||
api = _win32_drop_api()
|
||||
@@ -91,9 +92,8 @@ def test_win32_drop_api_pointer_width():
|
||||
api.CallWindowProc.argtypes[0](huge) # must not OverflowError
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "win32", reason="win32-only drag&drop API")
|
||||
def test_win32_drop_hook_survives_window_messages():
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
import tkinter as tk
|
||||
from md2gost.dnd import enable_file_drop
|
||||
root = tk.Tk()
|
||||
|
||||
Reference in New Issue
Block a user