Comparing version 6.2.2 to 6.2.3
{ | ||
"name": "abcjs", | ||
"version": "6.2.2", | ||
"version": "6.2.3", | ||
"description": "Renderer for abc music notation", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -87,2 +87,5 @@ /* | ||
nbPlugins++; | ||
} else if (instrument === '') { | ||
// create a placeholder - there is no tab for this staff | ||
returned.push(null) | ||
} else { | ||
@@ -89,0 +92,0 @@ // unknown tab plugin |
@@ -120,8 +120,8 @@ var tunebook = require('./abc_tunebook'); | ||
} | ||
if (params.afterParsing) | ||
params.afterParsing(tune, tuneNumber, abcString); | ||
if (!removeDiv && params.wrap && params.staffwidth) { | ||
tune = doLineWrapping(div, tune, tuneNumber, abcString, params); | ||
tune = doLineWrapping(div, tune, tuneNumber, abcString, params); | ||
return tune; | ||
} | ||
if (params.afterParsing) | ||
params.afterParsing(tune, tuneNumber, abcString); | ||
renderOne(div, tune, params, tuneNumber, 0); | ||
@@ -149,2 +149,4 @@ if (removeDiv) | ||
} | ||
if (params.afterParsing) | ||
params.afterParsing(tune, tuneNumber, abcString); | ||
renderOne(div, tune, ret.revisedParams, tuneNumber, 0); | ||
@@ -151,0 +153,0 @@ tune.explanation = ret.explanation; |
@@ -12,2 +12,16 @@ var parseCommon = require('./abc_common'); | ||
var { | ||
legalAccents, | ||
volumeDecorations, | ||
dynamicDecorations, | ||
accentPseudonyms, | ||
accentDynamicPseudonyms, | ||
nonDecorations, | ||
durations, | ||
pitches, | ||
rests, | ||
accMap, | ||
tripletQ | ||
} = require('./abc_parse_settings') | ||
var MusicParser = function(_tokenizer, _warn, _multilineVars, _tune, _tuneBuilder, _header) { | ||
@@ -80,3 +94,2 @@ tokenizer = _tokenizer; | ||
// back-tick, space, tab: space | ||
var nonDecorations = "ABCDEFGabcdefgxyzZ[]|^_{"; // use this to prescreen so we don't have to look for a decoration at every note. | ||
@@ -545,11 +558,4 @@ var isInTie = function(multilineVars, overlayLevel, el) { | ||
// Only durations less than a whole note are tested because whole note durations have some tricky rules. | ||
var durations = [ | ||
0.5, 0.75, 0.875, 0.9375, 0.96875, 0.984375, | ||
0.25, 0.375, 0.4375, 0.46875, 0.484375, 0.4921875, | ||
0.125, 0.1875, 0.21875, 0.234375, 0.2421875, 0.24609375, | ||
0.0625, 0.09375, 0.109375, 0.1171875, 0.12109375, 0.123046875, | ||
0.03125, 0.046875, 0.0546875, 0.05859375, 0.060546875, 0.0615234375, | ||
0.015625, 0.0234375, 0.02734375, 0.029296875, 0.0302734375, 0.03076171875, | ||
]; | ||
if (el.duration < 1 && durations.indexOf(el.duration) === -1 && el.duration !== 0) { | ||
if (el.duration < 1 && durations.indexOf(el.duration) === -1 && el.duration !== 0) { | ||
if (!el.rest || el.rest.type !== 'spacer') | ||
@@ -723,32 +729,5 @@ warn("Duration not representable: " + line.substring(startI, i), line, i); | ||
var legalAccents = [ | ||
"trill", "lowermordent", "uppermordent", "mordent", "pralltriller", "accent", | ||
"fermata", "invertedfermata", "tenuto", "0", "1", "2", "3", "4", "5", "+", "wedge", | ||
"open", "thumb", "snap", "turn", "roll", "breath", "shortphrase", "mediumphrase", "longphrase", | ||
"segno", "coda", "D.S.", "D.C.", "fine", "beambr1", "beambr2", | ||
"slide", "marcato", | ||
"upbow", "downbow", "/", "//", "///", "////", "trem1", "trem2", "trem3", "trem4", | ||
"turnx", "invertedturn", "invertedturnx", "trill(", "trill)", "arpeggio", "xstem", "mark", "umarcato", | ||
"style=normal", "style=harmonic", "style=rhythm", "style=x", "style=triangle", "D.C.alcoda", "D.C.alfine", "D.S.alcoda", "D.S.alfine", "editorial", "courtesy" | ||
]; | ||
var volumeDecorations = [ | ||
"p", "pp", "f", "ff", "mf", "mp", "ppp", "pppp", "fff", "ffff", "sfz" | ||
]; | ||
var dynamicDecorations = [ | ||
"crescendo(", "crescendo)", "diminuendo(", "diminuendo)", "glissando(", "glissando)" | ||
]; | ||
var accentPseudonyms = [ | ||
["<", "accent"], [">", "accent"], ["tr", "trill"], | ||
["plus", "+"], [ "emphasis", "accent"], | ||
[ "^", "umarcato" ], [ "marcato", "umarcato" ] | ||
]; | ||
var accentDynamicPseudonyms = [ | ||
["<(", "crescendo("], ["<)", "crescendo)"], | ||
[">(", "diminuendo("], [">)", "diminuendo)"] | ||
]; | ||
var letter_to_accent = function(line, i) { | ||
@@ -884,13 +863,2 @@ var macro = multilineVars.macros[line[i]]; | ||
var tripletQ = { | ||
2: 3, | ||
3: 2, | ||
4: 3, | ||
5: 2, // TODO-PER: not handling 6/8 rhythm yet | ||
6: 2, | ||
7: 2, // TODO-PER: not handling 6/8 rhythm yet | ||
8: 3, | ||
9: 2 // TODO-PER: not handling 6/8 rhythm yet | ||
}; | ||
var letter_to_open_slurs_and_triplets = function(line, i) { | ||
@@ -1058,5 +1026,2 @@ // consume spaces, and look for all the open parens. If there is a number after the open paren, | ||
var pitches = {A: 5, B: 6, C: 0, D: 1, E: 2, F: 3, G: 4, a: 12, b: 13, c: 7, d: 8, e: 9, f: 10, g: 11}; | ||
var rests = {x: 'invisible', X: 'invisible-multimeasure', y: 'spacer', z: 'rest', Z: 'multimeasure' }; | ||
var accMap = { 'dblflat': '__', 'flat': '_', 'natural': '=', 'sharp': '^', 'dblsharp': '^^', 'quarterflat': '_/', 'quartersharp': '^/'}; | ||
var getCoreNote = function(line, index, el, canHaveBrokenRhythm) { | ||
@@ -1063,0 +1028,0 @@ //var el = { startChar: index }; |
@@ -514,2 +514,6 @@ var getNote = require('./load-note'); | ||
self.getIsRunning = function() { | ||
return self.isRunning; | ||
} | ||
/////////////// Private functions ////////////// | ||
@@ -516,0 +520,0 @@ |
@@ -20,2 +20,8 @@ var soundsCache = require('./sounds-cache'); | ||
var noteName = pitchToNoteName[sound.pitch]; | ||
if (!soundsCache[sound.instrument]) { | ||
// It shouldn't happen that the entire instrument cache wasn't created, but this has been seen in practice, so guard against it. | ||
if (debugCallback) | ||
debugCallback('placeNote skipped (instrument empty): '+sound.instrument+':'+noteName) | ||
return Promise.resolve(); | ||
} | ||
var noteBufferPromise = soundsCache[sound.instrument][noteName]; | ||
@@ -22,0 +28,0 @@ |
@@ -5,3 +5,3 @@ var SynthSequence = require('./synth-sequence'); | ||
function playEvent(midiPitches, midiGracePitches, millisecondsPerMeasure) { | ||
function playEvent(midiPitches, midiGracePitches, millisecondsPerMeasure, soundFontUrl, debugCallback) { | ||
var sequence = new SynthSequence(); | ||
@@ -25,14 +25,16 @@ | ||
return ac.resume().then(function () { | ||
return doPlay(sequence, millisecondsPerMeasure); | ||
return doPlay(sequence, millisecondsPerMeasure, soundFontUrl, debugCallback); | ||
}); | ||
} else { | ||
return doPlay(sequence, millisecondsPerMeasure); | ||
return doPlay(sequence, millisecondsPerMeasure, soundFontUrl, debugCallback); | ||
} | ||
} | ||
function doPlay(sequence, millisecondsPerMeasure) { | ||
function doPlay(sequence, millisecondsPerMeasure, soundFontUrl, debugCallback) { | ||
var buffer = new CreateSynth(); | ||
return buffer.init({ | ||
sequence: sequence, | ||
millisecondsPerMeasure: millisecondsPerMeasure | ||
millisecondsPerMeasure: millisecondsPerMeasure, | ||
options: { soundFontUrl: soundFontUrl }, | ||
debugCallback: debugCallback, | ||
}).then(function () { | ||
@@ -39,0 +41,0 @@ return buffer.prime(); |
@@ -180,6 +180,7 @@ /** | ||
staffIndex, | ||
keySig ) { | ||
keySig, | ||
tabVoiceIndex ) { | ||
var staffSize = getInitialStaffSize(staffAbsolute); | ||
var source = staffAbsolute[staffIndex+voiceIndex]; | ||
var dest = staffAbsolute[staffSize+staffIndex+voiceIndex]; | ||
var dest = staffAbsolute[tabVoiceIndex]; | ||
var tabPos = null; | ||
@@ -186,0 +187,0 @@ var defNote = null; |
@@ -236,6 +236,7 @@ /* eslint-disable no-debugger */ | ||
tabVoice.staff = staffGroupInfos; | ||
var tabVoiceIndex = voices.length | ||
voices.splice(voices.length, 0, tabVoice); | ||
var keySig = checkVoiceKeySig(voices, ii + this.staffIndex); | ||
this.tabStaff.voices[ii] = []; | ||
this.absolutes.build(this.plugin, voices, this.tabStaff.voices[ii], ii , this.staffIndex ,keySig); | ||
this.absolutes.build(this.plugin, voices, this.tabStaff.voices[ii], ii , this.staffIndex ,keySig, tabVoiceIndex); | ||
} | ||
@@ -242,0 +243,0 @@ linkStaffAndTabs(staffGroup.staffs); // crossreference tabs and staff |
@@ -54,15 +54,15 @@ // abc_parser_lint.js: Analyzes the output of abc_parse. | ||
var ParserLint = function() { | ||
"use strict"; | ||
var decorationList = { type: 'array', optional: true, items: { type: 'string', Enum: [ | ||
"trill", "lowermordent", "uppermordent", "mordent", "pralltriller", "accent", | ||
"fermata", "invertedfermata", "tenuto", "0", "1", "2", "3", "4", "5", "+", "wedge", | ||
"open", "thumb", "snap", "turn", "roll", "irishroll", "breath", "shortphrase", "mediumphrase", "longphrase", | ||
"segno", "coda", "D.S.", "D.C.", "fine", "crescendo(", "crescendo)", "diminuendo(", "diminuendo)", "glissando(", "glissando)", | ||
"p", "pp", "f", "ff", "mf", "mp", "ppp", "pppp", "fff", "ffff", "sfz", "repeatbar", "repeatbar2", "slide", | ||
"upbow", "downbow", "staccato", "trem1", "trem2", "trem3", "trem4", | ||
"/", "//", "///", "////", "turnx", "invertedturn", "invertedturnx", "arpeggio", "trill(", "trill)", "xstem", | ||
"mark", "marcato", "umarcato", "D.C.alcoda", "D.C.alfine", "D.S.alcoda", "D.S.alfine", "editorial", "courtesy" | ||
] } }; | ||
var { legalAccents } = require('../parse/abc_parse_settings'); | ||
var ParserLint = function () { | ||
'use strict'; | ||
var decorationList = { | ||
type: 'array', | ||
optional: true, | ||
items: { | ||
type: 'string', | ||
Enum: legalAccents | ||
} | ||
}; | ||
var tempoProperties = { | ||
@@ -69,0 +69,0 @@ duration: { type: "array", optional: true, output: "join", requires: [ 'bpm'], items: { type: "number"} }, |
@@ -750,2 +750,8 @@ // abc_abstract_engraver.js: Creates a data structure suitable for printing a line of abc | ||
} | ||
if (noteHead && noteHead.c === 'noteheads.triangle.quarter') { | ||
if (dir === 'down') | ||
p2 -= 0.7; | ||
else | ||
p1 -= 1.2; | ||
} | ||
abselem.addRight(new RelativeElement(null, dx, 0, p1, { "type": "stem", "pitch2": p2, linewidth: width, bottom: p1 - 1 })); | ||
@@ -752,0 +758,0 @@ //var RelativeElement = function RelativeElement(c, dx, w, pitch, opt) { |
@@ -316,2 +316,3 @@ // abc_decoration.js: Creates a data structure suitable for printing a line of abc | ||
break; | ||
case '~(': | ||
case "glissando(": | ||
@@ -321,2 +322,3 @@ this.startGlissandoX = abselem; | ||
break; | ||
case '~)': | ||
case "glissando)": | ||
@@ -323,0 +325,0 @@ glissando = { start: this.startGlissandoX, stop: abselem }; |
@@ -14,2 +14,3 @@ var sprintf = require('./sprintf'); | ||
var rightX = params.anchor2.x + params.anchor2.w / 2 | ||
var len = lineLength(leftX, leftY, rightX, rightY) | ||
@@ -16,0 +17,0 @@ var marginLeft = params.anchor1.w / 2 + margin |
function setPaperSize(renderer, maxwidth, scale, responsive) { | ||
var w = (maxwidth + renderer.padding.right) * scale; | ||
var w = (maxwidth + renderer.padding.left + renderer.padding.right) * scale; | ||
var h = (renderer.y + renderer.padding.bottom) * scale; | ||
@@ -4,0 +4,0 @@ if (renderer.isPrint) |
@@ -20,3 +20,11 @@ var sprintf = require('./sprintf'); | ||
var el = drawArc(renderer, params.startX, params.endX, params.startY + fudgeY, params.endY + fudgeY, params.above, klass, params.isTie, params.dotted); | ||
selectables.wrapSvgEl({ el_type: "slur", startChar: -1, endChar: -1 }, el); | ||
var startChar = -1 | ||
// This gets the start and end points of the contents of the slur. We assume that the parenthesis are just to the outside of that. | ||
if (params.anchor1 && !params.isTie) | ||
startChar = params.anchor1.parent.abcelem.startChar - 1 | ||
var endChar = -1 | ||
if (params.anchor2 && !params.isTie) | ||
endChar = params.anchor2.parent.abcelem.endChar + 1 | ||
selectables.wrapSvgEl({ el_type: "slur", startChar: startChar, endChar: endChar }, el); | ||
return [el]; | ||
@@ -23,0 +31,0 @@ } |
@@ -43,2 +43,3 @@ // abc_engraver_controller.js: Controls the engraving process of an ABCJS abstract syntax tree as produced by ABCJS/parse | ||
this.initialClef = params.initialClef; | ||
this.expandToWidest = !!params.expandToWidest; | ||
this.scale = params.scale ? parseFloat(params.scale) : 0; | ||
@@ -246,4 +247,9 @@ this.classes = new Classes({ shouldAddClasses: params.add_classes }); | ||
// Do all the positioning, both horizontally and vertically | ||
var maxWidth = layout(this.renderer, abcTune, this.width, this.space); | ||
var maxWidth = layout(this.renderer, abcTune, this.width, this.space, this.expandToWidest); | ||
//Set the top text now that we know the width | ||
if (this.expandToWidest && maxWidth > this.width+1) { | ||
abcTune.topText = new TopText(abcTune.metaText, abcTune.metaTextInfo, abcTune.formatting, abcTune.lines, maxWidth, this.renderer.isPrint, this.renderer.padding.left, this.renderer.spacing, this.getTextSize); | ||
} | ||
// Deal with tablature for staff | ||
@@ -250,0 +256,0 @@ if (abcTune.tablatures) { |
@@ -130,2 +130,4 @@ var spacing = require('../helpers/spacing'); | ||
function findElementInHistory(selectables, el) { | ||
if (!el) | ||
return -1; | ||
for (var i = 0; i < selectables.length; i++) { | ||
@@ -199,5 +201,9 @@ if (el.dataset.index === selectables[i].svgEl.dataset.index) | ||
// This searches up the dom for the first item containing the attribute "selectable", or stopping at the SVG. | ||
if (!target) | ||
return null; | ||
if (target.tagName === "svg") | ||
return target; | ||
if (!target.getAttribute) | ||
return null; | ||
var found = target.getAttribute("selectable"); | ||
@@ -204,0 +210,0 @@ while (!found) { |
@@ -6,3 +6,3 @@ var layoutVoice = require('./voice'); | ||
var layout = function (renderer, abctune, width, space) { | ||
var layout = function (renderer, abctune, width, space, expandToWidest) { | ||
var i; | ||
@@ -15,4 +15,10 @@ var abcLine; | ||
if (abcLine.staff) { | ||
setXSpacing(renderer, width, space, abcLine.staffGroup, abctune.formatting, i === abctune.lines.length - 1, false); | ||
if (abcLine.staffGroup.w > maxWidth) maxWidth = abcLine.staffGroup.w; | ||
// console.log("=== line", i) | ||
var thisWidth = setXSpacing(renderer, maxWidth, space, abcLine.staffGroup, abctune.formatting, i === abctune.lines.length - 1, false); | ||
// console.log(thisWidth, maxWidth) | ||
if (Math.round(thisWidth) > Math.round(maxWidth)) { // to take care of floating point weirdness | ||
maxWidth = thisWidth | ||
if (expandToWidest) | ||
i = -1 // do the calculations over with the new width | ||
} | ||
} | ||
@@ -46,3 +52,6 @@ } | ||
for (var it = 0; it < 8; it++) { // TODO-PER: shouldn't need multiple passes, but each pass gets it closer to the right spacing. (Only affects long lines: normal lines break out of this loop quickly.) | ||
// console.log("iteration", it) | ||
// dumpGroup("before", staffGroup) | ||
var ret = layoutStaffGroup(newspace, renderer, debug, staffGroup, leftEdge); | ||
// dumpGroup("after",staffGroup) | ||
newspace = calcHorizontalSpacing(isLastLine, formatting.stretchlast, width + renderer.padding.left, staffGroup.w, newspace, ret.spacingUnits, ret.minSpace, renderer.padding.left + renderer.padding.right); | ||
@@ -54,4 +63,25 @@ if (debug) | ||
centerWholeRests(staffGroup.voices); | ||
return staffGroup.w - leftEdge | ||
}; | ||
// function dumpGroup(label, staffGroup) { | ||
// var output = { | ||
// line: staffGroup.line, | ||
// w: staffGroup.w, | ||
// voice: { | ||
// i: staffGroup.voices[0].i, | ||
// minx: staffGroup.voices[0].minx, | ||
// nextx: staffGroup.voices[0].nextx, | ||
// spacingduration: staffGroup.voices[0].spacingduration, | ||
// w: staffGroup.voices[0].w, | ||
// children: [], | ||
// } | ||
// } | ||
// for (var i = 0; i < staffGroup.voices[0].children.length; i++) { | ||
// var child = staffGroup.voices[0].children[i] | ||
// output.voice.children.push({ fixedW: child.fixed.w, w: child.w, x: child.x, type: child.type }) | ||
// } | ||
// console.log(label,output) | ||
// } | ||
function calcHorizontalSpacing(isLastLine, stretchLast, targetWidth, lineWidth, spacing, spacingUnits, minSpace, padding) { | ||
@@ -58,0 +88,0 @@ if (isLastLine) { |
@@ -74,2 +74,4 @@ declare module 'abcjs' { | ||
export type TablatureInstrument = 'guitar' | 'mandolin' | 'fiddle' | 'violin' | ''; | ||
// | ||
@@ -255,2 +257,10 @@ // Basic types | ||
export interface Tablature { | ||
instrument?: TablatureInstrument, | ||
capo?: number | ||
label?: string, | ||
tuning?: Array<string>, | ||
highestNote?: string, | ||
} | ||
export interface AbcVisualParams { | ||
@@ -263,2 +273,3 @@ add_classes?: boolean; | ||
dragging?: boolean; | ||
expandToWidest?: boolean; | ||
foregroundColor?: string; | ||
@@ -288,2 +299,3 @@ format?: { [attr in FormatAttributes]?: any }; | ||
stop_on_warning?: boolean; | ||
tablature?: Array<Tablature>; | ||
textboxpadding?: number; | ||
@@ -406,3 +418,3 @@ viewportHorizontal?: boolean; | ||
// renderAbc | ||
interface NoteTimingEvent { | ||
export interface NoteTimingEvent { | ||
milliseconds: number; | ||
@@ -429,2 +441,7 @@ millisecondsPerMeasure: number; | ||
// make an alias for backwards compatibility | ||
export interface TimingEvent extends NoteTimingEvent { | ||
} | ||
export interface PercMapElement { | ||
@@ -580,2 +597,3 @@ sound: number; | ||
// Caution: The contents of this object may change at any time. If you reference this, be sure you retest for each abcjs release. | ||
export interface EngraverController { | ||
@@ -689,2 +707,3 @@ classes: any; | ||
el_type: "bar"; | ||
type: 'bar_dbl_repeat' | 'bar_right_repeat' | 'bar_left_repeat' |'bar_invisible' | 'bar_thick_thin' | 'bar_thin_thin' | 'bar_thin' | 'bar_thin_thick'; | ||
barNumber?: number; | ||
@@ -840,2 +859,3 @@ chord?: Array<ChordProperties>; | ||
makeVoicesArray: () => Array<Selectable[]> | ||
deline: () => Array<TuneLine>; | ||
lineBreaks?: Array<number>; | ||
@@ -925,18 +945,2 @@ visualTranspose?: number; | ||
export interface TimingEvent { | ||
type: "event"; | ||
milliseconds: number; | ||
millisecondsPerMeasure: number; | ||
line: number; | ||
measureNumber: number; | ||
top: number; | ||
height: number; | ||
left: number; | ||
width: number; | ||
elements: Array< HTMLElement>; | ||
startCharArray: Array<number>; | ||
endCharArray: Array<number>; | ||
midiPitches: MidiPitches | ||
} | ||
export interface TimingCallbacksPosition { | ||
@@ -1081,6 +1085,8 @@ top: number; | ||
export type EventCallback = (event: TimingEvent) => void; | ||
type EventCallbackReturn = "continue" | Promise<"continue"> | undefined | ||
export type LineEndCallback = (info : LineEndInfo, event: TimingEvent, details: LineEndDetails) => void; | ||
export type EventCallback = (event: NoteTimingEvent | null) => EventCallbackReturn; | ||
export type LineEndCallback = (info : LineEndInfo, event: NoteTimingEvent, details: LineEndDetails) => void; | ||
// Editor | ||
@@ -1139,2 +1145,3 @@ export type OnChange = (editor: Editor) => void; | ||
pauseMidi(shouldPause: boolean): void; | ||
fireChanged():void; | ||
} | ||
@@ -1164,2 +1171,3 @@ | ||
download(): string // returns audio buffer in wav format as a reference to a blob | ||
getIsRunning(): boolean | ||
} | ||
@@ -1208,3 +1216,3 @@ | ||
export function getMidiFile(source: string | TuneObject, options?: MidiFileOptions): MidiFile; | ||
export function playEvent(pitches: MidiPitches, graceNotes: MidiGracePitches | undefined, milliSecondsPerMeasure: number): Promise<void>; | ||
export function playEvent(pitches: MidiPitches, graceNotes: MidiGracePitches | undefined, milliSecondsPerMeasure: number, soundFontUrl? : string, debugCallback?: (message: string) => void): Promise<void>; | ||
export function sequence(visualObj: TuneObject, options: AbcVisualParams): AudioSequence | ||
@@ -1211,0 +1219,0 @@ } |
@@ -1,3 +0,3 @@ | ||
var version = '6.2.2'; | ||
var version = '6.2.3'; | ||
module.exports = version; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Copyleft License
License(Experimental) Copyleft license information was found.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
Non-permissive License
License(Experimental) A license not known to be considered permissive was found.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
6422347
186
51097
3
70